| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="header-section"> |
| | | <div class="title-container"> |
| | | <span class="blue-bar"></span> |
| | | <span class="title-text">项ç®ç±»å</span> |
| | | </div> |
| | | <el-button type="primary" class="add-btn" @click="handleAdd">æ°å¢</el-button> |
| | | </div> |
| | | |
| | | <div class="content-section" v-loading="loading"> |
| | | <div v-for="item in projectTypeList" :key="item.id" class="project-type-card"> |
| | | <div class="card-header"> |
| | | <div class="info-group"> |
| | | <span class="label">ç±»ååç§°:</span> |
| | | <span class="value">{{ item.name }}</span> |
| | | </div> |
| | | <div class="info-group"> |
| | | <span class="label">夿³¨:</span> |
| | | <span class="value">{{ item.description || '--' }}</span> |
| | | </div> |
| | | <div class="info-group"> |
| | | <span class="label">éä»¶:</span> |
| | | <div class="attachment-info" v-if="item.attachment" @click="handleExpand(item)"> |
| | | <el-icon class="file-icon"><Document /></el-icon> |
| | | <span class="file-name">{{ item.attachment.name }}</span> |
| | | <el-icon class="download-icon" @click.stop="handleDownload(item.attachment)"><Download /></el-icon> |
| | | <span class="expand-link">{{ item.expanded ? 'æ¶èµ·' : 'å±å¼' }}</span> |
| | | <el-icon class="arrow-icon" :class="{ 'is-reverse': item.expanded }"><ArrowDown /></el-icon> |
| | | </div> |
| | | <span class="value" v-else>--</span> |
| | | </div> |
| | | <div class="actions"> |
| | | <el-button link type="primary" @click="handleUpdate(item)">ç¼è¾</el-button> |
| | | <el-button link type="primary" @click="handleCopy(item)">å¤å¶</el-button> |
| | | <el-button link type="danger" @click="handleDelete(item)">å é¤</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <el-collapse-transition> |
| | | <div v-show="item.expanded" class="expanded-content"> |
| | | <div class="attachment-list"> |
| | | <div class="attachment-item"> |
| | | <el-icon><Document /></el-icon> |
| | | <span>{{ item.attachment?.name }}</span> |
| | | <el-button link type="primary" size="small" @click="handleDownload(item.attachment)">ä¸è½½</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-collapse-transition> |
| | | |
| | | <div class="card-body"> |
| | | <div class="workflow-container"> |
| | | <div v-for="(step, index) in item.steps" :key="index" class="workflow-step"> |
| | | <div class="step-main"> |
| | | <div class="step-circle">{{ index + 1 }}</div> |
| | | <div v-if="index < item.steps.length - 1" class="step-line"></div> |
| | | </div> |
| | | <div class="step-label">{{ step.label }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="pagination-container"> |
| | | <el-pagination |
| | | v-model:current-page="queryParams.current" |
| | | v-model:page-size="queryParams.size" |
| | | :page-sizes="[10, 20, 30, 50]" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :total="total" |
| | | @size-change="getList" |
| | | @current-change="getList" |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- æ·»å æä¿®æ¹é¡¹ç®ç±»åå¯¹è¯æ¡ --> |
| | | <ProjectTypeDialog |
| | | v-model="open" |
| | | :title="title" |
| | | :data="editData" |
| | | @submit="handleDialogSubmit" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="ProjectType"> |
| | | import { ref, reactive, onMounted, getCurrentInstance } from 'vue'; |
| | | import { Document, Download, ArrowDown } from '@element-plus/icons-vue'; |
| | | import { ElMessage, ElMessageBox } from 'element-plus'; |
| | | import { listPlan, savePlan, deletePlan } from '@/api/projectManagement/projectType'; |
| | | import ProjectTypeDialog from './components/ProjectTypeDialog.vue'; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | // é®ç½©å± |
| | | const loading = ref(false); |
| | | // æ»æ¡æ° |
| | | const total = ref(0); |
| | | // 项ç®ç±»åè¡¨æ ¼æ°æ® |
| | | const projectTypeList = ref([]); |
| | | // å¼¹åºå±æ é¢ |
| | | const title = ref(""); |
| | | // æ¯å¦æ¾ç¤ºå¼¹åºå± |
| | | const open = ref(false); |
| | | // ç¼è¾æ°æ® |
| | | const editData = ref(null); |
| | | |
| | | // æ¥è¯¢åæ° |
| | | const queryParams = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | }); |
| | | |
| | | /** æ¥è¯¢é¡¹ç®ç±»åå表 */ |
| | | async function getList() { |
| | | loading.value = true; |
| | | try { |
| | | const res = await listPlan(queryParams); |
| | | if (res.code === 200) { |
| | | projectTypeList.value = res.data.records.map(item => ({ |
| | | ...item, |
| | | expanded: false, |
| | | // å端è¿åçèç¹å表å¯è½æ¯ planNodeList æ savePlanNodeList |
| | | steps: (item.planNodeList || item.savePlanNodeList || []).map(node => ({ |
| | | label: node.name |
| | | })) |
| | | })); |
| | | total.value = res.data.total; |
| | | } else { |
| | | ElMessage.error(res.msg || "è·åå表失败"); |
| | | } |
| | | } catch (error) { |
| | | console.error("è·åå表失败:", error); |
| | | // 妿æ¥å£ä¸éï¼ææ¶ä¿çæ¨¡ææ°æ®ä¾æ¼ç¤º |
| | | if (projectTypeList.value.length === 0) { |
| | | projectTypeList.value = [ |
| | | { |
| | | id: 1, |
| | | name: 'A项ç®', |
| | | description: '', |
| | | attachment: { name: 'precaution...' }, |
| | | steps: [{ label: 'ç«é¡¹' }, { label: '设计' }, { label: 'éè´' }, { label: 'ç产' }, { label: 'åºè´§' }], |
| | | expanded: false |
| | | } |
| | | ]; |
| | | total.value = 1; |
| | | } |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** æ°å¢æé®æä½ */ |
| | | function handleAdd() { |
| | | editData.value = null; |
| | | open.value = true; |
| | | title.value = "æ·»å 项ç®ç±»å"; |
| | | } |
| | | |
| | | /** ä¿®æ¹æé®æä½ */ |
| | | function handleUpdate(row) { |
| | | editData.value = row; |
| | | open.value = true; |
| | | title.value = "ä¿®æ¹é¡¹ç®ç±»å"; |
| | | } |
| | | |
| | | /** å¼¹çªæäº¤å¤ç */ |
| | | async function handleDialogSubmit(formData) { |
| | | try { |
| | | const res = await savePlan(formData); |
| | | if (res.code === 200) { |
| | | ElMessage.success(formData.id !== undefined ? "ä¿®æ¹æå" : "æ°å¢æå"); |
| | | open.value = false; |
| | | getList(); |
| | | } else { |
| | | ElMessage.error(res.msg || "ä¿å失败"); |
| | | } |
| | | } catch (error) { |
| | | console.error("ä¿å失败:", error); |
| | | } |
| | | } |
| | | |
| | | /** å é¤æé®æä½ */ |
| | | function handleDelete(row) { |
| | | ElMessageBox.confirm('æ¯å¦ç¡®è®¤å é¤é¡¹ç®ç±»åç¼å·ä¸º"' + row.id + '"çæ°æ®é¡¹ï¼', "ç³»ç»æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning" |
| | | }).then(async function() { |
| | | try { |
| | | const res = await deletePlan(row.id); |
| | | if (res.code === 200) { |
| | | ElMessage.success("å 餿å"); |
| | | getList(); |
| | | } else { |
| | | ElMessage.error(res.msg || "å é¤å¤±è´¥"); |
| | | } |
| | | } catch (error) { |
| | | console.error("å é¤å¤±è´¥:", error); |
| | | } |
| | | }).catch(() => {}); |
| | | } |
| | | |
| | | /** å¤å¶æé®æä½ */ |
| | | async function handleCopy(row) { |
| | | const copyData = { |
| | | name: row.name + " - 坿¬", |
| | | description: row.description, |
| | | attachmentIds: row.attachmentIds || [], |
| | | savePlanNodeList: (row.planNodeList || row.savePlanNodeList || []).map(node => ({ |
| | | name: node.name, |
| | | leaderId: node.leaderId, |
| | | leaderName: node.leaderName, |
| | | estimatedDuration: node.estimatedDuration, |
| | | hourlyRate: node.hourlyRate, |
| | | workContent: node.workContent |
| | | })) |
| | | }; |
| | | try { |
| | | const res = await savePlan(copyData); |
| | | if (res.code === 200) { |
| | | ElMessage.success("å¤å¶æå"); |
| | | getList(); |
| | | } else { |
| | | ElMessage.error(res.msg || "å¤å¶å¤±è´¥"); |
| | | } |
| | | } catch (error) { |
| | | console.error("å¤å¶å¤±è´¥:", error); |
| | | } |
| | | } |
| | | |
| | | /** å±å¼/æ¶èµ·éä»¶ */ |
| | | function handleExpand(item) { |
| | | item.expanded = !item.expanded; |
| | | } |
| | | |
| | | /** ä¸è½½éä»¶ */ |
| | | function handleDownload(attachment) { |
| | | // å®ç°ä¸è½½é»è¾ |
| | | ElMessage.info("å¼å§ä¸è½½: " + (attachment.name || "æä»¶")); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .app-container { |
| | | background-color: #f5f7fa; |
| | | min-height: calc(100vh - 84px); |
| | | padding: 20px; |
| | | } |
| | | |
| | | .header-section { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | background-color: #fff; |
| | | padding: 15px 20px; |
| | | border-radius: 8px; |
| | | margin-bottom: 20px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); |
| | | |
| | | .title-container { |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | .blue-bar { |
| | | width: 4px; |
| | | height: 18px; |
| | | background-color: #409eff; |
| | | margin-right: 10px; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .title-text { |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | color: #333; |
| | | } |
| | | } |
| | | |
| | | .add-btn { |
| | | padding: 8px 20px; |
| | | } |
| | | } |
| | | |
| | | .project-type-card { |
| | | background-color: #fff; |
| | | border-radius: 8px; |
| | | padding: 20px; |
| | | margin-bottom: 20px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 25px; |
| | | position: relative; |
| | | |
| | | .info-group { |
| | | margin-right: 40px; |
| | | display: flex; |
| | | align-items: center; |
| | | font-size: 14px; |
| | | |
| | | .label { |
| | | color: #666; |
| | | margin-right: 8px; |
| | | } |
| | | |
| | | .value { |
| | | color: #333; |
| | | } |
| | | |
| | | .attachment-info { |
| | | display: flex; |
| | | align-items: center; |
| | | background-color: #f0f4ff; |
| | | padding: 4px 10px; |
| | | border-radius: 4px; |
| | | color: #409eff; |
| | | cursor: pointer; |
| | | |
| | | .file-icon { |
| | | margin-right: 5px; |
| | | } |
| | | |
| | | .file-name { |
| | | margin-right: 5px; |
| | | max-width: 100px; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .download-icon { |
| | | font-size: 12px; |
| | | margin-right: 10px; |
| | | } |
| | | |
| | | .expand-link { |
| | | color: #409eff; |
| | | margin-right: 5px; |
| | | } |
| | | |
| | | .arrow-icon { |
| | | color: #409eff; |
| | | font-size: 12px; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .actions { |
| | | margin-left: auto; |
| | | } |
| | | } |
| | | |
| | | .expanded-content { |
| | | padding: 0 20px 20px; |
| | | border-bottom: 1px dashed #ebeef5; |
| | | margin-bottom: 20px; |
| | | |
| | | .attachment-list { |
| | | background-color: #f8f9fa; |
| | | padding: 15px; |
| | | border-radius: 4px; |
| | | |
| | | .attachment-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | |
| | | .el-icon { |
| | | font-size: 16px; |
| | | color: #409eff; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .card-body { |
| | | .workflow-container { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | padding: 10px 0; |
| | | overflow-x: auto; |
| | | |
| | | .workflow-step { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | min-width: 120px; |
| | | flex-shrink: 0; |
| | | |
| | | .step-main { |
| | | display: flex; |
| | | align-items: center; |
| | | width: 100%; |
| | | position: relative; |
| | | |
| | | .step-circle { |
| | | width: 24px; |
| | | height: 24px; |
| | | background-color: #409eff; |
| | | color: #fff; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | font-size: 12px; |
| | | font-weight: bold; |
| | | z-index: 2; |
| | | margin: 0 auto; |
| | | } |
| | | |
| | | .step-line { |
| | | position: absolute; |
| | | height: 2px; |
| | | background-color: #d9e6ff; |
| | | left: calc(50% + 12px); |
| | | right: calc(-50% + 12px); |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | z-index: 1; |
| | | } |
| | | } |
| | | |
| | | .step-label { |
| | | margin-top: 10px; |
| | | font-size: 13px; |
| | | color: #333; |
| | | text-align: center; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .pagination-container { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | margin-top: 20px; |
| | | } |
| | | |
| | | .step-config-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | margin-bottom: 10px; |
| | | } |
| | | .content-section{ |
| | | background: #fff; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); |
| | | } |
| | | </style> |