阳光彩印
1.协同审批管理不再需要选择审批人
2.审批管理添加审批流联调
3.销售发货、采购台账、销售报价不再需要选择审批人
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // 审æ¹ç®¡çé
ç½® |
| | | import request from "@/utils/request"; |
| | | |
| | | // æ¥è¯¢å®¡æ¹æµç¨é
ç½®èç¹å表 |
| | | export function getApproveProcessConfigNodeList(type) { |
| | | return request({ |
| | | url: '/approveProcessConfigNode/list', |
| | | method: 'get', |
| | | params: { type }, |
| | | }) |
| | | } |
| | | |
| | | // æ°å¢å®¡æ¹æµç¨é
ç½®èç¹ |
| | | export function addApproveProcessConfigNode(data) { |
| | | return request({ |
| | | url: '/approveProcessConfigNode/add', |
| | | method: 'post', |
| | | data: data, |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- 页颿 é¢ --> |
| | | <div class="page-header"> |
| | | <div class="header-title"> |
| | | <el-icon class="title-icon"><Setting /></el-icon> |
| | | <span>å®¡æ¹æµç¨é
ç½®</span> |
| | | </div> |
| | | <div class="header-desc">为ä¸å审æ¹ç±»åé
ç½®å®¡æ¹æµç¨å审æ¹äºº</div> |
| | | </div> |
| | | |
| | | <!-- 审æ¹ç±»å忢 - ç´§åæ ç¾å¼ --> |
| | | <div class="type-tabs"> |
| | | <div |
| | | v-for="type in approveTypes" |
| | | :key="type.value" |
| | | class="type-tab" |
| | | :class="{ active: activeTab === type.value }" |
| | | @click="activeTab = type.value; handleTabChange()" |
| | | > |
| | | <el-icon :size="14" :style="{ color: activeTab === type.value ? type.color : '#909399' }"> |
| | | <component :is="type.icon" /> |
| | | </el-icon> |
| | | <span class="tab-name">{{ type.label }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 主è¦å
容åºå --> |
| | | <el-card class="config-card" shadow="hover" v-loading="loading"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <div class="header-left"> |
| | | <div class="type-icon" :style="{ backgroundColor: getTypeColor(currentApproveType) }"> |
| | | <el-icon :size="20" color="#fff"><component :is="getTypeIcon(currentApproveType)" /></el-icon> |
| | | </div> |
| | | <div class="header-info"> |
| | | <span class="type-name">{{ currentApproveTypeName }}</span> |
| | | <el-tag :type="approverList.length > 0 ? 'success' : 'warning'" size="small" effect="light"> |
| | | {{ approverList.length > 0 ? `å·²é
ç½® ${approverList.length} 个审æ¹äºº` : 'æªé
置审æ¹äºº' }} |
| | | </el-tag> |
| | | </div> |
| | | </div> |
| | | <div class="header-actions" v-if="approverList.length > 0"> |
| | | <el-button @click="handleReset" size="default"> |
| | | <el-icon><RefreshLeft /></el-icon> |
| | | éç½® |
| | | </el-button> |
| | | <el-button type="primary" @click="handleSave" :loading="saveLoading" size="default"> |
| | | <el-icon><Check /></el-icon> |
| | | ä¿åé
ç½® |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- å®¡æ¹æµç¨å±ç¤º --> |
| | | <div class="flow-wrapper" v-if="approverList.length > 0"> |
| | | <div class="flow-container"> |
| | | <div |
| | | v-for="(item, index) in approverList" |
| | | :key="index" |
| | | class="flow-item" |
| | | > |
| | | <!-- 审æ¹èç¹å¡ç --> |
| | | <div class="node-card" :class="{ 'empty': !item.approverId }"> |
| | | <!-- é¡¶é¨åºå·åçº§å« --> |
| | | <div class="node-badge">{{ index + 1 }}</div> |
| | | |
| | | <!-- 头ååºå --> |
| | | <div class="node-avatar-section"> |
| | | <div |
| | | class="node-avatar" |
| | | :class="{ 'has-user': item.approverId }" |
| | | :style="item.approverId ? { backgroundColor: getAvatarColor(item.approverName) } : {}" |
| | | > |
| | | <span v-if="item.approverId">{{ item.approverName.charAt(0) }}</span> |
| | | <el-icon v-else :size="24"><User /></el-icon> |
| | | </div> |
| | | <div class="node-level">{{ getLevelText(index) }}</div> |
| | | </div> |
| | | |
| | | <!-- éæ©åºå --> |
| | | <div class="node-select-section"> |
| | | <el-select |
| | | v-model="item.approverId" |
| | | placeholder="鿩审æ¹äºº" |
| | | filterable |
| | | size="default" |
| | | @change="(val) => handleApproverChange(val, item)" |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | </div> |
| | | |
| | | <!-- æä½æé® --> |
| | | <div class="node-actions"> |
| | | <el-button |
| | | type="primary" |
| | | circle |
| | | :disabled="index === 0" |
| | | @click="moveLeft(index)" |
| | | size="small" |
| | | class="action-btn" |
| | | title="åç§»" |
| | | > |
| | | <el-icon><ArrowLeft /></el-icon> |
| | | </el-button> |
| | | <el-button |
| | | type="primary" |
| | | circle |
| | | :disabled="index === approverList.length - 1" |
| | | @click="moveRight(index)" |
| | | size="small" |
| | | class="action-btn" |
| | | title="åç§»" |
| | | > |
| | | <el-icon><ArrowRight /></el-icon> |
| | | </el-button> |
| | | <el-button |
| | | type="danger" |
| | | circle |
| | | @click="handleDelete(index)" |
| | | size="small" |
| | | class="action-btn" |
| | | > |
| | | <el-icon><Delete /></el-icon> |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- è¿æ¥ç®å¤´ --> |
| | | <div class="arrow-connector" v-if="index < approverList.length - 1"> |
| | | <div class="arrow-line"></div> |
| | | <el-icon class="arrow-icon"><ArrowRight /></el-icon> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- æ°å¢èç¹æé® - æ¾å¨æµç¨æå --> |
| | | <div class="add-node-item"> |
| | | <div class="arrow-connector" v-if="approverList.length > 0"> |
| | | <div class="arrow-line"></div> |
| | | <el-icon class="arrow-icon"><ArrowRight /></el-icon> |
| | | </div> |
| | | <div class="add-node-card" @click="handleAdd"> |
| | | <div class="add-icon-wrapper"> |
| | | <el-icon :size="28"><Plus /></el-icon> |
| | | </div> |
| | | <span class="add-text">æ°å¢å®¡æ¹äºº</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- ç©ºç¶æ --> |
| | | <div class="empty-state" v-else> |
| | | <div class="empty-content"> |
| | | <div class="empty-icon-wrapper"> |
| | | <el-icon :size="48" color="#c0c4cc"><User /></el-icon> |
| | | </div> |
| | | <div class="empty-text">ææ å®¡æ¹äººé
ç½®</div> |
| | | <div class="empty-subtext">ç¹å»ä¸æ¹æé®æ·»å 第ä¸ä¸ªå®¡æ¹äºº</div> |
| | | <el-button type="primary" size="large" @click="handleAdd" class="empty-add-btn"> |
| | | <el-icon><Plus /></el-icon> |
| | | æ°å¢å®¡æ¹äºº |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- åºé¨æç¤º --> |
| | | <div class="bottom-tips"> |
| | | <el-icon><InfoFilled /></el-icon> |
| | | <span>æç¤ºï¼æ¯ä¸ªæµç¨è³å°é
ç½®ä¸ä¸ªå®¡æ¹äººï¼å®¡æ¹æé¡ºåºæµè½¬ï¼å¯éè¿ç®å¤´è°æ´é¡ºåº</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted } from 'vue'; |
| | | import { ElMessage, ElMessageBox } from 'element-plus'; |
| | | import { |
| | | Plus, ArrowLeft, Delete, Check, RefreshLeft, Setting, |
| | | Suitcase, Calendar, Location, Money, ShoppingCart, DocumentChecked, |
| | | Van, ArrowRight, User, InfoFilled |
| | | } from '@element-plus/icons-vue'; |
| | | import { getApproveProcessConfigNodeList, addApproveProcessConfigNode } from '@/api/collaborativeApproval/approvalManagement'; |
| | | import { userListNoPage } from '@/api/system/user'; |
| | | |
| | | // å½åéä¸çæ ç¾é¡µ |
| | | const activeTab = ref('1'); |
| | | |
| | | // 审æ¹ç±»åé
ç½®æ°ç» |
| | | const approveTypes = [ |
| | | { value: '1', label: 'å
¬åºç®¡ç', icon: 'Suitcase', color: '#409EFF' }, |
| | | { value: '2', label: '请å管ç', icon: 'Calendar', color: '#67C23A' }, |
| | | { value: '3', label: 'åºå·®ç®¡ç', icon: 'Location', color: '#E6A23C' }, |
| | | { value: '4', label: 'æ¥é管ç', icon: 'Money', color: '#F56C6C' }, |
| | | { value: '5', label: 'éè´å®¡æ¹', icon: 'ShoppingCart', color: '#909399' }, |
| | | { value: '6', label: 'æ¥ä»·å®¡æ¹', icon: 'DocumentChecked', color: '#9B59B6' }, |
| | | { value: '7', label: 'å货审æ¹', icon: 'Van', color: '#1ABC9C' }, |
| | | ]; |
| | | |
| | | // 审æ¹ç±»ååç§°æ å° |
| | | const approveTypeNameMap = { |
| | | 1: 'å
¬åºç®¡ç', |
| | | 2: '请å管ç', |
| | | 3: 'åºå·®ç®¡ç', |
| | | 4: 'æ¥é管ç', |
| | | 5: 'éè´å®¡æ¹', |
| | | 6: 'æ¥ä»·å®¡æ¹', |
| | | 7: 'å货审æ¹', |
| | | }; |
| | | |
| | | // 审æ¹ç±»å徿 æ å° |
| | | const typeIconMap = { |
| | | 1: 'Suitcase', |
| | | 2: 'Calendar', |
| | | 3: 'Location', |
| | | 4: 'Money', |
| | | 5: 'ShoppingCart', |
| | | 6: 'DocumentChecked', |
| | | 7: 'Van', |
| | | }; |
| | | |
| | | // 审æ¹ç±»åé¢è²æ å° |
| | | const typeColorMap = { |
| | | 1: '#409EFF', |
| | | 2: '#67C23A', |
| | | 3: '#E6A23C', |
| | | 4: '#F56C6C', |
| | | 5: '#909399', |
| | | 6: '#9B59B6', |
| | | 7: '#1ABC9C', |
| | | }; |
| | | |
| | | // 头åé¢è²æ± |
| | | const avatarColors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#9B59B6', '#1ABC9C', '#FF6B6B', '#4ECDC4']; |
| | | |
| | | // å½å审æ¹ç±»ååç§° |
| | | const currentApproveTypeName = computed(() => { |
| | | return approveTypeNameMap[activeTab.value] || 'æªç¥ç±»å'; |
| | | }); |
| | | |
| | | // å½å审æ¹ç±»å |
| | | const currentApproveType = computed(() => { |
| | | return Number(activeTab.value); |
| | | }); |
| | | |
| | | // è·åç±»å徿 |
| | | const getTypeIcon = (type) => typeIconMap[type] || 'Setting'; |
| | | |
| | | // è·åç±»åé¢è² |
| | | const getTypeColor = (type) => typeColorMap[type] || '#409EFF'; |
| | | |
| | | // è·å头åé¢è² |
| | | const getAvatarColor = (name) => { |
| | | if (!name) return '#C0C4CC'; |
| | | let hash = 0; |
| | | for (let i = 0; i < name.length; i++) { |
| | | hash = name.charCodeAt(i) + ((hash << 5) - hash); |
| | | } |
| | | return avatarColors[Math.abs(hash) % avatarColors.length]; |
| | | }; |
| | | |
| | | // è·åçº§å«ææ¬ |
| | | const getLevelText = (index) => { |
| | | const texts = ['第ä¸çº§', '第äºçº§', '第ä¸çº§', '第å级', '第äºçº§', '第å
级', '第ä¸çº§', '第å
«çº§']; |
| | | return texts[index] || `第${index + 1}级`; |
| | | }; |
| | | |
| | | // 审æ¹äººå表ï¼ç宿¥å£ï¼ |
| | | const userList = ref([]); |
| | | |
| | | // 审æ¹äººå表 |
| | | const approverList = ref([]); |
| | | |
| | | // åå§æ°æ®ï¼ç¨äºéç½® |
| | | const originalList = ref([]); |
| | | |
| | | // å è½½ç¶æ |
| | | const loading = ref(false); |
| | | const saveLoading = ref(false); |
| | | |
| | | // æ ç¾é¡µåæ¢å¤ç |
| | | const handleTabChange = () => { |
| | | loadData(); |
| | | }; |
| | | |
| | | // å 载审æ¹é
ç½®æ°æ®ï¼æ¨¡æï¼ |
| | | const loadData = async () => { |
| | | loading.value = true; |
| | | try { |
| | | const res = await getApproveProcessConfigNodeList(currentApproveType.value); |
| | | const source = Array.isArray(res?.data) |
| | | ? res.data |
| | | : Array.isArray(res?.rows) |
| | | ? res.rows |
| | | : Array.isArray(res?.data?.records) |
| | | ? res.data.records |
| | | : []; |
| | | const data = source.map((item, index) => ({ |
| | | ...item, |
| | | sortOrder: item.nodeOrder ?? item.sortOrder ?? index + 1, |
| | | })); |
| | | approverList.value = data.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); |
| | | originalList.value = JSON.parse(JSON.stringify(approverList.value)); |
| | | } catch (error) { |
| | | approverList.value = []; |
| | | originalList.value = []; |
| | | ElMessage.error('å 载审æ¹é
置失败'); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const loadUserList = async () => { |
| | | try { |
| | | const res = await userListNoPage(); |
| | | userList.value = Array.isArray(res?.data) ? res.data : []; |
| | | } catch (error) { |
| | | userList.value = []; |
| | | ElMessage.error('å 载人åå表失败'); |
| | | } |
| | | }; |
| | | |
| | | // 审æ¹äººéæ©åå |
| | | const handleApproverChange = (userId, row) => { |
| | | const user = userList.value.find((u) => u.userId === userId); |
| | | if (user) { |
| | | row.approverName = user.nickName; |
| | | } |
| | | }; |
| | | |
| | | // æ°å¢å®¡æ¹äºº |
| | | const handleAdd = () => { |
| | | const newOrder = approverList.value.length + 1; |
| | | approverList.value.push({ |
| | | id: null, |
| | | approveType: currentApproveType.value, |
| | | approverId: null, |
| | | approverName: '', |
| | | sortOrder: newOrder, |
| | | }); |
| | | }; |
| | | |
| | | // å é¤å®¡æ¹äºº |
| | | const handleDelete = (index) => { |
| | | ElMessageBox.confirm('ç¡®å®å é¤è¯¥å®¡æ¹äººåï¼', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning', |
| | | }) |
| | | .then(() => { |
| | | approverList.value.splice(index, 1); |
| | | approverList.value.forEach((item, idx) => { |
| | | item.sortOrder = idx + 1; |
| | | }); |
| | | ElMessage.success('å 餿å'); |
| | | }) |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | // åç§» |
| | | const moveLeft = (index) => { |
| | | if (index === 0) return; |
| | | const temp = approverList.value[index]; |
| | | approverList.value[index] = approverList.value[index - 1]; |
| | | approverList.value[index - 1] = temp; |
| | | approverList.value[index].sortOrder = index + 1; |
| | | approverList.value[index - 1].sortOrder = index; |
| | | }; |
| | | |
| | | // åç§» |
| | | const moveRight = (index) => { |
| | | if (index === approverList.value.length - 1) return; |
| | | const temp = approverList.value[index]; |
| | | approverList.value[index] = approverList.value[index + 1]; |
| | | approverList.value[index + 1] = temp; |
| | | approverList.value[index].sortOrder = index + 1; |
| | | approverList.value[index + 1].sortOrder = index + 2; |
| | | }; |
| | | |
| | | // ä¿åé
ç½® |
| | | const handleSave = async () => { |
| | | if (approverList.value.length === 0) { |
| | | ElMessage.warning('请è³å°é
ç½®ä¸ä¸ªå®¡æ¹äºº'); |
| | | return; |
| | | } |
| | | |
| | | const hasEmptyApprover = approverList.value.some((item) => !item.approverId); |
| | | if (hasEmptyApprover) { |
| | | ElMessage.warning('è¯·éæ©ææå®¡æ¹äºº'); |
| | | return; |
| | | } |
| | | |
| | | const approverIds = approverList.value.map((item) => item.approverId); |
| | | const uniqueIds = [...new Set(approverIds)]; |
| | | if (uniqueIds.length !== approverIds.length) { |
| | | ElMessage.warning('审æ¹äººä¸è½éå¤'); |
| | | return; |
| | | } |
| | | |
| | | saveLoading.value = true; |
| | | try { |
| | | const payload = approverList.value.map((item, index) => ({ |
| | | approveType: currentApproveType.value, |
| | | nodeOrder: index + 1, |
| | | approverId: item.approverId, |
| | | approverName: item.approverName, |
| | | })); |
| | | await addApproveProcessConfigNode(payload); |
| | | ElMessage.success('ä¿åæå'); |
| | | await loadData(); |
| | | } catch (error) { |
| | | ElMessage.error('ä¿å失败'); |
| | | } finally { |
| | | saveLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // éç½® |
| | | const handleReset = () => { |
| | | if (originalList.value.length === 0) { |
| | | approverList.value = []; |
| | | return; |
| | | } |
| | | ElMessageBox.confirm('ç¡®å®è¦éç½®å½åé
ç½®åï¼æªä¿åçæ´æ¹å°ä¸¢å¤±ã', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning', |
| | | }) |
| | | .then(() => { |
| | | approverList.value = JSON.parse(JSON.stringify(originalList.value)); |
| | | ElMessage.success('å·²éç½®'); |
| | | }) |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | onMounted(async () => { |
| | | await loadUserList(); |
| | | await loadData(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .page-header { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .header-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | font-size: 20px; |
| | | font-weight: 600; |
| | | color: var(--el-text-color-primary, #303133); |
| | | margin-bottom: 6px; |
| | | } |
| | | |
| | | .title-icon { |
| | | font-size: 24px; |
| | | color: var(--el-color-primary, #409EFF); |
| | | } |
| | | |
| | | .header-desc { |
| | | font-size: 13px; |
| | | color: var(--el-text-color-secondary, #909399); |
| | | margin-left: 34px; |
| | | } |
| | | |
| | | /* 审æ¹ç±»å忢 - ç´§åæ ç¾å¼ */ |
| | | .type-tabs { |
| | | display: flex; |
| | | gap: 4px; |
| | | margin-bottom: 16px; |
| | | padding: 4px; |
| | | background: var(--el-fill-color-light, #f5f7fa); |
| | | border-radius: 8px; |
| | | overflow-x: auto; |
| | | } |
| | | |
| | | .type-tab { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | padding: 8px 14px; |
| | | border-radius: 6px; |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | white-space: nowrap; |
| | | font-size: 13px; |
| | | color: var(--el-text-color-regular, #606266); |
| | | } |
| | | |
| | | .type-tab:hover { |
| | | background: var(--el-color-primary-light-9, rgba(64, 158, 255, 0.1)); |
| | | color: var(--el-color-primary, #409EFF); |
| | | } |
| | | |
| | | .type-tab.active { |
| | | background: var(--el-bg-color, #fff); |
| | | color: var(--el-color-primary, #409EFF); |
| | | font-weight: 600; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| | | } |
| | | |
| | | .tab-name { |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .tab-count { |
| | | min-width: 16px; |
| | | height: 16px; |
| | | padding: 0 5px; |
| | | background: var(--el-color-success, #67C23A); |
| | | color: #fff; |
| | | border-radius: 8px; |
| | | font-size: 11px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .config-card { |
| | | margin-bottom: 16px; |
| | | border-radius: 12px; |
| | | } |
| | | |
| | | :deep(.el-card__header) { |
| | | padding: 16px 20px; |
| | | border-bottom: 1px solid var(--el-border-color-light, #ebeef5); |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .header-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 14px; |
| | | } |
| | | |
| | | .type-icon { |
| | | width: 44px; |
| | | height: 44px; |
| | | border-radius: 10px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .header-info { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .type-name { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: var(--el-text-color-primary, #303133); |
| | | } |
| | | |
| | | .header-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .flow-wrapper { |
| | | overflow-x: auto; |
| | | padding: 8px 4px; |
| | | } |
| | | |
| | | .flow-container { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 0; |
| | | min-width: min-content; |
| | | } |
| | | |
| | | .flow-item { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .node-card { |
| | | width: 200px; |
| | | background: var(--el-bg-color, #fff); |
| | | border: 2px solid var(--el-border-color, #e4e7ed); |
| | | border-radius: 12px; |
| | | padding: 16px; |
| | | position: relative; |
| | | transition: all 0.3s ease; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .node-card:hover { |
| | | border-color: var(--el-color-primary, #409EFF); |
| | | box-shadow: 0 4px 16px rgba(64, 158, 255, 0.15); |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | .node-card.empty { |
| | | border-style: dashed; |
| | | border-color: var(--el-border-color, #c0c4cc); |
| | | background: var(--el-fill-color-light, #fafbfc); |
| | | } |
| | | |
| | | .node-card.empty:hover { |
| | | border-color: var(--el-color-primary, #409EFF); |
| | | background: var(--el-fill-color-light, #f5f7fa); |
| | | } |
| | | |
| | | .node-badge { |
| | | position: absolute; |
| | | top: -10px; |
| | | left: 16px; |
| | | width: 24px; |
| | | height: 24px; |
| | | background: var(--el-color-primary, #409EFF); |
| | | color: #fff; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 13px; |
| | | font-weight: 700; |
| | | box-shadow: 0 2px 8px rgba(64, 158, 255, 0.4); |
| | | } |
| | | |
| | | .node-avatar-section { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | margin-bottom: 12px; |
| | | margin-top: 4px; |
| | | } |
| | | |
| | | .node-avatar { |
| | | width: 56px; |
| | | height: 56px; |
| | | border-radius: 50%; |
| | | background: var(--el-fill-color, #f0f2f5); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-bottom: 8px; |
| | | color: var(--el-text-color-placeholder, #c0c4cc); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .node-avatar.has-user { |
| | | color: #fff; |
| | | font-size: 22px; |
| | | font-weight: 600; |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
| | | } |
| | | |
| | | .node-level { |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary, #909399); |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .node-select-section { |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .node-select-section :deep(.el-select) { |
| | | width: 100%; |
| | | } |
| | | |
| | | .node-actions { |
| | | display: flex; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | padding-top: 12px; |
| | | border-top: 1px solid var(--el-border-color-light, #ebeef5); |
| | | } |
| | | |
| | | .action-btn { |
| | | transition: all 0.2s; |
| | | } |
| | | |
| | | .action-btn:hover:not(:disabled) { |
| | | transform: scale(1.1); |
| | | } |
| | | |
| | | .arrow-connector { |
| | | display: flex; |
| | | align-items: center; |
| | | width: 50px; |
| | | position: relative; |
| | | } |
| | | |
| | | .arrow-line { |
| | | flex: 1; |
| | | height: 2px; |
| | | background: var(--el-border-color, #c0c4cc); |
| | | } |
| | | |
| | | .arrow-icon { |
| | | color: var(--el-text-color-placeholder, #c0c4cc); |
| | | font-size: 14px; |
| | | margin-left: -2px; |
| | | } |
| | | |
| | | /* æ°å¢èç¹æ ·å¼ */ |
| | | .add-node-item { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .add-node-card { |
| | | width: 140px; |
| | | height: 200px; |
| | | border: 2px dashed var(--el-border-color, #c0c4cc); |
| | | border-radius: 12px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 12px; |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | background: var(--el-fill-color-light, #fafbfc); |
| | | flex-shrink: 0; |
| | | margin-left: 0; |
| | | } |
| | | |
| | | .add-node-card:hover { |
| | | border-color: var(--el-color-primary, #409EFF); |
| | | background: var(--el-color-primary-light-9, #f0f7ff); |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 4px 16px rgba(64, 158, 255, 0.15); |
| | | } |
| | | |
| | | .add-icon-wrapper { |
| | | width: 48px; |
| | | height: 48px; |
| | | border-radius: 50%; |
| | | background: var(--el-color-primary, #409EFF); |
| | | color: #fff; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .add-node-card:hover .add-icon-wrapper { |
| | | transform: scale(1.1); |
| | | box-shadow: 0 6px 16px rgba(64, 158, 255, 0.4); |
| | | } |
| | | |
| | | .add-text { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-regular, #606266); |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .add-node-card:hover .add-text { |
| | | color: var(--el-color-primary, #409EFF); |
| | | } |
| | | |
| | | /* ç©ºç¶æ */ |
| | | .empty-state { |
| | | padding: 50px 20px; |
| | | } |
| | | |
| | | .empty-content { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .empty-icon-wrapper { |
| | | width: 80px; |
| | | height: 80px; |
| | | border-radius: 50%; |
| | | background: var(--el-fill-color-light, #f5f7fa); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .empty-text { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: var(--el-text-color-regular, #606266); |
| | | } |
| | | |
| | | .empty-subtext { |
| | | font-size: 13px; |
| | | color: var(--el-text-color-secondary, #909399); |
| | | } |
| | | |
| | | .empty-add-btn { |
| | | margin-top: 8px; |
| | | padding: 12px 28px; |
| | | } |
| | | |
| | | /* åºé¨æç¤º */ |
| | | .bottom-tips { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | padding: 12px 20px; |
| | | background: var(--el-fill-color-light, #f5f7fa); |
| | | border-radius: 8px; |
| | | color: var(--el-text-color-regular, #606266); |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .bottom-tips .el-icon { |
| | | color: var(--el-color-primary, #409EFF); |
| | | font-size: 16px; |
| | | } |
| | | |
| | | @media (max-width: 768px) { |
| | | .type-tabs { |
| | | padding: 3px; |
| | | } |
| | | |
| | | .type-tab { |
| | | padding: 6px 10px; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .tab-name { |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .flow-container { |
| | | flex-wrap: wrap; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .arrow-connector { |
| | | width: 100%; |
| | | height: 30px; |
| | | flex-direction: row; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .arrow-line { |
| | | width: 2px; |
| | | height: 30px; |
| | | } |
| | | |
| | | .arrow-icon { |
| | | right: auto; |
| | | top: auto; |
| | | bottom: -5px; |
| | | transform: rotate(90deg); |
| | | } |
| | | |
| | | .add-node-item { |
| | | width: 100%; |
| | | justify-content: center; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .add-node-item .arrow-connector { |
| | | display: none; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <!-- 审æ¹äººéæ©ï¼å¨æèç¹ï¼ --> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç³è¯·äººï¼" prop="approveUser"> |
| | | <el-select |
| | | v-model="form.approveUser" |
| | | placeholder="éæ©äººå" |
| | | disabled |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç³è¯·æ¥æï¼" prop="approveTime"> |
| | | <el-date-picker |
| | | v-model="form.approveTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | disabled |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | |
| | | <!-- æ¥ä»·å®¡æ¹ï¼å±ç¤ºæ¥ä»·è¯¦æ
ï¼å¤ç¨é宿¥ä»·"æ¥ç详æ
å¯¹è¯æ¡"å
å®¹ç»æï¼ --> |
| | |
| | | <h4>夿³¨</h4> |
| | | <p>{{ currentQuotation.remark }}</p> |
| | | </div> |
| | | |
| | | <div v-if="quotationAttachments.length > 0" style="margin-top: 20px;"> |
| | | <h4>éä»¶</h4> |
| | | <el-table :data="quotationAttachments" border style="width: 100%"> |
| | | <el-table-column prop="name" label="éä»¶åç§°" min-width="360" show-overflow-tooltip /> |
| | | <el-table-column label="æä½" width="160" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button link type="primary" size="small" @click="downloadAttachment(row)">ä¸è½½</el-button> |
| | | <el-button link type="primary" size="small" @click="previewAttachment(row)">é¢è§</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </template> |
| | | </template> |
| | | </el-skeleton> |
| | |
| | | <el-descriptions-item label="éè´ååå·">{{ currentPurchase.purchaseContractNumber }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¾åºååç§°">{{ currentPurchase.supplierName }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®åç§°">{{ currentPurchase.projectName }}</el-descriptions-item> |
| | | <el-descriptions-item label="订åç¼å·">{{ currentPurchase.salesContractNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="éå®ååå·">{{ currentPurchase.salesContractNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç¾è®¢æ¥æ">{{ currentPurchase.executionDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="å½å
¥æ¥æ">{{ currentPurchase.entryDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="仿¬¾æ¹å¼">{{ currentPurchase.paymentMethod }}</el-descriptions-item> |
| | |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <filePreview ref="filePreviewRef" /> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | updateApproveNode |
| | | } from "@/api/collaborativeApproval/approvalProcess.js"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import {userListNoPageByTenantId} from "@/api/system/user.js"; |
| | | import { WarningFilled, Edit, Check, MoreFilled } from '@element-plus/icons-vue' |
| | | import { getQuotationDetail, getQuotationList } from "@/api/salesManagement/salesQuotation.js"; |
| | | import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; |
| | | import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js"; |
| | | import filePreview from "@/components/filePreview/index.vue"; |
| | | const emit = defineEmits(['close']) |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | |
| | | const formRef = ref(null); |
| | | const userStore = useUserStore() |
| | | const productOptions = ref([]); |
| | | const userList = ref([]) |
| | | const quotationLoading = ref(false) |
| | | const currentQuotation = ref({}) |
| | | const purchaseLoading = ref(false) |
| | | const currentPurchase = ref({}) |
| | | const filePreviewRef = ref() |
| | | const isQuotationApproval = computed(() => Number(props.approveType) === 6) |
| | | const isPurchaseApproval = computed(() => Number(props.approveType) === 5) |
| | | |
| | | const normalizeQuotationFiles = (raw) => { |
| | | const list = |
| | | (raw && Array.isArray(raw.salesLedgerFiles) && raw.salesLedgerFiles) || |
| | | (raw && Array.isArray(raw.quotationFiles) && raw.quotationFiles) || |
| | | (raw && Array.isArray(raw.fileList) && raw.fileList) || |
| | | (raw && Array.isArray(raw.files) && raw.files) || |
| | | [] |
| | | return list |
| | | .map((item) => ({ |
| | | id: item?.id, |
| | | name: item?.fileName || item?.name || item?.originalName || item?.filename || "éä»¶", |
| | | url: item?.fileUrl || item?.url || item?.path || item?.tempPath, |
| | | })) |
| | | .filter((i) => i.url) |
| | | } |
| | | |
| | | const quotationAttachments = computed(() => normalizeQuotationFiles(currentQuotation.value)) |
| | | |
| | | const downloadAttachment = (row) => { |
| | | proxy.$download.name(row.url) |
| | | } |
| | | const previewAttachment = (row) => { |
| | | filePreviewRef.value?.open?.(row.url) |
| | | } |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | approveTime: "", |
| | | approveId: "", |
| | | approveUser: "", |
| | | approveDeptId: "", |
| | | approveReason: "", |
| | | checkResult: "", |
| | |
| | | dialogFormVisible.value = true; |
| | | currentQuotation.value = {} |
| | | currentPurchase.value = {} |
| | | userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data; |
| | | }); |
| | | form.value = {...row} |
| | | // ç«å³æ¸
é¤è¡¨åéªè¯ç¶æï¼å ä¸ºåæ®µæ¯disabledçï¼ä¸éè¦éªè¯ï¼ |
| | | nextTick(() => { |
| | |
| | | const quotationNo = row?.approveReason; |
| | | if (quotationNo) { |
| | | quotationLoading.value = true |
| | | getQuotationList({ quotationNo }).then(async (res) => { |
| | | getQuotationList({ quotationNo }).then((res) => { |
| | | const records = res?.data?.records || [] |
| | | const first = records[0] || {} |
| | | currentQuotation.value = first |
| | | if (first?.id && normalizeQuotationFiles(first).length === 0) { |
| | | try { |
| | | // const detailRes = await getQuotationDetail({ id: first.id }) |
| | | // const detail = detailRes?.data || detailRes || {} |
| | | currentQuotation.value = { ...first } |
| | | } catch (e) { |
| | | } |
| | | } |
| | | currentQuotation.value = records[0] || {} |
| | | }).finally(() => { |
| | | quotationLoading.value = false |
| | | }) |
| | |
| | | <el-form-item label="请åå¼å§æ¶é´ï¼" prop="startDate"> |
| | | <el-date-picker |
| | | v-model="form.startDate" |
| | | type="datetime" |
| | | placeholder="è¯·éæ©å¼å§æ¶é´" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | type="date" |
| | | placeholder="è¯·éæ©å¼å§æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | /> |
| | |
| | | <el-form-item label="请åç»ææ¶é´ï¼" prop="endDate"> |
| | | <el-date-picker |
| | | v-model="form.endDate" |
| | | type="datetime" |
| | | placeholder="è¯·éæ©ç»ææ¶é´" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | type="date" |
| | | placeholder="è¯·éæ©ç»ææ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | /> |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <!-- 审æ¹äººéæ©ï¼å¨æèç¹ï¼ --> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item> |
| | | <template #label> |
| | | <span>审æ¹äººéæ©ï¼</span> |
| | | <el-button type="primary" @click="addApproverNode" style="margin-left: 8px;">æ°å¢èç¹</el-button> |
| | | </template> |
| | | <div style="display: flex; align-items: flex-end; flex-wrap: wrap;"> |
| | | <div |
| | | v-for="(node, index) in approverNodes" |
| | | :key="node.id" |
| | | style="margin-right: 30px; text-align: center; margin-bottom: 10px;" |
| | | > |
| | | <div> |
| | | <span>审æ¹äºº</span> |
| | | â |
| | | </div> |
| | | <el-select |
| | | v-model="node.userId" |
| | | placeholder="éæ©äººå" |
| | | style="width: 120px; margin-bottom: 8px;" |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | <div> |
| | | <el-button |
| | | type="danger" |
| | | size="small" |
| | | @click="removeApproverNode(index)" |
| | | v-if="approverNodes.length > 1" |
| | | >å é¤</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç³è¯·äººï¼" prop="approveUser"> |
| | | <el-select |
| | | v-model="form.approveUser" |
| | | placeholder="éæ©äººå" |
| | | filterable |
| | | default-first-option |
| | | :reserve-keyword="false" |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç³è¯·æ¥æï¼" prop="approveTime"> |
| | | <el-date-picker |
| | | v-model="form.approveTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="éä»¶ææï¼" prop="remark"> |
| | |
| | | import { |
| | | delLedgerFile, |
| | | } from "@/api/salesManagement/salesLedger.js"; |
| | | import {userListNoPageByTenantId} from "@/api/system/user.js"; |
| | | import { getToken } from "@/utils/auth"; |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { getCurrentDate } from "@/utils/index.js"; |
| | | import log from "@/views/monitor/job/log.vue"; |
| | | const userStore = useUserStore(); |
| | | |
| | | const dialogFormVisible = ref(false); |
| | |
| | | }); |
| | | const data = reactive({ |
| | | form: { |
| | | approveTime: "", |
| | | approveId: "", |
| | | approveUser: "", |
| | | approveDeptId: "", |
| | | approveDeptName: "", |
| | | approveReason: "", |
| | | checkResult: "", |
| | | tempFileIds: [], |
| | | approverList: [], // æ°å¢å段ï¼å卿æèç¹ç审æ¹äººid |
| | | startDate: "", // 请åå¼å§æ¶é´ |
| | | endDate: "", // 请åç»ææ¶é´ |
| | | price: null, // æ¥ééé¢ |
| | | location: "" // åºå·®å°ç¹ |
| | | }, |
| | | rules: { |
| | | approveTime: [{ required: false, message: "请è¾å
¥", trigger: "change" },], |
| | | approveId: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | | approveUser: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | | approveDeptName: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | approveReason: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | checkResult: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | |
| | | } |
| | | }) |
| | | |
| | | // 审æ¹äººèç¹ç¸å
³ |
| | | const approverNodes = ref([ |
| | | { id: 1, userId: null } |
| | | ]) |
| | | let nextApproverId = 2 |
| | | const userList = ref([]) |
| | | function addApproverNode() { |
| | | approverNodes.value.push({ id: nextApproverId++, userId: null }) |
| | | } |
| | | function removeApproverNode(index) { |
| | | approverNodes.value.splice(index, 1) |
| | | } |
| | | |
| | | // å¤çé¨é¨éæ©åå |
| | | const handleDeptChange = (deptId) => { |
| | | if (deptId) { |
| | |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data; |
| | | }); |
| | | form.value = {} |
| | | approverNodes.value = [ |
| | | { id: 1, userId: null } |
| | | ] |
| | | form.value.approveUser = userStore.id; |
| | | form.value.approveTime = getCurrentDate(); |
| | | |
| | | // è·åå½åç¨æ·ä¿¡æ¯å¹¶è®¾ç½®é¨é¨ID |
| | | form.value.approveDeptId = userStore.currentDeptId |
| | |
| | | currentApproveStatus.value = row.approveStatus |
| | | approveProcessGetInfo({id: row.approveId,approveReason: '1'}).then(res => { |
| | | form.value = {...res.data} |
| | | // 忾审æ¹äºº |
| | | if (res.data && res.data.approveUserIds) { |
| | | const userIds = res.data.approveUserIds.split(',') |
| | | approverNodes.value = userIds.map((userId, idx) => ({ |
| | | id: idx + 1, |
| | | userId: parseInt(userId.trim()) |
| | | })) |
| | | nextApproverId = userIds.length + 1 |
| | | } else { |
| | | approverNodes.value = [{ id: 1, userId: null }] |
| | | nextApproverId = 2 |
| | | } |
| | | }) |
| | | } |
| | | } |
| | |
| | | } |
| | | // æäº¤äº§å表å |
| | | const submitForm = () => { |
| | | // æ¶éææèç¹ç审æ¹äººid |
| | | form.value.approveUserIds = approverNodes.value.map(node => node.userId).join(',') |
| | | form.value.approveType = props.approveType |
| | | // 审æ¹äººå¿
å¡«æ ¡éª |
| | | const hasEmptyApprover = approverNodes.value.some(node => !node.userId) |
| | | if (hasEmptyApprover) { |
| | | proxy.$modal.msgError("请为ææå®¡æ¹èç¹éæ©å®¡æ¹äººï¼") |
| | | return |
| | | } |
| | | // å½ approveType 为 2 æ¶ï¼æ ¡éªè¯·åæ¶é´ |
| | | if (props.approveType == 2) { |
| | | if (!form.value.startDate) { |
| | |
| | | proxy.$modal.msgError("è¯·éæ©è¯·åç»ææ¶é´ï¼") |
| | | return |
| | | } |
| | | if (new Date(form.value.startDate).getTime() >= new Date(form.value.endDate).getTime()) { |
| | | proxy.$modal.msgError("请åå¼å§æ¶é´å¿
é¡»æ©äºç»ææ¶é´ï¼") |
| | | // æ ¡éªç»ææ¶é´ä¸è½æ©äºå¼å§æ¶é´ |
| | | if (new Date(form.value.endDate) < new Date(form.value.startDate)) { |
| | | proxy.$modal.msgError("请åç»ææ¶é´ä¸è½æ©äºå¼å§æ¶é´ï¼") |
| | | return |
| | | } |
| | | } |
| | |
| | | <el-table-column label="éä»¶åç§°" prop="name" min-width="400" show-overflow-tooltip /> |
| | | <el-table-column fixed="right" label="æä½" width="150" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">ä¸è½½</el-button> |
| | | <el-button link type="primary" size="small" @click="lookFile(scope.row)">é¢è§</el-button> |
| | | <el-button link type="danger" size="small" @click="handleDelete(scope.row)">å é¤</el-button> |
| | | <el-button link type="primary" @click="downLoadFile(scope.row)">ä¸è½½</el-button> |
| | | <el-button link type="primary" @click="lookFile(scope.row)">é¢è§</el-button> |
| | | <el-button link type="danger" @click="handleDelete(scope.row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- æ ç¾é¡µåæ¢ä¸åç审æ¹ç±»å --> |
| | | <el-tabs v-model="activeTab" @tab-change="handleTabChange" class="approval-tabs"> |
| | | <el-tab-pane label="å
¬åºç®¡ç" name="1"></el-tab-pane> |
| | | <el-tab-pane label="请å管ç" name="2"></el-tab-pane> |
| | | <el-tab-pane label="åºå·®ç®¡ç" name="3"></el-tab-pane> |
| | | <el-tab-pane label="æ¥é管ç" name="4"></el-tab-pane> |
| | | <el-tab-pane label="éè´å®¡æ¹" name="5"></el-tab-pane> |
| | | <el-tab-pane label="æ¥ä»·å®¡æ¹" name="6"></el-tab-pane> |
| | | <el-tab-pane label="å货审æ¹" name="7"></el-tab-pane> |
| | | </el-tabs> |
| | | <!-- 审æ¹ç±»å忢 - ç´§åæ ç¾å¼ --> |
| | | <div class="type-tabs"> |
| | | <div |
| | | v-for="type in approveTypes" |
| | | :key="type.value" |
| | | class="type-tab" |
| | | :class="{ active: activeTab === type.value }" |
| | | @click="activeTab = type.value; handleTabChange()" |
| | | > |
| | | <el-icon :size="14" :style="{ color: activeTab === type.value ? type.color : '#909399' }"> |
| | | <component :is="type.icon" /> |
| | | </el-icon> |
| | | <span class="tab-name">{{ type.label }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="search_form"> |
| | | <div> |
| | | <span class="search_title">æµç¨ç¼å·ï¼</span> |
| | | <!-- æç´¢åæä½åºå --> |
| | | <el-card class="search-card" shadow="never"> |
| | | <div class="search-content"> |
| | | <div class="search-filters"> |
| | | <div class="filter-item"> |
| | | <span class="filter-label">æµç¨ç¼å·</span> |
| | | <el-input |
| | | v-model="searchForm.approveId" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥æµç¨ç¼å·æç´¢" |
| | | @change="handleQuery" |
| | | placeholder="请è¾å
¥æµç¨ç¼å·" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | @keyup.enter="handleQuery" |
| | | class="search-input" |
| | | /> |
| | | <span class="search_title ml10">审æ¹ç¶æï¼</span> |
| | | <el-select v-model="searchForm.approveStatus" clearable @change="handleQuery" style="width: 240px"> |
| | | <el-option label="å¾
å®¡æ ¸" :value="0" /> |
| | | <el-option label="å®¡æ ¸ä¸" :value="1" /> |
| | | <el-option label="å®¡æ ¸å®æ" :value="2" /> |
| | | <el-option label="å®¡æ ¸æªéè¿" :value="3" /> |
| | | <el-option label="已鿰æäº¤" :value="4" /> |
| | | </el-select> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px" |
| | | >æç´¢</el-button |
| | | > |
| | | </div> |
| | | <div> |
| | | <div class="filter-item"> |
| | | <span class="filter-label">审æ¹ç¶æ</span> |
| | | <el-select |
| | | v-model="searchForm.approveStatus" |
| | | clearable |
| | | @change="handleQuery" |
| | | placeholder="è¯·éæ©ç¶æ" |
| | | class="search-select" |
| | | > |
| | | <el-option label="å¾
å®¡æ ¸" :value="0"> |
| | | <el-tag size="small" type="warning">å¾
å®¡æ ¸</el-tag> |
| | | </el-option> |
| | | <el-option label="å®¡æ ¸ä¸" :value="1"> |
| | | <el-tag size="small" type="primary">å®¡æ ¸ä¸</el-tag> |
| | | </el-option> |
| | | <el-option label="å®¡æ ¸å®æ" :value="2"> |
| | | <el-tag size="small" type="success">å®¡æ ¸å®æ</el-tag> |
| | | </el-option> |
| | | <el-option label="å®¡æ ¸æªéè¿" :value="3"> |
| | | <el-tag size="small" type="danger">å®¡æ ¸æªéè¿</el-tag> |
| | | </el-option> |
| | | <el-option label="已鿰æäº¤" :value="4"> |
| | | <el-tag size="small" type="info">已鿰æäº¤</el-tag> |
| | | </el-option> |
| | | </el-select> |
| | | </div> |
| | | <el-button type="primary" @click="handleQuery" class="search-btn"> |
| | | <el-icon><Search /></el-icon> |
| | | æç´¢ |
| | | </el-button> |
| | | <el-button @click="resetQuery" class="reset-btn"> |
| | | <el-icon><RefreshRight /></el-icon> |
| | | éç½® |
| | | </el-button> |
| | | </div> |
| | | <div class="search-actions"> |
| | | <el-button |
| | | type="primary" |
| | | @click="openForm('add')" |
| | | v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7" |
| | | >æ°å¢</el-button> |
| | | <el-button @click="handleOut">导åº</el-button> |
| | | class="action-btn primary" |
| | | > |
| | | <el-icon><Plus /></el-icon> |
| | | æ°å¢ |
| | | </el-button> |
| | | <el-button @click="handleOut" class="action-btn"> |
| | | <el-icon><Download /></el-icon> |
| | | å¯¼åº |
| | | </el-button> |
| | | <el-button |
| | | type="danger" |
| | | plain |
| | | @click="handleDelete" |
| | | v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7" |
| | | >å é¤</el-button> |
| | | class="action-btn danger" |
| | | > |
| | | <el-icon><Delete /></el-icon> |
| | | å é¤ |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | </el-card> |
| | | |
| | | <!-- æ°æ®è¡¨æ ¼ --> |
| | | <el-card class="table-card" shadow="never" v-loading="tableLoading"> |
| | | <template #header> |
| | | <div class="table-header"> |
| | | <div class="table-title"> |
| | | <div class="type-tag" :style="{ backgroundColor: currentTypeInfo.color }"> |
| | | <el-icon color="#fff" :size="16"><component :is="currentTypeInfo.icon" /></el-icon> |
| | | </div> |
| | | <span>{{ currentTypeInfo.label }}å表</span> |
| | | <el-tag type="info" size="small" effect="plain" class="count-tag"> |
| | | å
± {{ page.total }} æ¡ |
| | | </el-tag> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumnCopy" |
| | |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | :total="page.total" |
| | | class="custom-table" |
| | | ></PIMTable> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- å¼¹çªç»ä»¶ --> |
| | | <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="currentApproveType"></info-form-dia> |
| | | <approval-dia ref="approvalDia" @close="handleQuery" :approveType="currentApproveType"></approval-dia> |
| | | <FileList ref="fileListRef" /> |
| | |
| | | |
| | | <script setup> |
| | | import FileList from "./fileList.vue"; |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { Search, Plus, Delete, Download, RefreshRight, DocumentChecked } from "@element-plus/icons-vue"; |
| | | import {onMounted, ref, computed, reactive, toRefs, nextTick, getCurrentInstance} from "vue"; |
| | | import {ElMessageBox} from "element-plus"; |
| | | import { useRoute } from 'vue-router'; |
| | |
| | | // å½åéä¸çæ ç¾é¡µï¼é»è®¤ä¸ºå
¬åºç®¡ç |
| | | const activeTab = ref('1'); |
| | | |
| | | // åç±»åæ°éç»è®¡ |
| | | const typeCounts = ref({}); |
| | | |
| | | // 审æ¹ç±»åé
ç½® |
| | | const approveTypes = [ |
| | | { value: '1', label: 'å
¬åºç®¡ç', icon: 'Suitcase', color: '#409EFF' }, |
| | | { value: '2', label: '请å管ç', icon: 'Calendar', color: '#67C23A' }, |
| | | { value: '3', label: 'åºå·®ç®¡ç', icon: 'Location', color: '#E6A23C' }, |
| | | { value: '4', label: 'æ¥é管ç', icon: 'Money', color: '#F56C6C' }, |
| | | { value: '5', label: 'éè´å®¡æ¹', icon: 'ShoppingCart', color: '#909399' }, |
| | | { value: '6', label: 'æ¥ä»·å®¡æ¹', icon: 'DocumentChecked', color: '#9B59B6' }, |
| | | { value: '7', label: 'å货审æ¹', icon: 'Van', color: '#1ABC9C' }, |
| | | ]; |
| | | |
| | | // å½å审æ¹ç±»åä¿¡æ¯ |
| | | const currentTypeInfo = computed(() => { |
| | | return approveTypes.find(t => t.value === activeTab.value) || approveTypes[0]; |
| | | }); |
| | | |
| | | // è·åç±»åæ°é |
| | | const getTypeCount = (value) => { |
| | | return typeCounts.value[value] || 0; |
| | | }; |
| | | |
| | | // å½å审æ¹ç±»åï¼æ ¹æ®éä¸çæ ç¾é¡µè®¡ç® |
| | | const currentApproveType = computed(() => { |
| | | return Number(activeTab.value); |
| | | }); |
| | | |
| | | // æ ç¾é¡µåæ¢å¤ç |
| | | const handleTabChange = (tabName) => { |
| | | const handleTabChange = () => { |
| | | // 忢æ ç¾é¡µæ¶éç½®æç´¢æ¡ä»¶åå页ï¼å¹¶éæ°å è½½æ°æ® |
| | | searchForm.value.approveId = ''; |
| | | searchForm.value.approveStatus = ''; |
| | |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | |
| | | // éç½®æç´¢ |
| | | const resetQuery = () => { |
| | | searchForm.value.approveId = ''; |
| | | searchForm.value.approveStatus = ''; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // å¨æè¡¨æ ¼åé
ç½®ï¼æ ¹æ®å®¡æ¹ç±»åçæå |
| | | const tableColumnCopy = computed(() => { |
| | |
| | | }, |
| | | ]; |
| | | |
| | | // æ¥ä»·å®¡æ¹ï¼ç±»å 6ï¼ä¸å±ç¤ºâéä»¶âæä½ |
| | | // æ¥ä»·å®¡æ¹ï¼ç±»å 6ï¼ä¸å±ç¤º"éä»¶"æä½ |
| | | if (!isQuotationType) { |
| | | actionOperations.push({ |
| | | name: "éä»¶", |
| | |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records |
| | | page.total = res.data.total; |
| | | // æ´æ°å½åç±»åæ°é |
| | | typeCounts.value[activeTab.value] = res.data.total; |
| | | }).catch(err => { |
| | | tableLoading.value = false; |
| | | }) |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .approval-tabs { |
| | | margin-bottom: 10px; |
| | | .page-header { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .header-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .title-icon { |
| | | font-size: 28px; |
| | | color: var(--el-color-primary, #409EFF); |
| | | } |
| | | |
| | | .header-text { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .main-title { |
| | | font-size: 20px; |
| | | font-weight: 600; |
| | | color: var(--el-text-color-primary, #303133); |
| | | } |
| | | |
| | | .sub-title { |
| | | font-size: 13px; |
| | | color: var(--el-text-color-secondary, #909399); |
| | | } |
| | | |
| | | /* 审æ¹ç±»å忢 - ç´§åæ ç¾å¼ */ |
| | | .type-tabs { |
| | | display: flex; |
| | | gap: 4px; |
| | | margin-bottom: 16px; |
| | | padding: 4px; |
| | | background: var(--el-fill-color-light, #f5f7fa); |
| | | border-radius: 8px; |
| | | overflow-x: auto; |
| | | } |
| | | |
| | | .type-tab { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | padding: 8px 14px; |
| | | border-radius: 6px; |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | white-space: nowrap; |
| | | font-size: 13px; |
| | | color: var(--el-text-color-regular, #606266); |
| | | } |
| | | |
| | | .type-tab:hover { |
| | | background: var(--el-color-primary-light-9, rgba(64, 158, 255, 0.1)); |
| | | color: var(--el-color-primary, #409EFF); |
| | | } |
| | | |
| | | .type-tab.active { |
| | | background: var(--el-bg-color, #fff); |
| | | color: var(--el-color-primary, #409EFF); |
| | | font-weight: 600; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| | | } |
| | | |
| | | .tab-name { |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .tab-count { |
| | | min-width: 16px; |
| | | height: 16px; |
| | | padding: 0 5px; |
| | | background: var(--el-color-success, #67C23A); |
| | | color: #fff; |
| | | border-radius: 8px; |
| | | font-size: 11px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | /* æç´¢å¡ç */ |
| | | .search-card { |
| | | margin-bottom: 16px; |
| | | border-radius: 12px; |
| | | } |
| | | |
| | | :deep(.el-card__body) { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .search-content { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .search-filters { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .filter-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .filter-label { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-regular, #606266); |
| | | font-weight: 500; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .search-input, |
| | | .search-select { |
| | | width: 200px; |
| | | } |
| | | |
| | | .search-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .reset-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .search-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .action-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .action-btn.primary { |
| | | background: var(--el-color-primary, #409EFF); |
| | | border: none; |
| | | } |
| | | |
| | | .action-btn.danger { |
| | | transition: all 0.3s; |
| | | } |
| | | |
| | | .action-btn.danger:hover { |
| | | background: #f56c6c; |
| | | color: #fff; |
| | | } |
| | | |
| | | /* è¡¨æ ¼å¡ç */ |
| | | .table-card { |
| | | border-radius: 12px; |
| | | } |
| | | |
| | | :deep(.el-card__header) { |
| | | padding: 16px 20px; |
| | | border-bottom: 1px solid var(--el-border-color-light, #ebeef5); |
| | | } |
| | | |
| | | .table-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .table-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: var(--el-text-color-primary, #303133); |
| | | } |
| | | |
| | | .type-tag { |
| | | width: 32px; |
| | | height: 32px; |
| | | border-radius: 8px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .count-tag { |
| | | margin-left: 8px; |
| | | } |
| | | |
| | | .custom-table { |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | /* ååºå¼ */ |
| | | @media (max-width: 1200px) { |
| | | .search-content { |
| | | flex-direction: column; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .search-filters { |
| | | justify-content: flex-start; |
| | | } |
| | | |
| | | .search-actions { |
| | | justify-content: flex-end; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 768px) { |
| | | .type-tabs { |
| | | padding: 3px; |
| | | } |
| | | |
| | | .type-tab { |
| | | padding: 6px 10px; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .tab-name { |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .search-filters { |
| | | flex-direction: column; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .filter-item { |
| | | width: 100%; |
| | | } |
| | | |
| | | .search-input, |
| | | .search-select { |
| | | width: 100%; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item> |
| | | <template #label> |
| | | <div style="display: flex; align-items: center; justify-content: space-between; width: 100%;"> |
| | | <span>审æ¹äººéæ©ï¼</span> |
| | | <el-button type="primary" size="small" @click="addApproverNode" icon="Plus">æ°å¢èç¹</el-button> |
| | | </div> |
| | | </template> |
| | | <div class="approver-nodes-container"> |
| | | <div |
| | | v-for="(node, index) in approverNodes" |
| | | :key="node.id" |
| | | class="approver-node-item" |
| | | > |
| | | <div class="approver-node-header"> |
| | | <span class="approver-node-label">审æ¹èç¹ {{ index + 1 }}</span> |
| | | <el-button |
| | | v-if="approverNodes.length > 1" |
| | | type="danger" |
| | | size="small" |
| | | text |
| | | @click="removeApproverNode(index)" |
| | | icon="Delete" |
| | | >å é¤</el-button> |
| | | </div> |
| | | <el-select |
| | | v-model="node.userId" |
| | | placeholder="è¯·éæ©å®¡æ¹äºº" |
| | | filterable |
| | | style="width: 100%;" |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-form-item label="产åä¿¡æ¯ï¼" |
| | | prop="entryDate"> |
| | |
| | | |
| | | const userStore = useUserStore(); |
| | | |
| | | // 审æ¹äººèç¹ï¼ä»¿éå®å°è´¦å货审æ¹äººï¼ |
| | | const approverNodes = ref([{ id: 1, userId: null }]); |
| | | let nextApproverId = 2; |
| | | const addApproverNode = () => { |
| | | approverNodes.value.push({ id: nextApproverId++, userId: null }); |
| | | }; |
| | | const removeApproverNode = (index) => { |
| | | approverNodes.value.splice(index, 1); |
| | | }; |
| | | |
| | | // 订å审æ¹ç¶ææ¾ç¤ºææ¬ |
| | | const approvalStatusText = { |
| | | 1: "å¾
å®¡æ ¸", |
| | |
| | | rules: { |
| | | purchaseContractNumber: [ |
| | | { required: true, message: "请è¾å
¥", trigger: "blur" }, |
| | | ], |
| | | approverId: [ |
| | | { required: true, message: "è¯·éæ©å®¡æ¹äºº", trigger: "change" }, |
| | | ], |
| | | // projectName: [ |
| | | // { required: true, message: "请è¾å
¥é¡¹ç®åç§°", trigger: "blur" }, |
| | |
| | | } |
| | | |
| | | try { |
| | | // è·å审æ¹äººIDå符串 |
| | | const approveUserIds = approverNodes.value |
| | | .filter(node => node.userId) |
| | | .map(node => node.userId) |
| | | .join(","); |
| | | |
| | | let params = { |
| | | productData: proxy.HaveJson(productData.value), |
| | | supplierId: form.value.supplierId, |
| | | paymentMethod: form.value.paymentMethod, |
| | | recorderId: form.value.recorderId, |
| | | projectName: form.value.projectName, |
| | | approveUserIds: approveUserIds, |
| | | templateName: templateName.value.trim(), |
| | | }; |
| | | console.log("template params ===>", params, "currentTemplateId:", currentTemplateId.value); |
| | |
| | | templateName.value = ""; |
| | | filterInputValue.value = ""; |
| | | isTemplateNameDuplicate.value = false; |
| | | // é置审æ¹äººèç¹ï¼é»è®¤ä¸ä¸ªç©ºèç¹ï¼ |
| | | approverNodes.value = [{ id: 1, userId: null }]; |
| | | nextApproverId = 2; |
| | | try { |
| | | // å¹¶è¡å è½½åºç¡æ°æ® |
| | | const [userRes, salesRes, supplierRes] = await Promise.all([ |
| | |
| | | form.value = { ...purchaseRes }; |
| | | productData.value = purchaseRes.productData || []; |
| | | fileList.value = purchaseRes.salesLedgerFiles || []; |
| | | // 妿ç¼è¾æ¶æå®¡æ¹äººï¼è§£æå®¡æ¹äººIDå符串并设置å°èç¹ä¸ |
| | | if (purchaseRes.approveUserIds) { |
| | | const approverIds = purchaseRes.approveUserIds.split(","); |
| | | approverNodes.value = approverIds.map((id, index) => ({ |
| | | id: index + 1, |
| | | userId: Number(id) |
| | | })); |
| | | nextApproverId = approverIds.length + 1; |
| | | } |
| | | } catch (error) { |
| | | console.error("å è½½éè´å°è´¦æ°æ®å¤±è´¥:", error); |
| | | proxy.$modal.msgError("å è½½æ°æ®å¤±è´¥"); |
| | |
| | | const submitForm = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | // 审æ¹äººå¿
å¡«æ ¡éªï¼ææèç¹é½è¦éäººï¼ |
| | | const hasEmptyApprover = approverNodes.value.some(node => !node.userId); |
| | | if (hasEmptyApprover) { |
| | | proxy.$modal.msgError("请为ææå®¡æ¹èç¹éæ©å®¡æ¹äººï¼"); |
| | | return; |
| | | } |
| | | const approveUserIds = approverNodes.value.map(node => node.userId).join(","); |
| | | |
| | | if (productData.value.length > 0) { |
| | | // æ°å¢æ¶ï¼éè¦ä»æ¯ä¸ªäº§å对象ä¸å é¤ id åæ®µ |
| | | let processedProductData = productData.value; |
| | |
| | | } |
| | | form.value.tempFileIds = tempFileIds; |
| | | form.value.type = 2; |
| | | form.value.approveUserIds = approveUserIds; |
| | | |
| | | // 妿salesLedgerId为空ï¼åä¸ä¼ ésalesContractNo |
| | | if (!form.value.salesLedgerId) { |
| | |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | // é置审æ¹äººèç¹ï¼é»è®¤ä¸ä¸ªç©ºèç¹ï¼ |
| | | approverNodes.value = [{ id: 1, userId: null }]; |
| | | nextApproverId = 2; |
| | | dialogFormVisible.value = false; |
| | | }; |
| | | // æå¼äº§åå¼¹æ¡ |
| | |
| | | .select-button-group { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | // 审æ¹äººèç¹å®¹å¨æ ·å¼ |
| | | .approver-nodes-container { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | padding: 16px; |
| | | background-color: #f8f9fa; |
| | | border-radius: 4px; |
| | | border: 1px solid #e4e7ed; |
| | | } |
| | | |
| | | .approver-node-item { |
| | | flex: 0 0 calc(33.333% - 12px); |
| | | min-width: 200px; |
| | | padding: 12px; |
| | | background-color: #fff; |
| | | border-radius: 4px; |
| | | border: 1px solid #dcdfe6; |
| | | transition: all 0.3s; |
| | | |
| | | &:hover { |
| | | border-color: #409eff; |
| | | box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1); |
| | | } |
| | | } |
| | | |
| | | .approver-node-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .approver-node-label { |
| | | font-size: 13px; |
| | | font-weight: 500; |
| | | color: #606266; |
| | | } |
| | | |
| | | @media (max-width: 1200px) { |
| | | .approver-node-item { |
| | | flex: 0 0 calc(50% - 8px); |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 768px) { |
| | | .approver-node-item { |
| | | flex: 0 0 100%; |
| | | } |
| | | } |
| | | |
| | | // å é¤å¾æ æ ·å¼ |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 审æ¹äººéæ©ï¼ä»¿åå审æ¹éç审æ¹äººèç¹éæ©ï¼ --> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item> |
| | | <template #label> |
| | | <span>审æ¹äººéæ©ï¼</span> |
| | | <el-button type="primary" @click="addApproverNode" style="margin-left: 8px;">æ°å¢èç¹</el-button> |
| | | </template> |
| | | <div style="display: flex; align-items: flex-end; flex-wrap: wrap;"> |
| | | <div |
| | | v-for="(node, index) in approverNodes" |
| | | :key="node.id" |
| | | style="margin-right: 20px; text-align: center; margin-bottom: 10px;" |
| | | > |
| | | <div> |
| | | <span>审æ¹äºº</span> |
| | | â |
| | | </div> |
| | | <el-select |
| | | v-model="node.userId" |
| | | placeholder="éæ©äººå" |
| | | filterable |
| | | style="width: 140px; margin-bottom: 8px;" |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | <div> |
| | | <el-button |
| | | type="danger" |
| | | @click="removeApproverNode(index)" |
| | | v-if="approverNodes.length > 1" |
| | | >å é¤</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | |
| | | }, |
| | | }); |
| | | const { deliveryForm, deliveryRules } = toRefs(deliveryFormData); |
| | | |
| | | // å货审æ¹äººèç¹ï¼ä»¿ååå®¡æ¹ infoFormDia.vueï¼ |
| | | const approverNodes = ref([{ id: 1, userId: null }]); |
| | | let nextApproverId = 2; |
| | | const addApproverNode = () => { |
| | | approverNodes.value.push({ id: nextApproverId++, userId: null }); |
| | | }; |
| | | const removeApproverNode = (index) => { |
| | | approverNodes.value.splice(index, 1); |
| | | }; |
| | | |
| | | // 导å
¥ç¸å
³ |
| | | const importUploadRef = ref(null); |
| | |
| | | deliveryForm.value = { |
| | | type: "货车", |
| | | }; |
| | | // é置审æ¹äººèç¹ï¼é»è®¤ä¸ä¸ªç©ºèç¹ï¼ |
| | | approverNodes.value = [{ id: 1, userId: null }]; |
| | | nextApproverId = 2; |
| | | deliveryFormVisible.value = true; |
| | | }; |
| | | |
| | |
| | | const submitDelivery = () => { |
| | | proxy.$refs["deliveryFormRef"].validate((valid) => { |
| | | if (valid) { |
| | | // 审æ¹äººå¿
å¡«æ ¡éªï¼ææèç¹é½è¦éäººï¼ |
| | | const hasEmptyApprover = approverNodes.value.some(node => !node.userId); |
| | | if (hasEmptyApprover) { |
| | | proxy.$modal.msgError("请为ææå®¡æ¹èç¹éæ©å®¡æ¹äººï¼"); |
| | | return; |
| | | } |
| | | const approveUserIds = approverNodes.value.map(node => node.userId).join(","); |
| | | // ä¿åå½åå±å¼çè¡IDï¼ä»¥ä¾¿åè´§åéæ°å è½½åè¡¨æ ¼æ°æ® |
| | | const currentExpandedKeys = [...expandedRowKeys.value]; |
| | | const salesLedgerId = currentDeliveryRow.value.salesLedgerId; |
| | |
| | | salesLedgerId: salesLedgerId, |
| | | salesLedgerProductId: currentDeliveryRow.value.id, |
| | | type: deliveryForm.value.type, |
| | | approveUserIds, |
| | | }) |
| | | .then(() => { |
| | | proxy.$modal.msgSuccess("åè´§æå"); |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 审æ¹äººéæ©ï¼ä»¿åå审æ¹éç审æ¹äººèç¹éæ©ï¼ --> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item> |
| | | <template #label> |
| | | <span>审æ¹äººéæ©ï¼</span> |
| | | <el-button type="primary" @click="addApproverNode" style="margin-left: 8px;">æ°å¢èç¹</el-button> |
| | | </template> |
| | | <div style="display: flex; align-items: flex-end; flex-wrap: wrap;"> |
| | | <div |
| | | v-for="(node, index) in approverNodes" |
| | | :key="node.id" |
| | | style="margin-right: 20px; text-align: center; margin-bottom: 10px;" |
| | | > |
| | | <div> |
| | | <span>审æ¹äºº</span> |
| | | â |
| | | </div> |
| | | <el-select |
| | | v-model="node.userId" |
| | | placeholder="éæ©äººå" |
| | | filterable |
| | | style="width: 140px; margin-bottom: 8px;" |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | <div> |
| | | <el-button |
| | | type="danger" |
| | | @click="removeApproverNode(index)" |
| | | v-if="approverNodes.length > 1" |
| | | >å é¤</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | |
| | | }, |
| | | }); |
| | | const { deliveryForm, deliveryRules } = toRefs(deliveryFormData); |
| | | |
| | | // å货审æ¹äººèç¹ï¼ä»¿ååå®¡æ¹ infoFormDia.vueï¼ |
| | | const approverNodes = ref([{ id: 1, userId: null }]); |
| | | let nextApproverId = 2; |
| | | const addApproverNode = () => { |
| | | approverNodes.value.push({ id: nextApproverId++, userId: null }); |
| | | }; |
| | | const removeApproverNode = (index) => { |
| | | approverNodes.value.splice(index, 1); |
| | | }; |
| | | |
| | | // 导å
¥ç¸å
³ |
| | | const importUploadRef = ref(null); |
| | |
| | | deliveryForm.value = { |
| | | type: "货车", |
| | | }; |
| | | // é置审æ¹äººèç¹ï¼é»è®¤ä¸ä¸ªç©ºèç¹ï¼ |
| | | approverNodes.value = [{ id: 1, userId: null }]; |
| | | nextApproverId = 2; |
| | | deliveryFormVisible.value = true; |
| | | }; |
| | | |
| | |
| | | const submitDelivery = () => { |
| | | proxy.$refs["deliveryFormRef"].validate((valid) => { |
| | | if (valid) { |
| | | // 审æ¹äººå¿
å¡«æ ¡éªï¼ææèç¹é½è¦éäººï¼ |
| | | const hasEmptyApprover = approverNodes.value.some(node => !node.userId); |
| | | if (hasEmptyApprover) { |
| | | proxy.$modal.msgError("请为ææå®¡æ¹èç¹éæ©å®¡æ¹äººï¼"); |
| | | return; |
| | | } |
| | | const approveUserIds = approverNodes.value.map(node => node.userId).join(","); |
| | | // ä¿åå½åå±å¼çè¡IDï¼ä»¥ä¾¿åè´§åéæ°å è½½åè¡¨æ ¼æ°æ® |
| | | const currentExpandedKeys = [...expandedRowKeys.value]; |
| | | const salesLedgerId = currentDeliveryRow.value.salesLedgerId; |
| | |
| | | salesLedgerId: salesLedgerId, |
| | | salesLedgerProductId: currentDeliveryRow.value.id, |
| | | type: deliveryForm.value.type, |
| | | approveUserIds, |
| | | }) |
| | | .then(() => { |
| | | proxy.$modal.msgSuccess("åè´§æå"); |
| | |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 审æ¹äººä¿¡æ¯ --> |
| | | <el-card class="form-card" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header-wrapper"> |
| | | <el-icon class="card-icon"><UserFilled /></el-icon> |
| | | <span class="card-title">审æ¹äººéæ©</span> |
| | | <el-button type="primary" size="small" @click="addApproverNode" class="header-btn"> |
| | | <el-icon><Plus /></el-icon> |
| | | æ°å¢èç¹ |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | <div class="form-content"> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item> |
| | | <div class="approver-nodes-container"> |
| | | <div |
| | | v-for="(node, index) in approverNodes" |
| | | :key="node.id" |
| | | class="approver-node-item" |
| | | > |
| | | <div class="approver-node-label"> |
| | | <span class="node-step">{{ index + 1 }}</span> |
| | | <span class="node-text">审æ¹äºº</span> |
| | | <el-icon class="arrow-icon"><ArrowRight /></el-icon> |
| | | </div> |
| | | <el-select |
| | | v-model="node.userId" |
| | | placeholder="éæ©äººå" |
| | | class="approver-select" |
| | | filterable |
| | | clearable |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | <el-button |
| | | type="danger" |
| | | size="small" |
| | | :icon="Delete" |
| | | @click="removeApproverNode(index)" |
| | | v-if="approverNodes.length > 1" |
| | | class="remove-btn" |
| | | >å é¤</el-button> |
| | | </div> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 产åä¿¡æ¯ --> |
| | | <el-card class="form-card" shadow="hover"> |
| | | <template #header> |
| | |
| | | <script setup> |
| | | import { ref, reactive, computed, onMounted, markRaw, shallowRef, getCurrentInstance } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { Search, Document, UserFilled, Box, EditPen, Plus, ArrowRight, Delete, Paperclip, View, Download } from '@element-plus/icons-vue' |
| | | import { Search, Document, Box, EditPen, Plus, Delete, Paperclip, View, Download } from '@element-plus/icons-vue' |
| | | import Pagination from '@/components/PIMTable/Pagination.vue' |
| | | import FormDialog from '@/components/Dialog/FormDialog.vue' |
| | | import {getQuotationList,addQuotation,updateQuotation,deleteQuotation,deleteFile} from '@/api/salesManagement/salesQuotation.js' |
| | |
| | | const userList = ref([]); |
| | | const customerOption = ref([]); |
| | | |
| | | // 审æ¹äººèç¹ç¸å
³ |
| | | const approverNodes = ref([ |
| | | { id: 1, userId: null } |
| | | ]) |
| | | let nextApproverId = 2 |
| | | |
| | | const isEdit = ref(false) |
| | | const editId = ref(null) |
| | | const currentQuotation = ref({}) |
| | | const formRef = ref() |
| | | |
| | | // æ·»å 审æ¹äººèç¹ |
| | | function addApproverNode() { |
| | | approverNodes.value.push({ id: nextApproverId++, userId: null }) |
| | | } |
| | | |
| | | // å é¤å®¡æ¹äººèç¹ |
| | | function removeApproverNode(index) { |
| | | approverNodes.value.splice(index, 1) |
| | | } |
| | | |
| | | // 计ç®å±æ§ |
| | | const filteredList = computed(() => { |
| | |
| | | isEdit.value = false |
| | | resetForm() |
| | | fileList.value = [] |
| | | // é置审æ¹äººèç¹ |
| | | approverNodes.value = [{ id: 1, userId: null }] |
| | | nextApproverId = 2 |
| | | dialogVisible.value = true |
| | | let userLists = await userListNoPage(); |
| | | // åªå¤å¶éè¦çåæ®µï¼é¿å
å°ç»ä»¶å¼ç¨æ¾å
¥ååºå¼å¯¹è±¡ |
| | |
| | | form.discountAmount = row.discountAmount || 0 |
| | | form.totalAmount = row.totalAmount || 0 |
| | | |
| | | // 忾审æ¹äºº |
| | | if (row.approveUserIds) { |
| | | const userIds = row.approveUserIds.split(',') |
| | | approverNodes.value = userIds.map((userId, idx) => ({ |
| | | id: idx + 1, |
| | | userId: parseInt(userId.trim()) |
| | | })) |
| | | nextApproverId = userIds.length + 1 |
| | | } else { |
| | | approverNodes.value = [{ id: 1, userId: null }] |
| | | nextApproverId = 2 |
| | | } |
| | | |
| | | // å è½½ç¨æ·å表 |
| | | let userLists = await userListNoPage(); |
| | | userList.value = (userLists.data || []).map(item => ({ |
| | |
| | | return |
| | | } |
| | | |
| | | // 审æ¹äººå¿
å¡«æ ¡éª |
| | | const hasEmptyApprover = approverNodes.value.some(node => !node.userId) |
| | | if (hasEmptyApprover) { |
| | | ElMessage.error('请为ææå®¡æ¹èç¹éæ©å®¡æ¹äººï¼') |
| | | return |
| | | } |
| | | |
| | | // æ¶éææèç¹ç审æ¹äººid |
| | | form.approveUserIds = approverNodes.value.map(node => node.userId).join(',') |
| | | |
| | | form.files = fileList.value.map(f => ({ |
| | | id: f.id, |
| | | tempId: f.tempId, |
| | |
| | | validDate: item.validDate || '', |
| | | paymentMethod: item.paymentMethod || '', |
| | | status: item.status || 'è稿', |
| | | // 审æ¹äººï¼ç¨äºç¼è¾æ¶åæ¾ï¼ |
| | | approveUserIds: item.approveUserIds || '', |
| | | remark: item.remark || '', |
| | | salesLedgerFiles: normalizeQuotationFiles(item), |
| | | products: item.products ? item.products.map(product => ({ |
| | |
| | | } |
| | | } |
| | | |
| | | .approver-nodes-container { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 24px; |
| | | padding: 12px 0; |
| | | } |
| | | |
| | | .approver-node-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 12px; |
| | | padding: 16px; |
| | | background: #f8f9fa; |
| | | border-radius: 8px; |
| | | border: 1px solid #e4e7ed; |
| | | transition: all 0.3s ease; |
| | | min-width: 180px; |
| | | |
| | | &:hover { |
| | | border-color: #409eff; |
| | | background: #f0f7ff; |
| | | box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1); |
| | | } |
| | | } |
| | | |
| | | .approver-node-label { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | |
| | | .node-step { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | width: 24px; |
| | | height: 24px; |
| | | background: #409eff; |
| | | color: #fff; |
| | | border-radius: 50%; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .node-text { |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .arrow-icon { |
| | | color: #909399; |
| | | font-size: 14px; |
| | | } |
| | | } |
| | | |
| | | .approver-select { |
| | | width: 100%; |
| | | min-width: 150px; |
| | | } |
| | | |
| | | .remove-btn { |
| | | margin-top: 4px; |
| | | } |
| | | |
| | | .product-table { |
| | | :deep(.el-table__header) { |
| | | background-color: #f5f7fa; |
| | |
| | | } |
| | | |
| | | // ååºå¼ä¼å |
| | | @media (max-width: 1200px) { |
| | | .approver-nodes-container { |
| | | gap: 16px; |
| | | } |
| | | |
| | | .approver-node-item { |
| | | min-width: 160px; |
| | | } |
| | | } |
| | | </style> |