| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog v-model="visible" |
| | | :title="title" |
| | | width="800px" |
| | | destroy-on-close> |
| | | <div class="param-list-container"> |
| | | <div class="params-header"> |
| | | <span>åæ°å表</span> |
| | | <el-button v-if="editable" |
| | | type="primary" |
| | | link |
| | | size="small" |
| | | @click="handleAddParam"> |
| | | <el-icon> |
| | | <Plus /> |
| | | </el-icon>æ°å¢ |
| | | </el-button> |
| | | </div> |
| | | <div class="params-list"> |
| | | <div v-for="param in paramList" |
| | | :key="param.id" |
| | | class="param-item"> |
| | | <div class="param-info"> |
| | | <span class="param-code">{{ param.paramName }}</span> |
| | | <span v-if="param.valueMode == 1" |
| | | class="param-value"> |
| | | æ åå¼ï¼{{ param.standardValue || "-" }} {{ param.unit }} |
| | | </span> |
| | | <span v-else |
| | | class="param-value"> |
| | | æ åå¼ï¼{{ param.minValue || "-" }}-{{ param.maxValue || "-" }} {{ param.unit }} |
| | | </span> |
| | | </div> |
| | | <div class="param-actions"> |
| | | <el-button v-if="editable" |
| | | link |
| | | type="primary" |
| | | size="small" |
| | | @click="handleEditParam(param)"> |
| | | ç¼è¾ |
| | | </el-button> |
| | | <el-button v-if="editable" |
| | | link |
| | | type="danger" |
| | | size="small" |
| | | @click="handleDeleteParam(param)"> |
| | | å é¤ |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <el-empty v-if="!paramList || paramList.length === 0" |
| | | description="ææ åæ°" |
| | | :image-size="50" /> |
| | | </div> |
| | | </div> |
| | | <!-- éæ©åæ°å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="selectParamDialogVisible" |
| | | title="鿩忰" |
| | | width="1000px"> |
| | | <div class="param-select-container"> |
| | | <!-- 左侧忰å表 --> |
| | | <div class="param-list-area"> |
| | | <div class="area-title">å¯éåæ°</div> |
| | | <div class="search-box"> |
| | | <el-input v-model="paramSearchKeyword" |
| | | placeholder="请è¾å
¥åæ°åç§°æç´¢" |
| | | clearable |
| | | size="small" |
| | | @input="getBaseParamListData"> |
| | | <template #prefix> |
| | | <el-icon> |
| | | <Search /> |
| | | </el-icon> |
| | | </template> |
| | | </el-input> |
| | | </div> |
| | | <el-table :data="filteredParamList" |
| | | height="400" |
| | | border |
| | | highlight-current-row |
| | | @current-change="handleSelectParam"> |
| | | <el-table-column prop="paramName" |
| | | label="åæ°åç§°" /> |
| | | <el-table-column prop="paramType" |
| | | label="åæ°ç±»å"> |
| | | <template #default="scope"> |
| | | <el-tag size="small" |
| | | :type="getParamTypeTag(scope.row.paramType)">{{ getParamTypeText(scope.row.paramType) }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <!-- å页æ§ä»¶ --> |
| | | <div class="pagination-container" |
| | | style="margin-top: 10px;"> |
| | | <el-pagination v-model:current-page="paramPage.current" |
| | | v-model:page-size="paramPage.size" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :total="paramPage.total" |
| | | @size-change="getBaseParamListData" |
| | | @current-change="getBaseParamListData" |
| | | size="small" /> |
| | | </div> |
| | | </div> |
| | | <!-- å³ä¾§åæ°è¯¦æ
--> |
| | | <div class="param-detail-area"> |
| | | <div class="area-title">åæ°è¯¦æ
</div> |
| | | <el-form v-if="selectedParam" |
| | | :model="selectedParam" |
| | | label-width="100px" |
| | | class="param-detail-form"> |
| | | <el-form-item label="åæ°åç§°"> |
| | | <span class="detail-text">{{ selectedParam.paramName }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°æ¨¡å¼"> |
| | | <el-tag size="small" |
| | | :type="selectedParam.valueMode == '1' ? 'success' : 'warning'"> |
| | | {{ selectedParam.valueMode == '1' ? 'åå¼' : 'åºé´' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°ç±»å"> |
| | | <el-tag size="small" |
| | | :type="getParamTypeTag(selectedParam.paramType)">{{ getParamTypeText(selectedParam.paramType) }}</el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°æ ¼å¼"> |
| | | <span class="detail-text">{{ selectedParam.paramFormat || '-' }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="åä½"> |
| | | <span class="detail-text">{{ selectedParam.unit || '-' }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="æ åå¼" |
| | | v-if="selectedParam.valueMode == '1' && selectedParam.paramType == '1'"> |
| | | <el-input v-model="selectedParam.standardValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥é»è®¤å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æå°å¼" |
| | | v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'"> |
| | | <el-input v-model="selectedParam.minValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æå°å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æå¤§å¼" |
| | | v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'"> |
| | | <el-input v-model="selectedParam.maxValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æå¤§å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æåº"> |
| | | <el-input v-model="selectedParam.sort" |
| | | type="number" |
| | | placeholder="请è¾å
¥æåº" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¯å¦å¿
å¡«"> |
| | | <el-switch v-model="selectedParam.isRequired" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <el-empty v-else |
| | | description="请ä»å·¦ä¾§éæ©åæ°" |
| | | :image-size="100" /> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <el-button @click="selectParamDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="handleParamSelectSubmit">ç¡®å®</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- ç¼è¾åæ°å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="editParamDialogVisible" |
| | | title="ç¼è¾åæ°" |
| | | width="600px"> |
| | | <el-form :model="editParamForm" |
| | | :rules="editParamRules" |
| | | ref="editParamFormRef" |
| | | label-width="120px"> |
| | | <el-form-item label="åæ°åç§°"> |
| | | <span class="detail-text">{{ editParamForm.paramName }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°æ¨¡å¼"> |
| | | <el-tag size="small" |
| | | :type="editParamForm.valueMode == '1' ? 'success' : 'warning'"> |
| | | {{ editParamForm.valueMode == '1' ? 'åå¼' : 'åºé´' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°ç±»å"> |
| | | <el-tag size="small" |
| | | :type="getParamTypeTag(editParamForm.paramType)"> |
| | | {{ getParamTypeText(editParamForm.paramType) }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°æ ¼å¼"> |
| | | <span class="detail-text">{{ editParamForm.paramFormat || '-' }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="åä½"> |
| | | <span class="detail-text">{{ editParamForm.unit || '-' }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="æ åå¼" |
| | | v-if="editParamForm.valueMode == '1' && editParamForm.paramType == '1'" |
| | | prop="standardValue"> |
| | | <el-input v-model="editParamForm.standardValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æ åå¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æå°å¼" |
| | | v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'" |
| | | prop="minValue"> |
| | | <el-input v-model="editParamForm.minValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æå°å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æå¤§å¼" |
| | | v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'" |
| | | prop="maxValue"> |
| | | <el-input v-model="editParamForm.maxValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æå¤§å¼" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="editParamDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="handleEditParamSubmit">ç¡®å®</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, watch } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { Plus, Search } from "@element-plus/icons-vue"; |
| | | import { |
| | | delProcessRouteItemParam, |
| | | editProcessRouteItemParam, |
| | | addProcessRouteItemParam, |
| | | } from "@/api/productionManagement/processRouteItem.js"; |
| | | import { getBaseParamList } from "@/api/basicData/parameterMaintenance.js"; |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | title: { |
| | | type: String, |
| | | default: "åæ°å表", |
| | | }, |
| | | routeId: { |
| | | type: Number, |
| | | default: 0, |
| | | }, |
| | | process: { |
| | | type: Object, |
| | | default: () => ({}), |
| | | }, |
| | | paramList: { |
| | | type: Array, |
| | | default: () => [], |
| | | }, |
| | | editable: { |
| | | type: Boolean, |
| | | default: true, |
| | | }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:modelValue", "refresh"]); |
| | | |
| | | const visible = computed({ |
| | | get: () => props.modelValue, |
| | | set: value => emit("update:modelValue", value), |
| | | }); |
| | | |
| | | // ååºå¼æ°æ® |
| | | const selectParamDialogVisible = ref(false); |
| | | const editParamDialogVisible = ref(false); |
| | | const paramSearchKeyword = ref(""); |
| | | const selectedParam = ref(null); |
| | | const filteredParamList = ref([]); |
| | | const paramPage = ref({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | const editParamForm = ref({ |
| | | id: null, |
| | | processId: null, |
| | | paramId: null, |
| | | paramName: "", |
| | | valueMode: "1", |
| | | standardValue: null, |
| | | minValue: null, |
| | | maxValue: null, |
| | | sort: 1, |
| | | isRequired: 0, |
| | | paramType: null, |
| | | paramFormat: "", |
| | | unit: "", |
| | | }); |
| | | const editParamRules = ref({ |
| | | standardValue: [{ required: true, message: "请è¾å
¥æ åå¼", trigger: "blur" }], |
| | | minValue: [{ required: true, message: "请è¾å
¥æå°å¼", trigger: "blur" }], |
| | | maxValue: [{ required: true, message: "请è¾å
¥æå¤§å¼", trigger: "blur" }], |
| | | }); |
| | | const editParamFormRef = ref(null); |
| | | |
| | | // æ°å¢åæ° |
| | | const handleAddParam = () => { |
| | | selectedParam.value = null; |
| | | paramSearchKeyword.value = ""; |
| | | paramPage.current = 1; |
| | | // è·åå¯éåæ°å表 |
| | | getBaseParamListData(); |
| | | selectParamDialogVisible.value = true; |
| | | }; |
| | | |
| | | // ç¼è¾åæ° |
| | | const handleEditParam = param => { |
| | | editParamForm.value = { |
| | | id: param.id, |
| | | processId: props.process.id, |
| | | paramId: param.paramId, |
| | | paramName: param.parameterName || param.paramName, |
| | | valueMode: param.parameterType2 || param.valueMode || "1", |
| | | standardValue: param.standardValue, |
| | | minValue: param.minValue, |
| | | maxValue: param.maxValue, |
| | | sort: param.sort || 1, |
| | | isRequired: param.isRequired || 0, |
| | | paramType: param.parameterType || param.paramType, |
| | | paramFormat: param.parameterFormat || param.paramFormat, |
| | | unit: param.unit || param.unit, |
| | | }; |
| | | editParamDialogVisible.value = true; |
| | | }; |
| | | |
| | | // å é¤åæ° |
| | | const handleDeleteParam = param => { |
| | | ElMessageBox.confirm("ç¡®å®è¦å é¤è¯¥åæ°åï¼", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | // è°ç¨APIå é¤åæ° |
| | | delProcessRouteItemParam(param.id) |
| | | .then(res => { |
| | | ElMessage.success("å 餿å"); |
| | | emit("refresh"); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("å é¤åæ°å¤±è´¥"); |
| | | console.error("å é¤åæ°å¤±è´¥ï¼", err); |
| | | }); |
| | | }) |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | // è·åå¯éåæ°å表 |
| | | const getBaseParamListData = () => { |
| | | getBaseParamList({ |
| | | paramName: paramSearchKeyword.value, |
| | | current: paramPage.current, |
| | | size: paramPage.size, |
| | | }).then(res => { |
| | | if (res.code === 200) { |
| | | filteredParamList.value = res.data?.records || []; |
| | | paramPage.total = res.data?.total || 0; |
| | | } else { |
| | | ElMessage.error(res.msg || "æ¥è¯¢å¤±è´¥"); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // 鿩忰 |
| | | const handleSelectParam = param => { |
| | | selectedParam.value = param; |
| | | }; |
| | | |
| | | // æäº¤éæ©åæ° |
| | | const handleParamSelectSubmit = () => { |
| | | if (!selectedParam.value) { |
| | | ElMessage.warning("请å
éæ©ä¸ä¸ªåæ°"); |
| | | return; |
| | | } |
| | | |
| | | if (!props.process || !props.process.id) { |
| | | ElMessage.error("å·¥èºè·¯çº¿é¡¹ç®ä¿¡æ¯ä¸å®æ´"); |
| | | return; |
| | | } |
| | | |
| | | // å¤æåæ°ç±»åï¼åªææ°å¼ç±»åæä¼ æ åå¼ãæå¤§å¼åæå°å¼ |
| | | const isNumericMode = selectedParam.value.valueMode === 1; |
| | | |
| | | // è°ç¨APIæ°å¢åæ° |
| | | addProcessRouteItemParam({ |
| | | routeItemId: props.process.id, |
| | | paramId: selectedParam.value.id, |
| | | standardValue: isNumericMode ? selectedParam.value.standardValue || "" : "", |
| | | minValue: isNumericMode ? selectedParam.value.minValue || 0 : null, |
| | | maxValue: isNumericMode ? selectedParam.value.maxValue || 0 : null, |
| | | isRequired: selectedParam.value.isRequired || 0, |
| | | sort: selectedParam.value.sort || 1, |
| | | }) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | ElMessage.success("æ·»å åæ°æå"); |
| | | selectParamDialogVisible.value = false; |
| | | emit("refresh"); |
| | | } else { |
| | | ElMessage.error(res.msg || "æ·»å åæ°å¤±è´¥"); |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("æ·»å åæ°å¤±è´¥"); |
| | | console.error("æ·»å åæ°å¤±è´¥ï¼", err); |
| | | }); |
| | | }; |
| | | |
| | | // æäº¤ç¼è¾åæ° |
| | | const handleEditParamSubmit = () => { |
| | | if (!editParamFormRef.value) return; |
| | | editParamFormRef.value.validate(valid => { |
| | | if (valid) { |
| | | // å¤æåæ°ç±»åï¼åªææ°å¼ç±»åæä¼ æ åå¼ãæå¤§å¼åæå°å¼ |
| | | const isNumericMode = editParamForm.value.valueMode == 1; |
| | | |
| | | // è°ç¨APIä¿®æ¹åæ° |
| | | editProcessRouteItemParam({ |
| | | id: editParamForm.value.id, |
| | | routeItemId: props.process.id, |
| | | paramId: editParamForm.value.paramId, |
| | | standardValue: isNumericMode |
| | | ? editParamForm.value.standardValue || "" |
| | | : "", |
| | | minValue: isNumericMode ? editParamForm.value.minValue || 0 : null, |
| | | maxValue: isNumericMode ? editParamForm.value.maxValue || 0 : null, |
| | | isRequired: editParamForm.value.isRequired || 0, |
| | | }) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | ElMessage.success("ç¼è¾æå"); |
| | | editParamDialogVisible.value = false; |
| | | emit("refresh"); |
| | | } else { |
| | | ElMessage.error(res.msg || "ç¼è¾å¤±è´¥"); |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("ç¼è¾åæ°å¤±è´¥"); |
| | | console.error("ç¼è¾åæ°å¤±è´¥ï¼", err); |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // è·ååæ°ç±»åæ ç¾ |
| | | const getParamTypeTag = type => { |
| | | const typeMap = { |
| | | 1: "primary", |
| | | 2: "info", |
| | | 3: "warning", |
| | | 4: "success", |
| | | }; |
| | | return typeMap[type] || "default"; |
| | | }; |
| | | |
| | | // è·ååæ°ç±»åææ¬ |
| | | const getParamTypeText = type => { |
| | | const typeMap = { |
| | | 1: "æ°å¼æ ¼å¼", |
| | | 2: "ææ¬æ ¼å¼", |
| | | 3: "䏿é项", |
| | | 4: "æ¶é´æ ¼å¼", |
| | | }; |
| | | return typeMap[type] || type; |
| | | }; |
| | | |
| | | watch( |
| | | () => props.modelValue, |
| | | newVal => { |
| | | if (!newVal) { |
| | | // å¼¹çªå
³éæ¶éç½®æ°æ® |
| | | selectParamDialogVisible.value = false; |
| | | editParamDialogVisible.value = false; |
| | | selectedParam.value = null; |
| | | paramSearchKeyword.value = ""; |
| | | paramPage.current = 1; |
| | | filteredParamList.value = []; |
| | | editParamForm.value = { |
| | | id: null, |
| | | processId: null, |
| | | paramId: null, |
| | | paramName: "", |
| | | valueMode: "1", |
| | | standardValue: null, |
| | | minValue: null, |
| | | maxValue: null, |
| | | sort: 1, |
| | | isRequired: 0, |
| | | paramType: null, |
| | | paramFormat: "", |
| | | unit: "", |
| | | }; |
| | | } |
| | | } |
| | | ); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .param-list-container { |
| | | padding: 10px 0; |
| | | } |
| | | |
| | | .params-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 15px; |
| | | padding-bottom: 10px; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | } |
| | | |
| | | .params-header span { |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | } |
| | | |
| | | .params-list { |
| | | max-height: 400px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .param-item { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 12px 16px; |
| | | margin-bottom: 8px; |
| | | background-color: #f9f9f9; |
| | | border-radius: 4px; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .param-item:hover { |
| | | background-color: #ecf5ff; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .param-info { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 20px; |
| | | flex: 1; |
| | | } |
| | | |
| | | .param-code { |
| | | font-weight: 500; |
| | | color: #303133; |
| | | min-width: 120px; |
| | | } |
| | | |
| | | .param-value { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .param-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | |
| | | /* æ»å¨æ¡æ ·å¼ */ |
| | | .params-list::-webkit-scrollbar { |
| | | width: 6px; |
| | | } |
| | | |
| | | .params-list::-webkit-scrollbar-track { |
| | | background: #f1f1f1; |
| | | border-radius: 3px; |
| | | } |
| | | |
| | | .params-list::-webkit-scrollbar-thumb { |
| | | background: #c1c1c1; |
| | | border-radius: 3px; |
| | | } |
| | | |
| | | .params-list::-webkit-scrollbar-thumb:hover { |
| | | background: #a8a8a8; |
| | | } |
| | | |
| | | /* éæ©åæ°å¯¹è¯æ¡æ ·å¼ */ |
| | | .param-select-container { |
| | | display: flex; |
| | | gap: 20px; |
| | | } |
| | | |
| | | .param-list-area { |
| | | flex: 1; |
| | | min-width: 400px; |
| | | } |
| | | |
| | | .param-detail-area { |
| | | flex: 1; |
| | | min-width: 300px; |
| | | } |
| | | |
| | | .area-title { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | margin-bottom: 10px; |
| | | color: #303133; |
| | | } |
| | | |
| | | .search-box { |
| | | display: flex; |
| | | gap: 10px; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .param-detail-form { |
| | | background: #f9f9f9; |
| | | padding: 15px; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .detail-text { |
| | | font-weight: 500; |
| | | } |
| | | </style> |
| | |
| | | res.value[dictType] = dicts
|
| | | } else {
|
| | | getDicts(dictType).then(resp => {
|
| | | res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
|
| | | res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue,id: p.dictCode, elTagType: p.listClass, elTagClass: p.cssClass }))
|
| | | useDictStore().setDict(dictType, res.value[dictType])
|
| | | })
|
| | | }
|
| | |
| | | } |
| | | } |
| | | |
| | | // çå¬å¼¹çªæå¼ï¼éç½®éæ© |
| | | // çå¬å¼¹çªæå¼ï¼éç½®éæ©åæç´¢æ¡ä»¶ |
| | | watch( |
| | | () => props.modelValue, |
| | | visible => { |
| | | if (visible) { |
| | | multipleSelection.value = []; |
| | | // éç½®æç´¢æ¡ä»¶ |
| | | query.model = ""; |
| | | query.materialCode = ""; |
| | | query.productName = ""; |
| | | page.pageNum = 1; |
| | | // éæ°å è½½æ°æ® |
| | | loadData(); |
| | | } |
| | | } |
| | | ); |
| | |
| | | // è½èææ¬æ ¸ç® |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- æç´¢åºå --> |
| | | <div class="search_form"> |
| | | <el-form :model="searchForm" |
| | | :inline="true"> |
| | | <el-form-item label="ç»è®¡ç»´åº¦:"> |
| | | <el-radio-group v-model="statisticsType" |
| | | @change="handleTypeChange"> |
| | | <el-radio-button label="day">ææ¥ç»è®¡</el-radio-button> |
| | | <el-radio-button label="month">ææç»è®¡</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="è½èç±»å:"> |
| | | <el-select v-model="searchForm.energyType" |
| | | placeholder="å
¨é¨" |
| | | clearable |
| | | style="width: 140px;" |
| | | @change="handleQuery"> |
| | | <el-option label="å
¨é¨" |
| | | value="å
¨é¨" /> |
| | | <el-option label="æ°´" |
| | | value="æ°´" /> |
| | | <el-option label="çµ" |
| | | value="çµ" /> |
| | | <el-option label="æ°" |
| | | value="æ°" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="è½èç¨é:"> |
| | | <el-select v-model="searchForm.energyPurpose" |
| | | placeholder="å
¨é¨" |
| | | clearable |
| | | style="width: 140px;" |
| | | @change="handleQuery"> |
| | | <el-option label="å
¨é¨" |
| | | value="å
¨é¨" /> |
| | | <el-option label="ç产" |
| | | value="ç产" /> |
| | | <el-option label="åå
¬" |
| | | value="åå
¬" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æ¶é´èå´:"> |
| | | <el-date-picker v-if="statisticsType === 'day'" |
| | | v-model="searchForm.dateRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 240px;" |
| | | @change="handleQuery" /> |
| | | <el-date-picker v-else |
| | | v-model="searchForm.monthRange" |
| | | type="monthrange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æä»½" |
| | | end-placeholder="ç»ææä»½" |
| | | value-format="YYYY-MM" |
| | | style="width: 240px;" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" |
| | | @click="handleQuery">æ¥è¯¢</el-button> |
| | | <el-button @click="handleReset">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div> |
| | | <el-button type="success" |
| | | @click="handleExport">å¯¼åºæ¥è¡¨</el-button> |
| | | <div class="energy-cost-page"> |
| | | <!-- çéåºå --> |
| | | <el-card class="filter-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="card-head"> |
| | | <div class="card-head-left"> |
| | | <el-icon class="card-icon ui-icon"> |
| | | <DataLine /> |
| | | </el-icon> |
| | | <span class="card-title">æ¥è¯¢æ¡ä»¶</span> |
| | | </div> |
| | | <div class="card-head-right"> |
| | | <el-radio-group v-model="statisticsType" |
| | | size="small" |
| | | @change="handleTypeChange"> |
| | | <el-radio-button label="day">ææ¥</el-radio-button> |
| | | <el-radio-button label="month">ææ</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="filter-layout"> |
| | | <el-form :model="searchForm" |
| | | :inline="true" |
| | | class="filter-form"> |
| | | <el-form-item label="è½èç±»å"> |
| | | <el-select v-model="searchForm.energyType" |
| | | placeholder="å
¨é¨" |
| | | clearable |
| | | class="w-140" |
| | | @change="handleQuery"> |
| | | <el-option label="å
¨é¨" |
| | | value="å
¨é¨" /> |
| | | <el-option label="æ°´" |
| | | value="æ°´" /> |
| | | <el-option label="çµ" |
| | | value="çµ" /> |
| | | <el-option label="æ°" |
| | | value="æ°" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="è½èç¨é"> |
| | | <el-select v-model="searchForm.energyPurpose" |
| | | placeholder="å
¨é¨" |
| | | clearable |
| | | class="w-140" |
| | | @change="handleQuery"> |
| | | <el-option label="å
¨é¨" |
| | | value="å
¨é¨" /> |
| | | <el-option label="ç产" |
| | | value="ç产" /> |
| | | <el-option label="åå
¬" |
| | | value="åå
¬" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æ¶é´èå´"> |
| | | <el-date-picker v-if="statisticsType === 'day'" |
| | | v-model="searchForm.dateRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | class="w-260" |
| | | @change="handleQuery" /> |
| | | <el-date-picker v-else |
| | | v-model="searchForm.monthRange" |
| | | type="monthrange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æä»½" |
| | | end-placeholder="ç»ææä»½" |
| | | value-format="YYYY-MM" |
| | | class="w-260" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="filter-actions"> |
| | | <el-button class="lux-btn" |
| | | type="primary" |
| | | :loading="tableLoading" |
| | | @click="handleQuery">å·æ°</el-button> |
| | | <el-button class="lux-btn" |
| | | @click="handleReset">éç½®</el-button> |
| | | <el-button class="lux-btn" |
| | | type="success" |
| | | plain |
| | | @click="handleExport">导åº</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- ç»è®¡æ¦è§å¡ç --> |
| | | <div class="statistics-overview"> |
| | | <h2 class="section-header"> |
| | | <el-icon class="header-icon"> |
| | | <DataLine /> |
| | | </el-icon> |
| | | è½èææ¬æ¦è§ |
| | | </h2> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <div class="overview-card blue-card"> |
| | | <div class="overview-icon blue-icon"> |
| | | <el-icon> |
| | | </el-card> |
| | | |
| | | <!-- å¾è¡¨åºå --> |
| | | <div class="charts"> |
| | | <el-card class="panel-card" |
| | | shadow="never"> |
| | | <div class="kpi-strip" |
| | | :class="{ pulse: queryPulse }" |
| | | title="å¿«æ·é®ï¼Enter å·æ° / Esc éç½® / Alt+E 导åº"> |
| | | <button class="kpi-item kpi-total" |
| | | type="button" |
| | | :class="{ selected: selectedKpi === 'all' }" |
| | | @click="handleKpiClick('all')"> |
| | | <div class="kpi-left"> |
| | | <div class="kpi-label">æ»è½èææ¬</div> |
| | | <div class="kpi-value">Â¥{{ formatMoney(animatedOverview.totalCost) }}</div> |
| | | <div class="kpi-meta"> |
| | | <span class="kpi-chip" |
| | | :class="kpiDelta.total.pct >= 0 ? 'up' : 'down'" |
| | | v-if="kpiDelta.total.valid">{{ kpiDelta.total.pct >= 0 ? '+' : '' }}{{ kpiDelta.total.pct.toFixed(1) }}%</span> |
| | | <svg class="kpi-spark" |
| | | viewBox="0 0 72 22" |
| | | aria-hidden="true"> |
| | | <polyline :points="sparklinePoints(kpiSeries.total)" |
| | | fill="none" |
| | | stroke="rgba(47, 111, 237, 0.85)" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" /> |
| | | </svg> |
| | | </div> |
| | | </div> |
| | | <div class="kpi-icon"> |
| | | <el-icon class="ui-icon"> |
| | | <Money /> |
| | | </el-icon> |
| | | </div> |
| | | <div class="overview-info"> |
| | | <div class="overview-label">æ»è½èææ¬</div> |
| | | <div class="overview-value">Â¥{{ overview.totalCost }}</div> |
| | | <div class="kpi-actions" |
| | | @click.stop> |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="copyKpi('totalCost')">å¤å¶</button> |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="viewKpiDetails('all')">æç»</button> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="overview-card green-card"> |
| | | <div class="overview-icon green-icon"> |
| | | <el-icon> |
| | | </button> |
| | | <button class="kpi-item kpi-production" |
| | | type="button" |
| | | :class="{ selected: selectedKpi === 'production' }" |
| | | @click="handleKpiClick('production')"> |
| | | <div class="kpi-left"> |
| | | <div class="kpi-label">ç产è½èææ¬</div> |
| | | <div class="kpi-value">Â¥{{ formatMoney(animatedOverview.productionCost) }}</div> |
| | | <div class="kpi-meta"> |
| | | <span class="kpi-chip" |
| | | :class="kpiDelta.production.pct >= 0 ? 'up' : 'down'" |
| | | v-if="kpiDelta.production.valid">{{ kpiDelta.production.pct >= 0 ? '+' : '' }}{{ kpiDelta.production.pct.toFixed(1) }}%</span> |
| | | <svg class="kpi-spark" |
| | | viewBox="0 0 72 22" |
| | | aria-hidden="true"> |
| | | <polyline :points="sparklinePoints(kpiSeries.production)" |
| | | fill="none" |
| | | stroke="rgba(22, 163, 74, 0.85)" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" /> |
| | | </svg> |
| | | </div> |
| | | </div> |
| | | <div class="kpi-icon"> |
| | | <el-icon class="ui-icon"> |
| | | <DataLine /> |
| | | </el-icon> |
| | | </div> |
| | | <div class="overview-info"> |
| | | <div class="overview-label">ç产è½èææ¬</div> |
| | | <div class="overview-value">Â¥{{ overview.productionCost }}</div> |
| | | <div class="kpi-actions" |
| | | @click.stop> |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="copyKpi('productionCost')">å¤å¶</button> |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="viewKpiDetails('production')">æç»</button> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="overview-card purple-card"> |
| | | <div class="overview-icon purple-icon"> |
| | | <el-icon> |
| | | </button> |
| | | <button class="kpi-item kpi-office" |
| | | type="button" |
| | | :class="{ selected: selectedKpi === 'office' }" |
| | | @click="handleKpiClick('office')"> |
| | | <div class="kpi-left"> |
| | | <div class="kpi-label">åå
¬è½èææ¬</div> |
| | | <div class="kpi-value">Â¥{{ formatMoney(animatedOverview.officeCost) }}</div> |
| | | <div class="kpi-meta"> |
| | | <span class="kpi-chip" |
| | | :class="kpiDelta.office.pct >= 0 ? 'up' : 'down'" |
| | | v-if="kpiDelta.office.valid">{{ kpiDelta.office.pct >= 0 ? '+' : '' }}{{ kpiDelta.office.pct.toFixed(1) }}%</span> |
| | | <svg class="kpi-spark" |
| | | viewBox="0 0 72 22" |
| | | aria-hidden="true"> |
| | | <polyline :points="sparklinePoints(kpiSeries.office)" |
| | | fill="none" |
| | | stroke="rgba(100, 116, 139, 0.90)" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" /> |
| | | </svg> |
| | | </div> |
| | | </div> |
| | | <div class="kpi-icon"> |
| | | <el-icon class="ui-icon"> |
| | | <TrendCharts /> |
| | | </el-icon> |
| | | </div> |
| | | <div class="overview-info"> |
| | | <div class="overview-label">åå
¬è½èææ¬</div> |
| | | <div class="overview-value">Â¥{{ overview.officeCost }}</div> |
| | | <div class="kpi-actions" |
| | | @click.stop> |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="copyKpi('officeCost')">å¤å¶</button> |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="viewKpiDetails('office')">æç»</button> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="overview-card gray-card"> |
| | | <div class="overview-icon gray-icon"> |
| | | <el-icon> |
| | | </button> |
| | | <button class="kpi-item kpi-avg" |
| | | type="button" |
| | | @click="handleKpiClick('all')"> |
| | | <div class="kpi-left"> |
| | | <div class="kpi-label">平忿¬</div> |
| | | <div class="kpi-value">Â¥{{ formatMoney(animatedOverview.avgCost) }} <span class="kpi-unit">/{{ statisticsType === 'day' ? 'æ¥' : 'æ' }}</span></div> |
| | | <div class="kpi-meta muted">åºäºå½åçé䏿ç»ç»è®¡</div> |
| | | </div> |
| | | <div class="kpi-icon"> |
| | | <el-icon class="ui-icon"> |
| | | <Histogram /> |
| | | </el-icon> |
| | | </div> |
| | | <div class="overview-info"> |
| | | <div class="overview-label">å¹³åè½èææ¬</div> |
| | | <div class="overview-value">Â¥{{ overview.avgCost }} <span class="unit">/{{ statisticsType === 'day' ? 'æ¥' : 'æ' }}</span></div> |
| | | <div class="kpi-actions" |
| | | @click.stop> |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="copyKpi('avgCost')">å¤å¶</button> |
| | | <button class="kpi-action" |
| | | type="button" |
| | | @click="viewKpiDetails('all')">æç»</button> |
| | | </div> |
| | | </button> |
| | | </div> |
| | | |
| | | <div class="panel-head"> |
| | | <div class="segmented" |
| | | role="tablist" |
| | | aria-label="åæé¢æ¿åæ¢" |
| | | :class="{ 'no-active': chartPanel === 'none' }"> |
| | | <div class="segmented-indicator" |
| | | :class="{ hidden: chartPanel === 'none' }" |
| | | :style="panelIndicatorStyle"></div> |
| | | <button class="segmented-item" |
| | | type="button" |
| | | role="tab" |
| | | :aria-selected="chartPanel === 'core'" |
| | | :class="{ active: chartPanel === 'core' }" |
| | | @click="handleChartPanelClick('core')"> |
| | | <span class="seg-title">æ ¸å¿åæ</span> |
| | | <span class="seg-sub">è¶å¿ / ç±»åå æ¯</span> |
| | | </button> |
| | | <button class="segmented-item" |
| | | type="button" |
| | | role="tab" |
| | | :aria-selected="chartPanel === 'advanced'" |
| | | :class="{ active: chartPanel === 'advanced' }" |
| | | @click="handleChartPanelClick('advanced')"> |
| | | <span class="seg-title">é«çº§åæ</span> |
| | | <span class="seg-sub">ç¨éå æ¯ / å价对æ¯</span> |
| | | </button> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <transition name="lux-collapse"> |
| | | <div v-show="chartPanel === 'core'" |
| | | class="panel-body"> |
| | | <el-row :gutter="16"> |
| | | <el-col :xs="24" |
| | | :lg="12"> |
| | | <el-card class="chart-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="chart-head"> |
| | | <span class="chart-title">è½èææ¬è¶å¿</span> |
| | | <div class="chart-tools" |
| | | @click.stop> |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="downloadChart('cost', 'è½èææ¬è¶å¿')">ä¸è½½</button> |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="openBigChart('cost', 'è½èææ¬è¶å¿')">大å¾</button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div ref="costChartWrap" |
| | | class="chart-wrap" |
| | | v-loading="tableLoading"> |
| | | <div ref="costChart" |
| | | class="chart-content" |
| | | v-show="hasTableData"></div> |
| | | <div class="chart-empty" |
| | | v-show="!hasTableData"> |
| | | <el-empty description="ææ æ°æ®" /> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="24" |
| | | :lg="12"> |
| | | <el-card class="chart-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="chart-head"> |
| | | <span class="chart-title">è½èç±»åææ¬å æ¯</span> |
| | | <div class="chart-tools" |
| | | @click.stop> |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="downloadChart('type', 'è½èç±»åææ¬å æ¯')">ä¸è½½</button> |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="openBigChart('type', 'è½èç±»åææ¬å æ¯')">大å¾</button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div ref="typeChartWrap" |
| | | class="chart-wrap" |
| | | v-loading="tableLoading"> |
| | | <div ref="typeChart" |
| | | class="chart-content" |
| | | v-show="hasTableData"></div> |
| | | <div class="chart-empty" |
| | | v-show="!hasTableData"> |
| | | <el-empty description="ææ æ°æ®" /> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </transition> |
| | | |
| | | <transition name="lux-collapse"> |
| | | <div v-show="chartPanel === 'advanced'" |
| | | class="panel-body"> |
| | | <el-row :gutter="16" |
| | | class="charts-row"> |
| | | <el-col :xs="24" |
| | | :lg="12"> |
| | | <el-card class="chart-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="chart-head"> |
| | | <span class="chart-title">è½èç¨éææ¬å æ¯</span> |
| | | <div class="chart-tools" |
| | | @click.stop> |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="downloadChart('purpose', 'è½èç¨éææ¬å æ¯')">ä¸è½½</button> |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="openBigChart('purpose', 'è½èç¨éææ¬å æ¯')">大å¾</button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div ref="purposeChartWrap" |
| | | class="chart-wrap" |
| | | v-loading="tableLoading"> |
| | | <div ref="purposeChart" |
| | | class="chart-content" |
| | | v-show="hasTableData"></div> |
| | | <div class="chart-empty" |
| | | v-show="!hasTableData"> |
| | | <el-empty description="ææ æ°æ®" /> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="24" |
| | | :lg="12"> |
| | | <el-card class="chart-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="chart-head"> |
| | | <span class="chart-title">è½èå价对æ¯</span> |
| | | <div class="chart-tools" |
| | | @click.stop> |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="downloadChart('price', 'è½èå价对æ¯')">ä¸è½½</button> |
| | | <button class="chart-tool" |
| | | type="button" |
| | | @click="openBigChart('price', 'è½èå价对æ¯')">大å¾</button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div ref="priceChartWrap" |
| | | class="chart-wrap" |
| | | v-loading="tableLoading"> |
| | | <div ref="priceChart" |
| | | class="chart-content" |
| | | v-show="hasTableData"></div> |
| | | <div class="chart-empty" |
| | | v-show="!hasTableData"> |
| | | <el-empty description="ææ æ°æ®" /> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </transition> |
| | | </el-card> |
| | | </div> |
| | | <!-- å¾è¡¨åºå --> |
| | | <div class="charts-container"> |
| | | <h2 class="section-header"> |
| | | <el-icon class="header-icon"> |
| | | <Histogram /> |
| | | </el-icon> |
| | | è½èææ¬åæ |
| | | </h2> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <div class="chart-card"> |
| | | <div class="chart-title">è½èææ¬è¶å¿</div> |
| | | <div ref="costChart" |
| | | class="chart-content"></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="chart-card"> |
| | | <div class="chart-title">è½èç±»åææ¬å æ¯</div> |
| | | <div ref="typeChart" |
| | | class="chart-content"></div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20" |
| | | style="margin-top: 20px;"> |
| | | <el-col :span="12"> |
| | | <div class="chart-card"> |
| | | <div class="chart-title">è½èç¨éææ¬å æ¯</div> |
| | | <div ref="purposeChart" |
| | | class="chart-content"></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="chart-card"> |
| | | <div class="chart-title">è½èå价对æ¯</div> |
| | | <div ref="priceChart" |
| | | class="chart-content"></div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <el-dialog v-model="bigChartVisible" |
| | | :title="bigChartTitle" |
| | | width="92%" |
| | | top="6vh" |
| | | class="big-chart-dialog" |
| | | destroy-on-close |
| | | @opened="handleBigChartOpened" |
| | | @closed="handleBigChartClosed"> |
| | | <div ref="bigChartEl" |
| | | class="big-chart-canvas"></div> |
| | | <template #footer> |
| | | <div class="big-chart-footer"> |
| | | <el-button class="lux-btn" |
| | | @click="downloadChart(bigChartKey, bigChartTitle)">ä¸è½½å¾ç</el-button> |
| | | <el-button class="lux-btn" |
| | | type="primary" |
| | | @click="bigChartVisible = false">å
³é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- æ°æ®è¡¨æ ¼ --> |
| | | <div class="table-section"> |
| | | <h2 class="section-header"> |
| | | <el-icon class="header-icon"> |
| | | <List /> |
| | | </el-icon> |
| | | è¯¦ç»æ°æ® |
| | | </h2> |
| | | <el-table :data="tableData" |
| | | <el-card class="table-card" |
| | | shadow="never"> |
| | | <div ref="tableAnchor"></div> |
| | | <template #header> |
| | | <div class="card-head"> |
| | | <div class="card-head-left"> |
| | | <el-icon class="card-icon ui-icon"> |
| | | <List /> |
| | | </el-icon> |
| | | <span class="card-title">æç»æ°æ®</span> |
| | | </div> |
| | | <div class="card-head-right subtle"> |
| | | <span>å
± {{ page.total }} æ¡</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-table :data="displayTableData" |
| | | v-loading="tableLoading" |
| | | border> |
| | | stripe |
| | | :header-cell-style="{ height: '44px' }" |
| | | class="data-table lux-table" |
| | | @sort-change="handleSortChange"> |
| | | <template #empty> |
| | | <el-empty description="ææ æç»æ°æ®" /> |
| | | </template> |
| | | <el-table-column type="index" |
| | | label="åºå·" |
| | | width="60" |
| | | align="center" /> |
| | | <el-table-column prop="timePeriod" |
| | | :label="timeColumnLabel" |
| | | align="center" /> |
| | | align="center" |
| | | sortable="custom" /> |
| | | <el-table-column prop="energyType" |
| | | label="è½èç±»å" |
| | | width="100" |
| | | align="center"> |
| | | align="center" |
| | | :filters="energyTypeFilters" |
| | | :filter-method="filterEnergyType" |
| | | filter-placement="bottom-end"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getEnergyTypeType(scope.row.energyType)"> |
| | | {{ scope.row.energyType }} |
| | |
| | | <el-table-column prop="energyPurpose" |
| | | label="è½èç¨é" |
| | | width="100" |
| | | align="center"> |
| | | align="center" |
| | | :filters="energyPurposeFilters" |
| | | :filter-method="filterEnergyPurpose" |
| | | filter-placement="bottom-end"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.energyPurpose === 'ç产' ? 'primary' : 'info'"> |
| | | {{ scope.row.energyPurpose }} |
| | |
| | | label="ç¨é" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="consumption-value">{{ scope.row.consumption }}</span> |
| | | <span class="consumption-value">{{ formatNumber(scope.row.consumption, 2) }}</span> |
| | | <span class="consumption-unit">{{ scope.row.unit }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="price" |
| | | label="åä»·(å
)" |
| | | align="right"> |
| | | align="right" |
| | | sortable="custom"> |
| | | <template #default="scope"> |
| | | <span class="price-value">{{ scope.row.price }}</span> |
| | | <span class="price-value">{{ formatNumber(scope.row.price, 2) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="cost" |
| | | label="ææ¬(å
)" |
| | | align="right" |
| | | sortable="custom" |
| | | fixed="right"> |
| | | <template #default="scope"> |
| | | <span class="cost-value">Â¥{{ scope.row.cost }}</span> |
| | | <span class="cost-value">Â¥{{ formatNumber(scope.row.cost, 2) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | <!-- å页 --> |
| | | <div class="pagination-container"> |
| | | <el-pagination v-model:current-page="page.current" |
| | | v-model:page-size="page.size" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | :total="page.total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | @size-change="handleSizeChange" |
| | | @current-change="handleCurrentChange" /> |
| | | </div> |
| | | |
| | | <div class="pagination-container"> |
| | | <el-pagination v-model:current-page="page.current" |
| | | v-model:page-size="page.size" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | :total="page.total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | @size-change="handleSizeChange" |
| | | @current-change="handleCurrentChange" /> |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, computed, nextTick } from "vue"; |
| | | import { ref, reactive, onMounted, onUnmounted, computed, nextTick, watch } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { |
| | | Money, |
| | |
| | | TrendCharts, |
| | | Histogram, |
| | | List, |
| | | ArrowDown, |
| | | } from "@element-plus/icons-vue"; |
| | | import * as echarts from "echarts"; |
| | | import { energyCostStatistics } from "@/api/costAccounting/energyCosts"; |
| | | |
| | | // import { energyCostStatistics } from "@/api/costAccounting/energyCosts"; |
| | | import { energyConsumptionDetailStatistics } from "@/api/energyManagement/energyType"; |
| | | // ç»è®¡ç»´åº¦ï¼day-ææ¥ï¼month-ææ |
| | | const statisticsType = ref("day"); |
| | | |
| | |
| | | avgCost: "0.00", |
| | | }); |
| | | |
| | | const selectedKpi = ref("all"); // all | production | office |
| | | const animatedOverview = reactive({ |
| | | totalCost: 0, |
| | | productionCost: 0, |
| | | officeCost: 0, |
| | | avgCost: 0, |
| | | }); |
| | | |
| | | const formatMoney = v => { |
| | | const n = Number.parseFloat(v); |
| | | const value = Number.isFinite(n) ? n : 0; |
| | | return value.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | }; |
| | | |
| | | const formatNumber = (v, digits = 2) => { |
| | | const n = Number.parseFloat(v); |
| | | if (!Number.isFinite(n)) return "--"; |
| | | return n.toLocaleString("zh-CN", { minimumFractionDigits: digits, maximumFractionDigits: digits }); |
| | | }; |
| | | |
| | | const animateNumber = (key, toValue, duration = 420) => { |
| | | const from = animatedOverview[key] || 0; |
| | | const to = Number.isFinite(toValue) ? toValue : 0; |
| | | const start = performance.now(); |
| | | const easeOut = t => 1 - Math.pow(1 - t, 3); |
| | | |
| | | const tick = now => { |
| | | const p = Math.min(1, (now - start) / duration); |
| | | animatedOverview[key] = from + (to - from) * easeOut(p); |
| | | if (p < 1) requestAnimationFrame(tick); |
| | | }; |
| | | requestAnimationFrame(tick); |
| | | }; |
| | | |
| | | watch( |
| | | () => ({ ...overview }), |
| | | val => { |
| | | animateNumber("totalCost", Number.parseFloat(val.totalCost)); |
| | | animateNumber("productionCost", Number.parseFloat(val.productionCost)); |
| | | animateNumber("officeCost", Number.parseFloat(val.officeCost)); |
| | | animateNumber("avgCost", Number.parseFloat(val.avgCost)); |
| | | }, |
| | | { deep: true, immediate: true } |
| | | ); |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | const tableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | const hasTableData = computed(() => Array.isArray(tableData.value) && tableData.value.length > 0); |
| | | const queryPulse = ref(false); |
| | | |
| | | const kpiSeries = computed(() => { |
| | | const rows = Array.isArray(tableData.value) ? tableData.value : []; |
| | | const byTime = new Map(); |
| | | for (const r of rows) { |
| | | const t = r?.timePeriod ?? ""; |
| | | if (!t) continue; |
| | | if (!byTime.has(t)) byTime.set(t, { total: 0, production: 0, office: 0 }); |
| | | const bucket = byTime.get(t); |
| | | const c = Number.parseFloat(r?.cost); |
| | | const cost = Number.isFinite(c) ? c : 0; |
| | | bucket.total += cost; |
| | | if (r?.energyPurpose === "ç产") bucket.production += cost; |
| | | if (r?.energyPurpose === "åå
¬") bucket.office += cost; |
| | | } |
| | | const times = Array.from(byTime.keys()).sort((a, b) => String(a).localeCompare(String(b))); |
| | | const total = times.map(t => byTime.get(t).total); |
| | | const production = times.map(t => byTime.get(t).production); |
| | | const office = times.map(t => byTime.get(t).office); |
| | | return { times, total, production, office }; |
| | | }); |
| | | |
| | | const kpiDelta = computed(() => { |
| | | const pick = arr => { |
| | | const a = Array.isArray(arr) ? arr : []; |
| | | if (a.length < 2) return { pct: 0, valid: false }; |
| | | const prev = a[a.length - 2]; |
| | | const cur = a[a.length - 1]; |
| | | if (!Number.isFinite(prev) || prev === 0) return { pct: 0, valid: false }; |
| | | return { pct: ((cur - prev) / prev) * 100, valid: true }; |
| | | }; |
| | | return { |
| | | total: pick(kpiSeries.value.total), |
| | | production: pick(kpiSeries.value.production), |
| | | office: pick(kpiSeries.value.office), |
| | | }; |
| | | }); |
| | | |
| | | const sparklinePoints = values => { |
| | | const v = (Array.isArray(values) ? values : []).slice(-12); |
| | | if (v.length < 2) return ""; |
| | | const min = Math.min(...v); |
| | | const max = Math.max(...v); |
| | | const range = max - min || 1; |
| | | const w = 72; |
| | | const h = 22; |
| | | return v |
| | | .map((n, i) => { |
| | | const x = (i / (v.length - 1)) * w; |
| | | const y = h - ((n - min) / range) * h; |
| | | return `${x.toFixed(2)},${y.toFixed(2)}`; |
| | | }) |
| | | .join(" "); |
| | | }; |
| | | |
| | | const handleKpiClick = key => { |
| | | selectedKpi.value = key; |
| | | if (key === "all") searchForm.energyPurpose = ""; |
| | | if (key === "production") searchForm.energyPurpose = "ç产"; |
| | | if (key === "office") searchForm.energyPurpose = "åå
¬"; |
| | | page.current = 1; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | const viewKpiDetails = key => { |
| | | handleKpiClick(key); |
| | | nextTick(() => { |
| | | const el = tableAnchor.value; |
| | | if (el?.scrollIntoView) el.scrollIntoView({ behavior: "smooth", block: "start" }); |
| | | }); |
| | | }; |
| | | |
| | | const copyKpi = async field => { |
| | | const map = { |
| | | totalCost: animatedOverview.totalCost, |
| | | productionCost: animatedOverview.productionCost, |
| | | officeCost: animatedOverview.officeCost, |
| | | avgCost: animatedOverview.avgCost, |
| | | }; |
| | | const raw = map[field]; |
| | | const text = `Â¥${formatMoney(raw)}`; |
| | | try { |
| | | if (navigator?.clipboard?.writeText) { |
| | | await navigator.clipboard.writeText(text); |
| | | } else { |
| | | const input = document.createElement("input"); |
| | | input.value = text; |
| | | document.body.appendChild(input); |
| | | input.select(); |
| | | document.execCommand("copy"); |
| | | document.body.removeChild(input); |
| | | } |
| | | ElMessage.success("å·²å¤å¶å°åªè´´æ¿"); |
| | | } catch (e) { |
| | | console.error(e); |
| | | ElMessage.error("å¤å¶å¤±è´¥"); |
| | | } |
| | | }; |
| | | |
| | | const getChartByKey = key => { |
| | | if (key === "cost") return costChartInstance; |
| | | if (key === "type") return typeChartInstance; |
| | | if (key === "purpose") return purposeChartInstance; |
| | | if (key === "price") return priceChartInstance; |
| | | return null; |
| | | }; |
| | | |
| | | const ensurePanelForChart = key => { |
| | | if (key === "cost" || key === "type") chartPanel.value = "core"; |
| | | if (key === "purpose" || key === "price") chartPanel.value = "advanced"; |
| | | }; |
| | | |
| | | const downloadChart = (key, title) => { |
| | | ensurePanelForChart(key); |
| | | nextTick(() => { |
| | | ensureChartsReady(chartPanel.value === "none" ? "core" : chartPanel.value); |
| | | const ins = getChartByKey(key); |
| | | if (!ins) return; |
| | | const url = ins.getDataURL({ pixelRatio: 2, backgroundColor: "#ffffff" }); |
| | | const a = document.createElement("a"); |
| | | a.href = url; |
| | | const typePart = searchForm.energyType ? `_${searchForm.energyType}` : ""; |
| | | const purposePart = searchForm.energyPurpose ? `_${searchForm.energyPurpose}` : ""; |
| | | let rangePart = ""; |
| | | if (statisticsType.value === "day") { |
| | | if (searchForm.dateRange?.length === 2) rangePart = `_${searchForm.dateRange[0]}~${searchForm.dateRange[1]}`; |
| | | } else { |
| | | if (searchForm.monthRange?.length === 2) rangePart = `_${searchForm.monthRange[0]}~${searchForm.monthRange[1]}`; |
| | | } |
| | | a.download = `${title || "chart"}${typePart}${purposePart}${rangePart}.png`; |
| | | a.click(); |
| | | }); |
| | | }; |
| | | |
| | | const openBigChart = (key, title) => { |
| | | bigChartKey.value = key; |
| | | bigChartTitle.value = title || "å¾è¡¨"; |
| | | bigChartVisible.value = true; |
| | | }; |
| | | |
| | | const handleBigChartOpened = () => { |
| | | nextTick(() => { |
| | | ensurePanelForChart(bigChartKey.value); |
| | | ensureChartsReady(chartPanel.value === "none" ? "core" : chartPanel.value); |
| | | const src = getChartByKey(bigChartKey.value); |
| | | const el = bigChartEl.value; |
| | | if (!src || !el) return; |
| | | |
| | | try { |
| | | bigChartInstance?.dispose?.(); |
| | | } catch (e) { |
| | | // ignore |
| | | } |
| | | bigChartInstance = echarts.init(el); |
| | | const opt = src.getOption(); |
| | | bigChartInstance.setOption(opt, true); |
| | | bigChartInstance.resize(); |
| | | }); |
| | | }; |
| | | |
| | | const handleBigChartClosed = () => { |
| | | try { |
| | | bigChartInstance?.dispose?.(); |
| | | } catch (e) { |
| | | // ignore |
| | | } |
| | | bigChartInstance = null; |
| | | }; |
| | | |
| | | const handleBigChartResize = () => { |
| | | try { |
| | | bigChartInstance?.resize?.(); |
| | | } catch (e) { |
| | | // ignore |
| | | } |
| | | }; |
| | | // è¡¨æ ¼æåºï¼å端æåºï¼ä»
å½±åå½åé¡µæ°æ®ï¼é¿å
ç ´åå端å页åè®®ï¼ |
| | | const sortState = reactive({ |
| | | prop: "", |
| | | order: "", |
| | | }); |
| | | |
| | | const handleSortChange = ({ prop, order }) => { |
| | | sortState.prop = prop || ""; |
| | | sortState.order = order || ""; |
| | | }; |
| | | |
| | | const displayTableData = computed(() => { |
| | | const data = Array.isArray(tableData.value) ? [...tableData.value] : []; |
| | | if (!sortState.prop || !sortState.order) return data; |
| | | |
| | | const prop = sortState.prop; |
| | | const direction = sortState.order === "ascending" ? 1 : -1; |
| | | const numFields = new Set(["price", "cost", "consumption"]); |
| | | |
| | | return data.sort((a, b) => { |
| | | const av = a?.[prop]; |
| | | const bv = b?.[prop]; |
| | | |
| | | if (numFields.has(prop)) { |
| | | const an = Number.parseFloat(av); |
| | | const bn = Number.parseFloat(bv); |
| | | const aNum = Number.isFinite(an) ? an : -Infinity; |
| | | const bNum = Number.isFinite(bn) ? bn : -Infinity; |
| | | return (aNum - bNum) * direction; |
| | | } |
| | | |
| | | return String(av ?? "").localeCompare(String(bv ?? ""), "zh-Hans-CN") * direction; |
| | | }); |
| | | }); |
| | | |
| | | const energyTypeFilters = [ |
| | | { text: "æ°´", value: "æ°´" }, |
| | | { text: "çµ", value: "çµ" }, |
| | | { text: "æ°", value: "æ°" }, |
| | | ]; |
| | | const energyPurposeFilters = [ |
| | | { text: "ç产", value: "ç产" }, |
| | | { text: "åå
¬", value: "åå
¬" }, |
| | | ]; |
| | | |
| | | const filterEnergyType = (value, row) => row.energyType === value; |
| | | const filterEnergyPurpose = (value, row) => row.energyPurpose === value; |
| | | |
| | | // å页 |
| | | const page = reactive({ |
| | |
| | | const purposeChart = ref(null); |
| | | const priceChart = ref(null); |
| | | |
| | | const costChartWrap = ref(null); |
| | | const typeChartWrap = ref(null); |
| | | const purposeChartWrap = ref(null); |
| | | const priceChartWrap = ref(null); |
| | | |
| | | const tableAnchor = ref(null); |
| | | |
| | | const bigChartVisible = ref(false); |
| | | const bigChartKey = ref("cost"); |
| | | const bigChartTitle = ref(""); |
| | | const bigChartEl = ref(null); |
| | | let bigChartInstance = null; |
| | | |
| | | watch(bigChartVisible, v => { |
| | | if (v) window.addEventListener("resize", handleBigChartResize); |
| | | else window.removeEventListener("resize", handleBigChartResize); |
| | | }); |
| | | |
| | | onUnmounted(() => { |
| | | window.removeEventListener("resize", handleBigChartResize); |
| | | try { |
| | | bigChartInstance?.dispose?.(); |
| | | } catch (e) { |
| | | // ignore |
| | | } |
| | | }); |
| | | |
| | | // å¾è¡¨å®ä¾ |
| | | let costChartInstance = null; |
| | | let typeChartInstance = null; |
| | | let purposeChartInstance = null; |
| | | let priceChartInstance = null; |
| | | |
| | | // å¾è¡¨åºåæ¢ï¼core | advanced | noneï¼ç¹å»å½åéä¸å¯æ¶èµ·ï¼ |
| | | const chartPanel = ref("core"); |
| | | |
| | | const ensureChartsReady = panel => { |
| | | if (panel === "core") { |
| | | if (costChart.value && !costChartInstance) costChartInstance = echarts.init(costChart.value); |
| | | if (typeChart.value && !typeChartInstance) typeChartInstance = echarts.init(typeChart.value); |
| | | if (costChartInstance) updateCostChart(); |
| | | if (typeChartInstance) updateTypeChart(); |
| | | return; |
| | | } |
| | | if (panel === "advanced") { |
| | | if (purposeChart.value && !purposeChartInstance) purposeChartInstance = echarts.init(purposeChart.value); |
| | | if (priceChart.value && !priceChartInstance) priceChartInstance = echarts.init(priceChart.value); |
| | | if (purposeChartInstance) updatePurposeChart(); |
| | | if (priceChartInstance) updatePriceChart(); |
| | | } |
| | | }; |
| | | |
| | | const resizeChartsAfterExpand = () => { |
| | | nextTick(() => { |
| | | ensureChartsReady(chartPanel.value === "none" ? "core" : chartPanel.value); |
| | | handleResize(); |
| | | updateCharts(); |
| | | }); |
| | | }; |
| | | |
| | | const handleChartPanelClick = key => { |
| | | chartPanel.value = chartPanel.value === key ? "none" : key; |
| | | }; |
| | | |
| | | const panelIndicatorStyle = computed(() => { |
| | | const x = chartPanel.value === "advanced" ? "calc(100% + 4px)" : "0"; |
| | | return { transform: `translateX(${x})` }; |
| | | }); |
| | | |
| | | watch(chartPanel, val => { |
| | | if (val !== "none") resizeChartsAfterExpand(); |
| | | }); |
| | | |
| | | // è·åè½èç±»åæ ç¾ç±»å |
| | | const getEnergyTypeType = type => { |
| | |
| | | // åå§åå¾è¡¨ |
| | | const initCharts = () => { |
| | | nextTick(() => { |
| | | // è½èææ¬è¶å¿å¾ |
| | | if (costChart.value) { |
| | | costChartInstance = echarts.init(costChart.value); |
| | | updateCostChart(); |
| | | } |
| | | // è½èç±»åææ¬å æ¯å¾ |
| | | if (typeChart.value) { |
| | | typeChartInstance = echarts.init(typeChart.value); |
| | | updateTypeChart(); |
| | | } |
| | | // è½èç¨éææ¬å æ¯å¾ |
| | | if (purposeChart.value) { |
| | | purposeChartInstance = echarts.init(purposeChart.value); |
| | | updatePurposeChart(); |
| | | } |
| | | // è½èå价对æ¯å¾ |
| | | if (priceChart.value) { |
| | | priceChartInstance = echarts.init(priceChart.value); |
| | | updatePriceChart(); |
| | | } |
| | | // åªåå§åå¯è§é¢æ¿ï¼é¿å
éè容å¨åå§å为 0 尺寸导è´ç©ºç½ |
| | | ensureChartsReady(chartPanel.value === "none" ? "core" : chartPanel.value); |
| | | }); |
| | | }; |
| | | |
| | |
| | | tooltip: { |
| | | trigger: "axis", |
| | | axisPointer: { type: "shadow" }, |
| | | backgroundColor: "rgba(255, 255, 255, 0.95)", |
| | | borderColor: "#409EFF", |
| | | backgroundColor: "rgba(255, 255, 255, 0.96)", |
| | | borderColor: "#2f6fed", |
| | | borderWidth: 1, |
| | | textStyle: { color: "#303133" }, |
| | | textStyle: { color: "rgba(15, 23, 42, 0.92)" }, |
| | | extraCssText: "box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;", |
| | | }, |
| | | legend: { |
| | | data: ["ç产è½èææ¬", "åå
¬è½èææ¬"], |
| | |
| | | data: data.map(item => item.timePeriod), |
| | | axisLabel: { |
| | | rotate: statisticsType.value === "day" ? 45 : 0, |
| | | color: "#606266", |
| | | color: "rgba(15, 23, 42, 0.62)", |
| | | }, |
| | | axisLine: { lineStyle: { color: "#ebeef5" } }, |
| | | axisLine: { lineStyle: { color: "rgba(15, 23, 42, 0.08)" } }, |
| | | splitLine: { show: false }, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "ææ¬(å
)", |
| | | nameTextStyle: { color: "#606266" }, |
| | | axisLabel: { color: "#606266" }, |
| | | nameTextStyle: { color: "rgba(15, 23, 42, 0.58)" }, |
| | | axisLabel: { color: "rgba(15, 23, 42, 0.58)" }, |
| | | axisLine: { show: false }, |
| | | splitLine: { lineStyle: { color: "#f0f2f5" } }, |
| | | splitLine: { lineStyle: { color: "rgba(15, 23, 42, 0.06)" } }, |
| | | }, |
| | | series: [ |
| | | { |
| | |
| | | tooltip: { |
| | | trigger: "item", |
| | | formatter: "{a} <br/>{b}: ¥{c} ({d}%)", |
| | | backgroundColor: "rgba(255, 255, 255, 0.95)", |
| | | borderColor: "#409EFF", |
| | | backgroundColor: "rgba(255, 255, 255, 0.96)", |
| | | borderColor: "#2f6fed", |
| | | borderWidth: 1, |
| | | textStyle: { color: "#303133" }, |
| | | textStyle: { color: "rgba(15, 23, 42, 0.92)" }, |
| | | extraCssText: "box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;", |
| | | }, |
| | | legend: { |
| | | orient: "horizontal", |
| | | bottom: 0, |
| | | textStyle: { color: "#606266" }, |
| | | textStyle: { color: "rgba(15, 23, 42, 0.62)" }, |
| | | }, |
| | | series: [ |
| | | { |
| | |
| | | data: chartData, |
| | | }, |
| | | ], |
| | | color: ["#409EFF", "#67C23A", "#E6A23C"], |
| | | color: ["#2f6fed", "#16a34a", "#f59e0b"], |
| | | }; |
| | | typeChartInstance.setOption(option); |
| | | }; |
| | |
| | | tooltip: { |
| | | trigger: "item", |
| | | formatter: "{a} <br/>{b}: ¥{c} ({d}%)", |
| | | backgroundColor: "rgba(255, 255, 255, 0.95)", |
| | | borderColor: "#409EFF", |
| | | backgroundColor: "rgba(255, 255, 255, 0.96)", |
| | | borderColor: "#2f6fed", |
| | | borderWidth: 1, |
| | | textStyle: { color: "#303133" }, |
| | | textStyle: { color: "rgba(15, 23, 42, 0.92)" }, |
| | | extraCssText: "box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;", |
| | | }, |
| | | legend: { |
| | | orient: "horizontal", |
| | | bottom: 0, |
| | | textStyle: { color: "#606266" }, |
| | | textStyle: { color: "rgba(15, 23, 42, 0.62)" }, |
| | | }, |
| | | series: [ |
| | | { |
| | |
| | | label: { |
| | | show: true, |
| | | formatter: "{b}: {d}%", |
| | | color: "#606266", |
| | | color: "rgba(15, 23, 42, 0.62)", |
| | | }, |
| | | labelLine: { |
| | | show: true, |
| | | lineStyle: { color: "#dcdfe6" }, |
| | | lineStyle: { color: "rgba(15, 23, 42, 0.10)" }, |
| | | }, |
| | | }, |
| | | ], |
| | | color: ["#409EFF", "#67C23A"], |
| | | color: ["#2f6fed", "#16a34a"], |
| | | }; |
| | | purposeChartInstance.setOption(option); |
| | | }; |
| | |
| | | tooltip: { |
| | | trigger: "axis", |
| | | axisPointer: { type: "shadow" }, |
| | | backgroundColor: "rgba(255, 255, 255, 0.95)", |
| | | borderColor: "#409EFF", |
| | | backgroundColor: "rgba(255, 255, 255, 0.96)", |
| | | borderColor: "#2f6fed", |
| | | borderWidth: 1, |
| | | textStyle: { color: "#303133" }, |
| | | textStyle: { color: "rgba(15, 23, 42, 0.92)" }, |
| | | extraCssText: "box-shadow: 0 14px 40px rgba(15,23,42,.14); border-radius: 12px;", |
| | | }, |
| | | legend: { |
| | | data: ["ç产è½èåä»·", "åå
¬è½èåä»·"], |
| | | top: 0, |
| | | right: 10, |
| | | textStyle: { color: "#606266" }, |
| | | textStyle: { color: "rgba(15, 23, 42, 0.62)" }, |
| | | }, |
| | | grid: { |
| | | left: "3%", |
| | |
| | | xAxis: { |
| | | type: "category", |
| | | data: energyTypes, |
| | | axisLabel: { color: "#606266" }, |
| | | axisLine: { lineStyle: { color: "#ebeef5" } }, |
| | | axisLabel: { color: "rgba(15, 23, 42, 0.62)" }, |
| | | axisLine: { lineStyle: { color: "rgba(15, 23, 42, 0.08)" } }, |
| | | splitLine: { show: false }, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "åä»·(å
)", |
| | | nameTextStyle: { color: "#606266" }, |
| | | axisLabel: { color: "#606266" }, |
| | | nameTextStyle: { color: "rgba(15, 23, 42, 0.58)" }, |
| | | axisLabel: { color: "rgba(15, 23, 42, 0.58)" }, |
| | | axisLine: { show: false }, |
| | | splitLine: { lineStyle: { color: "#f0f2f5" } }, |
| | | splitLine: { lineStyle: { color: "rgba(15, 23, 42, 0.06)" } }, |
| | | }, |
| | | series: [ |
| | | { |
| | |
| | | data: productionPrices, |
| | | itemStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: "#409EFF" }, |
| | | { offset: 1, color: "#66b1ff" }, |
| | | { offset: 0, color: "#2f6fed" }, |
| | | { offset: 1, color: "#5b8cff" }, |
| | | ]), |
| | | borderRadius: [4, 4, 0, 0], |
| | | }, |
| | |
| | | data: officePrices, |
| | | itemStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: "#67C23A" }, |
| | | { offset: 1, color: "#85ce61" }, |
| | | { offset: 0, color: "#16a34a" }, |
| | | { offset: 1, color: "rgba(22, 163, 74, 0.65)" }, |
| | | ]), |
| | | borderRadius: [4, 4, 0, 0], |
| | | }, |
| | |
| | | |
| | | // æ¥è¯¢ |
| | | const handleQuery = () => { |
| | | queryPulse.value = true; |
| | | window.setTimeout(() => { |
| | | queryPulse.value = false; |
| | | }, 520); |
| | | tableLoading.value = true; |
| | | |
| | | // æé 请æ±åæ° |
| | |
| | | type: statisticsType.value, |
| | | energyType: searchForm.energyType || undefined, |
| | | energyPurpose: searchForm.energyPurpose || undefined, |
| | | // 项ç®å
常ç¨å页忰å½å |
| | | pageNum: page.current, |
| | | pageSize: page.size, |
| | | }; |
| | | |
| | | if (statisticsType.value === "day") { |
| | |
| | | } |
| | | |
| | | // è°ç¨æ¥å£è·åæ°æ® |
| | | energyCostStatistics(params) |
| | | energyConsumptionDetailStatistics(params) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | tableData.value = res.data.records || []; |
| | |
| | | ElMessage.error(res.message || "è·åæ°æ®å¤±è´¥"); |
| | | tableData.value = []; |
| | | page.total = 0; |
| | | overview.totalCost = "0.00"; |
| | | overview.productionCost = "0.00"; |
| | | overview.officeCost = "0.00"; |
| | | overview.avgCost = "0.00"; |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åæ°æ®å¼å¸¸ï¼", err); |
| | | // çæåæ°æ® |
| | | generateMockData(); |
| | | // ãåæ°æ®ï¼Mockï¼å·²ç¦ç¨ãæ¥å£å¼å¸¸æ¶ä¸åçæéæºåæ°æ®ï¼é¿å
误ç¨å°çäº§æ°æ®é¾è·¯ |
| | | ElMessage.error("è·åæ°æ®å¼å¸¸"); |
| | | tableData.value = []; |
| | | page.total = 0; |
| | | overview.totalCost = "0.00"; |
| | | overview.productionCost = "0.00"; |
| | | overview.officeCost = "0.00"; |
| | | overview.avgCost = "0.00"; |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | |
| | | }); |
| | | }; |
| | | |
| | | // ãåæ°æ®ï¼Mockï¼å·²ç¦ç¨ãåå²ä¸ç¨äºæ¥å£å¼å¸¸å
åºçéæºæ°æ®çæé»è¾ï¼ç°å·²æ´ä½æ³¨éï¼é¿å
误ç¨äºç产ã |
| | | /* |
| | | // çæåæ°æ® |
| | | const generateMockData = () => { |
| | | if (statisticsType.value === "day") { |
| | |
| | | // æ´æ°ç»è®¡æ¦è§æ°æ® |
| | | calculateOverview(); |
| | | }; |
| | | */ |
| | | |
| | | // ãåæ°æ®ï¼Mockï¼å·²ç¦ç¨ãä¸ generateMockData é
å¥çåç«¯æ±æ»è®¡ç®ï¼ä»
ä¾åæ°æ®å±ç¤ºï¼ï¼ç°å·²æ³¨é |
| | | /* |
| | | // 计ç®ç»è®¡æ¦è§æ°æ® |
| | | const calculateOverview = () => { |
| | | let totalCost = 0; |
| | |
| | | overview.officeCost = officeCost.toFixed(2); |
| | | overview.avgCost = (totalCost / tableData.value.length).toFixed(2); |
| | | }; |
| | | */ |
| | | |
| | | // æ´æ°ææå¾è¡¨ |
| | | const updateCharts = () => { |
| | |
| | | // å页大å°åå |
| | | const handleSizeChange = val => { |
| | | page.size = val; |
| | | page.current = 1; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // 页ç åå |
| | | const handleCurrentChange = val => { |
| | | page.current = val; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // çªå£å¤§å°ååæ¶éæ°æ¸²æå¾è¡¨ |
| | |
| | | initCharts(); |
| | | window.addEventListener("resize", handleResize); |
| | | }); |
| | | |
| | | const handleGlobalHotkeys = e => { |
| | | // é¿å
å¨è¾å
¥æ¡å
误触 |
| | | const target = e?.target; |
| | | const tag = target?.tagName?.toLowerCase?.(); |
| | | const isTyping = |
| | | tag === "input" || |
| | | tag === "textarea" || |
| | | target?.isContentEditable || |
| | | target?.classList?.contains?.("el-input__inner"); |
| | | if (isTyping) return; |
| | | |
| | | // Enter: å·æ°æ¥è¯¢ |
| | | if (e.key === "Enter") { |
| | | e.preventDefault(); |
| | | handleQuery(); |
| | | return; |
| | | } |
| | | |
| | | // Esc: éç½® |
| | | if (e.key === "Escape") { |
| | | e.preventDefault(); |
| | | handleReset(); |
| | | return; |
| | | } |
| | | |
| | | // Alt+E: å¯¼åº |
| | | if (e.altKey && (e.key === "e" || e.key === "E")) { |
| | | e.preventDefault(); |
| | | handleExport(); |
| | | } |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | window.addEventListener("keydown", handleGlobalHotkeys); |
| | | }); |
| | | |
| | | onUnmounted(() => { |
| | | window.removeEventListener("keydown", handleGlobalHotkeys); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .app-container { |
| | | padding: 20px; |
| | | .energy-cost-page { |
| | | --lux-bg: #f6f7fb; |
| | | --lux-card: rgba(255, 255, 255, 0.86); |
| | | --lux-card-solid: #ffffff; |
| | | --lux-border: rgba(15, 23, 42, 0.08); |
| | | --lux-text: rgba(15, 23, 42, 0.92); |
| | | --lux-subtle: rgba(15, 23, 42, 0.58); |
| | | --lux-muted: rgba(15, 23, 42, 0.38); |
| | | --lux-primary: #2f6fed; |
| | | --lux-primary-2: #5b8cff; |
| | | --lux-success: #16a34a; |
| | | --lux-warning: #f59e0b; |
| | | --lux-danger: #ef4444; |
| | | --lux-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); |
| | | --lux-shadow-soft: 0 10px 28px rgba(15, 23, 42, 0.06); |
| | | --lux-radius: 14px; |
| | | --lux-radius-sm: 12px; |
| | | |
| | | padding: 18px 22px 24px; |
| | | background: |
| | | radial-gradient(1200px 420px at 20% 0%, rgba(47, 111, 237, 0.10), transparent 55%), |
| | | radial-gradient(900px 380px at 90% 10%, rgba(22, 163, 74, 0.06), transparent 55%), |
| | | linear-gradient(180deg, var(--lux-bg) 0%, #ffffff 58%); |
| | | } |
| | | |
| | | .search_form { |
| | | .filter-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | flex-wrap: nowrap; |
| | | margin-top: 0; |
| | | padding-top: 0; |
| | | border-top: none; |
| | | justify-content: flex-end; |
| | | flex: 0 0 auto; |
| | | white-space: nowrap; |
| | | align-self: flex-start; |
| | | padding-bottom: 0; |
| | | width: 290px; |
| | | } |
| | | |
| | | .filter-actions :deep(.el-button) { |
| | | min-width: 78px; |
| | | } |
| | | |
| | | .filter-actions :deep(.el-button.is-loading) { |
| | | min-width: 90px; |
| | | } |
| | | |
| | | .filter-layout { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | padding: 15px; |
| | | background-color: #f5f7fa; |
| | | border-radius: 8px; |
| | | gap: 14px; |
| | | } |
| | | |
| | | .statistics-overview { |
| | | margin-bottom: 30px; |
| | | .filter-form { |
| | | flex: 1 1 auto; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .charts-container { |
| | | margin-bottom: 30px; |
| | | .w-260 { |
| | | width: 260px; |
| | | max-width: 100%; |
| | | } |
| | | |
| | | .table-section { |
| | | margin-bottom: 20px; |
| | | .lux-btn { |
| | | transition: transform 0.18s ease, box-shadow 0.18s ease, filter 0.18s ease; |
| | | |
| | | &:hover { |
| | | transform: translateY(-1px); |
| | | box-shadow: 0 10px 22px rgba(15, 23, 42, 0.10); |
| | | filter: saturate(1.02); |
| | | } |
| | | |
| | | &:active { |
| | | transform: translateY(0); |
| | | box-shadow: none; |
| | | } |
| | | } |
| | | |
| | | .section-header { |
| | | .filter-card { |
| | | margin-bottom: 16px; |
| | | border-radius: var(--lux-radius); |
| | | border-color: var(--lux-border); |
| | | background: var(--lux-card); |
| | | backdrop-filter: blur(10px); |
| | | box-shadow: var(--lux-shadow-soft); |
| | | } |
| | | |
| | | /* æ¥è¯¢åºæ§ä»¶ç»ä¸ç®è¤ */ |
| | | :deep(.filter-card .el-form-item__label) { |
| | | color: rgba(15, 23, 42, 0.70); |
| | | font-weight: 650; |
| | | } |
| | | |
| | | :deep(.filter-card .el-input__wrapper), |
| | | :deep(.filter-card .el-select__wrapper) { |
| | | border-radius: 12px; |
| | | box-shadow: none; |
| | | border: 1px solid rgba(15, 23, 42, 0.10); |
| | | background: rgba(255, 255, 255, 0.82); |
| | | transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease; |
| | | } |
| | | |
| | | :deep(.filter-card .el-input__wrapper:hover), |
| | | :deep(.filter-card .el-select__wrapper:hover) { |
| | | border-color: rgba(47, 111, 237, 0.20); |
| | | transform: translateY(-1px); |
| | | } |
| | | |
| | | :deep(.filter-card .is-focus .el-input__wrapper), |
| | | :deep(.filter-card .is-focus .el-select__wrapper) { |
| | | border-color: rgba(47, 111, 237, 0.30); |
| | | box-shadow: 0 0 0 3px rgba(47, 111, 237, 0.14); |
| | | } |
| | | |
| | | :deep(.filter-card .el-range-editor.el-input__wrapper) { |
| | | border-radius: 12px; |
| | | } |
| | | |
| | | .card-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .card-head-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | min-width: 200px; |
| | | } |
| | | |
| | | .card-head-right { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .card-icon { |
| | | color: var(--lux-primary); |
| | | } |
| | | |
| | | .card-title { |
| | | font-weight: 760; |
| | | color: var(--lux-text); |
| | | } |
| | | |
| | | .subtle { |
| | | color: var(--lux-subtle); |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .filter-form { |
| | | display: flex; |
| | | flex-wrap: nowrap; |
| | | gap: 10px 14px; |
| | | align-items: flex-end; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .filter-form :deep(.el-form-item) { |
| | | margin-right: 0; |
| | | margin-bottom: 0; |
| | | flex: 0 0 auto; |
| | | } |
| | | |
| | | .filter-form :deep(.el-form-item__content) { |
| | | min-width: 0; |
| | | } |
| | | |
| | | .filter-form :deep(.el-form-item:last-child) { |
| | | flex: 1 1 auto; |
| | | } |
| | | |
| | | .filter-form :deep(.el-form-item:last-child .el-form-item__content) { |
| | | width: 100%; |
| | | } |
| | | |
| | | .w-140 { |
| | | width: 140px; |
| | | } |
| | | |
| | | .w-260 { |
| | | width: 260px; |
| | | max-width: 100%; |
| | | } |
| | | |
| | | @media (max-width: 1280px) { |
| | | .filter-form { |
| | | flex-wrap: wrap; |
| | | align-items: flex-start; |
| | | } |
| | | } |
| | | |
| | | .section-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | margin: 10px 0 12px; |
| | | font-size: 14px; |
| | | font-weight: 700; |
| | | color: var(--lux-text); |
| | | } |
| | | |
| | | .section-icon { |
| | | color: var(--lux-primary); |
| | | } |
| | | |
| | | .metrics { |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .ui-icon { |
| | | font-size: 16px; |
| | | transition: transform 0.18s ease, opacity 0.18s ease; |
| | | } |
| | | |
| | | .card-head-left:hover .ui-icon, |
| | | .section-title:hover .ui-icon { |
| | | transform: translateY(-1px); |
| | | } |
| | | |
| | | .metric-card { |
| | | border-radius: var(--lux-radius-sm); |
| | | padding: 14px 14px 14px 16px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | border: 1px solid var(--lux-border); |
| | | background: rgba(255, 255, 255, 0.9); |
| | | backdrop-filter: blur(10px); |
| | | min-height: 78px; |
| | | transition: box-shadow 0.20s ease, transform 0.20s ease, border-color 0.20s ease; |
| | | |
| | | &:hover { |
| | | transform: translateY(-1px); |
| | | box-shadow: var(--lux-shadow); |
| | | border-color: rgba(47, 111, 237, 0.18); |
| | | } |
| | | } |
| | | |
| | | .metric-left { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .metric-label { |
| | | color: var(--lux-subtle); |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .metric-value { |
| | | color: var(--lux-text); |
| | | font-size: 20px; |
| | | font-weight: 800; |
| | | letter-spacing: 0.2px; |
| | | } |
| | | |
| | | .metric-unit { |
| | | font-size: 12px; |
| | | font-weight: 500; |
| | | color: var(--lux-muted); |
| | | margin-left: 4px; |
| | | } |
| | | |
| | | .metric-right { |
| | | width: 42px; |
| | | height: 42px; |
| | | border-radius: 10px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .metric-icon { |
| | | font-size: 20px; |
| | | color: #fff; |
| | | } |
| | | |
| | | .metric-total { |
| | | background: linear-gradient(135deg, rgba(47, 111, 237, 0.12), rgba(47, 111, 237, 0.02)); |
| | | |
| | | .metric-right { |
| | | background: linear-gradient(135deg, var(--lux-primary), var(--lux-primary-2)); |
| | | } |
| | | } |
| | | |
| | | .metric-production { |
| | | background: linear-gradient(135deg, rgba(22, 163, 74, 0.12), rgba(22, 163, 74, 0.02)); |
| | | |
| | | .metric-right { |
| | | background: linear-gradient(135deg, var(--lux-success), rgba(22, 163, 74, 0.65)); |
| | | } |
| | | } |
| | | |
| | | .metric-office { |
| | | background: linear-gradient(135deg, rgba(144, 147, 153, 0.14), rgba(144, 147, 153, 0.03)); |
| | | |
| | | .metric-right { |
| | | background: linear-gradient(135deg, #909399, #b1b3b8); |
| | | } |
| | | } |
| | | |
| | | .metric-avg { |
| | | background: linear-gradient(135deg, rgba(245, 158, 11, 0.12), rgba(245, 158, 11, 0.02)); |
| | | |
| | | .metric-right { |
| | | background: linear-gradient(135deg, var(--lux-warning), rgba(245, 158, 11, 0.62)); |
| | | } |
| | | } |
| | | |
| | | .charts { |
| | | margin-top: 6px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .charts-row { |
| | | margin-top: 16px; |
| | | } |
| | | |
| | | .kpi-strip { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, minmax(0, 1fr)); |
| | | gap: 12px; |
| | | padding: 4px 4px 10px; |
| | | } |
| | | |
| | | .kpi-strip.pulse { |
| | | animation: kpiPulse 520ms cubic-bezier(0.16, 1, 0.3, 1); |
| | | } |
| | | |
| | | @keyframes kpiPulse { |
| | | 0% { |
| | | filter: saturate(1.02); |
| | | } |
| | | 35% { |
| | | filter: saturate(1.10); |
| | | } |
| | | 100% { |
| | | filter: saturate(1.02); |
| | | } |
| | | } |
| | | |
| | | .kpi-item { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | padding: 12px 12px 12px 14px; |
| | | border-radius: 14px; |
| | | border: 1px solid rgba(15, 23, 42, 0.08); |
| | | background: rgba(255, 255, 255, 0.86); |
| | | transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; |
| | | min-height: 68px; |
| | | text-align: left; |
| | | cursor: pointer; |
| | | position: relative; |
| | | overflow: hidden; |
| | | outline: none; |
| | | transform: translateZ(0); |
| | | } |
| | | |
| | | .kpi-item:hover { |
| | | transform: translateY(-1px); |
| | | box-shadow: 0 16px 40px rgba(15, 23, 42, 0.10); |
| | | border-color: rgba(47, 111, 237, 0.18); |
| | | } |
| | | |
| | | .kpi-item::before { |
| | | content: ""; |
| | | position: absolute; |
| | | inset: 0; |
| | | background: |
| | | radial-gradient(520px 140px at 20% 0%, rgba(255, 255, 255, 0.65), transparent 60%), |
| | | radial-gradient(620px 180px at 90% 40%, rgba(47, 111, 237, 0.10), transparent 55%); |
| | | opacity: 0; |
| | | transform: translateX(-8%) translateY(-2%); |
| | | transition: opacity 0.22s ease, transform 0.42s cubic-bezier(0.16, 1, 0.3, 1); |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .kpi-item:hover::before { |
| | | opacity: 1; |
| | | transform: translateX(0) translateY(0); |
| | | } |
| | | |
| | | .kpi-item::after { |
| | | content: ""; |
| | | position: absolute; |
| | | inset: -1px; |
| | | border-radius: 15px; |
| | | background: linear-gradient( |
| | | 135deg, |
| | | rgba(47, 111, 237, 0.18), |
| | | rgba(255, 255, 255, 0.0), |
| | | rgba(22, 163, 74, 0.14) |
| | | ); |
| | | opacity: 0; |
| | | transition: opacity 0.22s ease; |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .kpi-item:hover::after { |
| | | opacity: 1; |
| | | } |
| | | |
| | | .kpi-item:active { |
| | | transform: translateY(0); |
| | | box-shadow: 0 10px 22px rgba(15, 23, 42, 0.08); |
| | | } |
| | | |
| | | .kpi-item:focus-visible { |
| | | box-shadow: |
| | | 0 16px 44px rgba(15, 23, 42, 0.10), |
| | | 0 0 0 3px rgba(47, 111, 237, 0.18); |
| | | border-color: rgba(47, 111, 237, 0.22); |
| | | } |
| | | |
| | | .kpi-item.selected { |
| | | border-color: rgba(47, 111, 237, 0.22); |
| | | box-shadow: |
| | | 0 16px 44px rgba(15, 23, 42, 0.10), |
| | | inset 0 0 0 1px rgba(47, 111, 237, 0.10); |
| | | } |
| | | |
| | | .kpi-left { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 6px; |
| | | min-width: 0; |
| | | position: relative; |
| | | z-index: 1; |
| | | } |
| | | |
| | | .kpi-label { |
| | | font-size: 12px; |
| | | color: var(--lux-subtle); |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | .kpi-value { |
| | | font-size: 18px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | margin-bottom: 15px; |
| | | padding-left: 10px; |
| | | border-left: 3px solid #409eff; |
| | | |
| | | .header-icon { |
| | | margin-right: 8px; |
| | | color: #409eff; |
| | | } |
| | | font-weight: 850; |
| | | letter-spacing: 0.2px; |
| | | color: var(--lux-text); |
| | | line-height: 1.1; |
| | | } |
| | | |
| | | .overview-card { |
| | | .kpi-meta { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 20px; |
| | | border-radius: 4px; |
| | | background: #fff; |
| | | border: 1px solid #ebeef5; |
| | | gap: 8px; |
| | | margin-top: 2px; |
| | | min-height: 22px; |
| | | } |
| | | |
| | | &.blue-card { |
| | | background-color: #ecf5ff; |
| | | } |
| | | .kpi-meta.muted { |
| | | font-size: 11px; |
| | | color: var(--lux-muted); |
| | | } |
| | | |
| | | &.green-card { |
| | | background-color: #f0f9eb; |
| | | } |
| | | .kpi-chip { |
| | | font-size: 11px; |
| | | font-weight: 700; |
| | | padding: 2px 8px; |
| | | border-radius: 999px; |
| | | border: 1px solid rgba(15, 23, 42, 0.08); |
| | | background: rgba(255, 255, 255, 0.72); |
| | | color: rgba(15, 23, 42, 0.72); |
| | | } |
| | | |
| | | &.purple-card { |
| | | background-color: #f3f0ff; |
| | | } |
| | | .kpi-chip.up { |
| | | border-color: rgba(22, 163, 74, 0.20); |
| | | color: rgba(22, 163, 74, 0.96); |
| | | background: rgba(22, 163, 74, 0.06); |
| | | } |
| | | |
| | | &.gray-card { |
| | | background-color: #f5f7fa; |
| | | } |
| | | .kpi-chip.down { |
| | | border-color: rgba(239, 68, 68, 0.20); |
| | | color: rgba(239, 68, 68, 0.96); |
| | | background: rgba(239, 68, 68, 0.06); |
| | | } |
| | | |
| | | .overview-icon { |
| | | width: 40px; |
| | | height: 40px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 50%; |
| | | margin-right: 15px; |
| | | .kpi-spark { |
| | | width: 72px; |
| | | height: 22px; |
| | | opacity: 0.9; |
| | | filter: drop-shadow(0 8px 16px rgba(15, 23, 42, 0.10)); |
| | | } |
| | | |
| | | &.blue-icon { |
| | | background-color: #409eff; |
| | | color: #fff; |
| | | } |
| | | .kpi-actions { |
| | | position: absolute; |
| | | top: 10px; |
| | | right: 10px; |
| | | display: flex; |
| | | gap: 6px; |
| | | opacity: 0; |
| | | transform: translateY(-2px); |
| | | pointer-events: none; |
| | | transition: opacity 0.16s ease, transform 0.16s ease; |
| | | z-index: 2; |
| | | } |
| | | |
| | | &.green-icon { |
| | | background-color: #67c23a; |
| | | color: #fff; |
| | | } |
| | | .kpi-item:hover .kpi-actions { |
| | | opacity: 1; |
| | | transform: translateY(0); |
| | | pointer-events: auto; |
| | | } |
| | | |
| | | &.purple-icon { |
| | | background-color: #909399; |
| | | color: #fff; |
| | | } |
| | | .kpi-action { |
| | | font-size: 11px; |
| | | font-weight: 650; |
| | | padding: 4px 8px; |
| | | border-radius: 999px; |
| | | border: 1px solid rgba(15, 23, 42, 0.10); |
| | | background: rgba(255, 255, 255, 0.78); |
| | | color: rgba(15, 23, 42, 0.78); |
| | | cursor: pointer; |
| | | transition: background-color 0.16s ease, border-color 0.16s ease, transform 0.16s ease; |
| | | } |
| | | |
| | | &.gray-icon { |
| | | background-color: #909399; |
| | | color: #fff; |
| | | } |
| | | .kpi-action:hover { |
| | | background: rgba(47, 111, 237, 0.08); |
| | | border-color: rgba(47, 111, 237, 0.22); |
| | | transform: translateY(-1px); |
| | | } |
| | | |
| | | .el-icon { |
| | | font-size: 20px; |
| | | } |
| | | } |
| | | .kpi-action:active { |
| | | transform: translateY(0); |
| | | } |
| | | |
| | | .overview-info { |
| | | flex: 1; |
| | | .chart-wrap { |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | position: relative; |
| | | } |
| | | |
| | | .overview-label { |
| | | font-size: 14px; |
| | | color: #606266; |
| | | margin-bottom: 5px; |
| | | } |
| | | .chart-empty { |
| | | height: 240px; |
| | | display: grid; |
| | | place-items: center; |
| | | background: rgba(255, 255, 255, 0.70); |
| | | border-radius: 12px; |
| | | position: absolute; |
| | | inset: 0; |
| | | } |
| | | |
| | | .overview-value { |
| | | font-size: 20px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | :deep(.big-chart-dialog .el-dialog) { |
| | | border-radius: 16px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .unit { |
| | | font-size: 12px; |
| | | font-weight: normal; |
| | | color: #909399; |
| | | } |
| | | } |
| | | } |
| | | :deep(.big-chart-dialog .el-dialog__header) { |
| | | padding: 14px 16px; |
| | | background: |
| | | radial-gradient(900px 240px at 10% 0%, rgba(47, 111, 237, 0.10), transparent 55%), |
| | | rgba(255, 255, 255, 0.92); |
| | | border-bottom: 1px solid rgba(15, 23, 42, 0.06); |
| | | } |
| | | |
| | | :deep(.big-chart-dialog .el-dialog__body) { |
| | | padding: 14px 16px 8px; |
| | | background: rgba(255, 255, 255, 0.92); |
| | | } |
| | | |
| | | :deep(.big-chart-dialog .el-dialog__footer) { |
| | | padding: 10px 16px 14px; |
| | | background: rgba(255, 255, 255, 0.92); |
| | | border-top: 1px solid rgba(15, 23, 42, 0.06); |
| | | } |
| | | |
| | | .big-chart-canvas { |
| | | width: 100%; |
| | | height: min(74vh, 760px); |
| | | border-radius: 14px; |
| | | border: 1px solid rgba(15, 23, 42, 0.08); |
| | | background: #ffffff; |
| | | } |
| | | |
| | | .big-chart-footer { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .chart-tools { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | opacity: 0.0; |
| | | transform: translateY(-2px); |
| | | transition: opacity 0.16s ease, transform 0.16s ease; |
| | | } |
| | | |
| | | .chart-card:hover .chart-tools { |
| | | opacity: 1; |
| | | transform: translateY(0); |
| | | } |
| | | |
| | | .chart-card:focus-within .chart-tools { |
| | | opacity: 1; |
| | | transform: translateY(0); |
| | | } |
| | | |
| | | :deep(.chart-wrap .el-loading-mask) { |
| | | border-radius: 12px; |
| | | backdrop-filter: blur(2px); |
| | | background-color: rgba(255, 255, 255, 0.55); |
| | | } |
| | | |
| | | .chart-tool { |
| | | font-size: 11px; |
| | | font-weight: 650; |
| | | padding: 4px 8px; |
| | | border-radius: 10px; |
| | | border: 1px solid rgba(15, 23, 42, 0.10); |
| | | background: rgba(255, 255, 255, 0.78); |
| | | color: rgba(15, 23, 42, 0.78); |
| | | cursor: pointer; |
| | | transition: background-color 0.16s ease, border-color 0.16s ease, transform 0.16s ease; |
| | | } |
| | | |
| | | .chart-tool:hover { |
| | | background: rgba(47, 111, 237, 0.08); |
| | | border-color: rgba(47, 111, 237, 0.22); |
| | | transform: translateY(-1px); |
| | | } |
| | | |
| | | .kpi-unit { |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | color: var(--lux-muted); |
| | | margin-left: 4px; |
| | | } |
| | | |
| | | .kpi-icon { |
| | | width: 38px; |
| | | height: 38px; |
| | | border-radius: 12px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: #fff; |
| | | flex: 0 0 auto; |
| | | position: relative; |
| | | z-index: 1; |
| | | transition: transform 0.28s cubic-bezier(0.16, 1, 0.3, 1), filter 0.28s ease; |
| | | } |
| | | |
| | | .kpi-item:hover .kpi-icon { |
| | | transform: translateY(-1px) rotate(-2deg); |
| | | filter: saturate(1.06); |
| | | } |
| | | |
| | | .kpi-total { |
| | | background: linear-gradient(135deg, rgba(47, 111, 237, 0.10), rgba(255, 255, 255, 0.86)); |
| | | } |
| | | .kpi-total .kpi-icon { |
| | | background: linear-gradient(135deg, var(--lux-primary), var(--lux-primary-2)); |
| | | } |
| | | |
| | | .kpi-production { |
| | | background: linear-gradient(135deg, rgba(22, 163, 74, 0.10), rgba(255, 255, 255, 0.86)); |
| | | } |
| | | .kpi-production .kpi-icon { |
| | | background: linear-gradient(135deg, var(--lux-success), rgba(22, 163, 74, 0.65)); |
| | | } |
| | | |
| | | .kpi-office { |
| | | background: linear-gradient(135deg, rgba(100, 116, 139, 0.10), rgba(255, 255, 255, 0.86)); |
| | | } |
| | | .kpi-office .kpi-icon { |
| | | background: linear-gradient(135deg, #64748b, #94a3b8); |
| | | } |
| | | |
| | | .kpi-avg { |
| | | background: linear-gradient(135deg, rgba(245, 158, 11, 0.10), rgba(255, 255, 255, 0.86)); |
| | | } |
| | | .kpi-avg .kpi-icon { |
| | | background: linear-gradient(135deg, var(--lux-warning), rgba(245, 158, 11, 0.62)); |
| | | } |
| | | |
| | | .panel-card { |
| | | margin-top: 0; |
| | | border-radius: var(--lux-radius); |
| | | border-color: var(--lux-border); |
| | | background: var(--lux-card); |
| | | backdrop-filter: blur(10px); |
| | | box-shadow: var(--lux-shadow-soft); |
| | | padding-bottom: 8px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .panel-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | padding: 8px 4px 6px; |
| | | } |
| | | |
| | | .segmented { |
| | | position: relative; |
| | | display: grid; |
| | | grid-template-columns: 1fr 1fr; |
| | | padding: 4px; |
| | | border-radius: 14px; |
| | | border: 1px solid rgba(15, 23, 42, 0.08); |
| | | background: rgba(15, 23, 42, 0.03); |
| | | overflow: hidden; |
| | | flex: 1 1 auto; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .segmented::after { |
| | | content: ""; |
| | | position: absolute; |
| | | top: 10px; |
| | | bottom: 10px; |
| | | left: 50%; |
| | | width: 2px; |
| | | border-radius: 999px; |
| | | background: linear-gradient( |
| | | 180deg, |
| | | rgba(15, 23, 42, 0.06), |
| | | rgba(15, 23, 42, 0.12), |
| | | rgba(15, 23, 42, 0.06) |
| | | ); |
| | | transform: translateX(-0.5px); |
| | | pointer-events: none; |
| | | z-index: 0; |
| | | } |
| | | |
| | | .segmented.no-active { |
| | | background: |
| | | radial-gradient(900px 220px at 20% 0%, rgba(47, 111, 237, 0.06), transparent 55%), |
| | | rgba(15, 23, 42, 0.03); |
| | | border-color: rgba(15, 23, 42, 0.10); |
| | | } |
| | | |
| | | .segmented-indicator { |
| | | position: absolute; |
| | | top: 4px; |
| | | left: 4px; |
| | | width: calc(50% - 4px); |
| | | height: calc(100% - 8px); |
| | | border-radius: 13px; |
| | | background: linear-gradient(180deg, rgba(47, 111, 237, 0.10), rgba(255, 255, 255, 0.82)); |
| | | border: 1px solid rgba(47, 111, 237, 0.18); |
| | | box-shadow: |
| | | 0 14px 30px rgba(15, 23, 42, 0.10), |
| | | 0 1px 0 rgba(255, 255, 255, 0.65) inset; |
| | | transition: |
| | | transform 0.36s cubic-bezier(0.16, 1, 0.3, 1), |
| | | opacity 0.20s ease; |
| | | pointer-events: none; |
| | | will-change: transform; |
| | | z-index: 1; |
| | | } |
| | | |
| | | .segmented-indicator.hidden { |
| | | opacity: 0; |
| | | } |
| | | |
| | | .segmented-item { |
| | | position: relative; |
| | | z-index: 2; |
| | | width: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 2px; |
| | | text-align: left; |
| | | padding: 10px 12px; |
| | | border-radius: 12px; |
| | | border: 1px solid transparent; |
| | | background: transparent; |
| | | cursor: pointer; |
| | | transition: transform 0.16s ease, color 0.16s ease, background-color 0.16s ease; |
| | | } |
| | | |
| | | .segmented-item:hover { |
| | | transform: translateY(-1px); |
| | | } |
| | | |
| | | .segmented-item:active { |
| | | transform: translateY(0); |
| | | } |
| | | |
| | | .seg-title { |
| | | font-size: 13px; |
| | | font-weight: 780; |
| | | color: rgba(15, 23, 42, 0.86); |
| | | letter-spacing: 0.2px; |
| | | } |
| | | |
| | | .seg-sub { |
| | | font-size: 11px; |
| | | color: rgba(15, 23, 42, 0.46); |
| | | } |
| | | |
| | | .segmented-item.active .seg-title { |
| | | color: var(--lux-text); |
| | | } |
| | | |
| | | .segmented-item.active .seg-sub { |
| | | color: rgba(15, 23, 42, 0.56); |
| | | } |
| | | |
| | | .panel-body { |
| | | padding-top: 4px; |
| | | } |
| | | |
| | | .core-kpi { |
| | | font-size: 12px; |
| | | font-weight: 650; |
| | | color: rgba(15, 23, 42, 0.78); |
| | | padding: 2px 10px; |
| | | border-radius: 999px; |
| | | background: rgba(15, 23, 42, 0.04); |
| | | border: 1px solid rgba(15, 23, 42, 0.06); |
| | | } |
| | | |
| | | .chart-card { |
| | | background: #fff; |
| | | border-radius: 4px; |
| | | border: 1px solid #ebeef5; |
| | | padding: 20px; |
| | | border-radius: var(--lux-radius); |
| | | border-color: var(--lux-border); |
| | | background: var(--lux-card); |
| | | backdrop-filter: blur(10px); |
| | | box-shadow: var(--lux-shadow-soft); |
| | | transition: box-shadow 0.22s ease, transform 0.22s ease, border-color 0.22s ease; |
| | | |
| | | .chart-title { |
| | | font-size: 14px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | margin-bottom: 15px; |
| | | padding-bottom: 10px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | &:hover { |
| | | transform: translateY(-2px); |
| | | box-shadow: var(--lux-shadow); |
| | | border-color: rgba(47, 111, 237, 0.16); |
| | | } |
| | | } |
| | | |
| | | .chart-content { |
| | | height: 300px; |
| | | .chart-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .chart-title { |
| | | font-weight: 700; |
| | | color: var(--lux-text); |
| | | } |
| | | |
| | | .chart-content { |
| | | height: 240px; |
| | | } |
| | | |
| | | .table-card { |
| | | border-radius: var(--lux-radius); |
| | | border-color: var(--lux-border); |
| | | background: var(--lux-card); |
| | | backdrop-filter: blur(10px); |
| | | box-shadow: var(--lux-shadow-soft); |
| | | transition: box-shadow 0.22s ease, transform 0.22s ease, border-color 0.22s ease; |
| | | |
| | | &:hover { |
| | | transform: translateY(-1px); |
| | | box-shadow: var(--lux-shadow); |
| | | border-color: rgba(15, 23, 42, 0.10); |
| | | } |
| | | } |
| | | |
| | | .data-table { |
| | | width: 100%; |
| | | } |
| | | |
| | | .consumption-value { |
| | | font-weight: bold; |
| | | color: #409eff; |
| | | color: var(--lux-primary); |
| | | } |
| | | |
| | | .consumption-unit { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | color: var(--lux-muted); |
| | | margin-left: 2px; |
| | | } |
| | | |
| | | .price-value { |
| | | font-weight: bold; |
| | | color: #67c23a; |
| | | color: var(--lux-success); |
| | | } |
| | | |
| | | .cost-value { |
| | | font-weight: bold; |
| | | color: #f56c6c; |
| | | color: var(--lux-danger); |
| | | } |
| | | |
| | | .pagination-container { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding-top: 12px; |
| | | } |
| | | |
| | | /* Element Plus æ·±åº¦æ ·å¼ï¼å¡çå¼è¡¨æ ¼è´¨æ */ |
| | | :deep(.lux-table) { |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | font-variant-numeric: tabular-nums; |
| | | } |
| | | |
| | | :deep(.lux-table .el-table__inner-wrapper::before) { |
| | | height: 0; |
| | | } |
| | | |
| | | :deep(.lux-table .el-table__header-wrapper) { |
| | | background: |
| | | linear-gradient(180deg, rgba(15, 23, 42, 0.04) 0%, rgba(15, 23, 42, 0.02) 100%); |
| | | } |
| | | |
| | | :deep(.lux-table th.el-table__cell) { |
| | | background: transparent; |
| | | color: rgba(15, 23, 42, 0.78); |
| | | font-weight: 700; |
| | | letter-spacing: 0.2px; |
| | | border-bottom: 1px solid rgba(15, 23, 42, 0.08); |
| | | } |
| | | |
| | | :deep(.lux-table td.el-table__cell) { |
| | | border-bottom: 1px solid rgba(15, 23, 42, 0.06); |
| | | } |
| | | |
| | | :deep(.lux-table .el-table__row) { |
| | | transition: background-color 0.18s ease; |
| | | } |
| | | |
| | | :deep(.lux-table .el-table__row:hover > td.el-table__cell) { |
| | | background-color: rgba(47, 111, 237, 0.06) !important; |
| | | } |
| | | |
| | | :deep(.lux-table .el-table__row:hover) { |
| | | box-shadow: inset 3px 0 0 rgba(47, 111, 237, 0.30); |
| | | } |
| | | |
| | | :deep(.lux-table .el-table__body tr.el-table__row--striped > td.el-table__cell) { |
| | | background: rgba(15, 23, 42, 0.018); |
| | | } |
| | | |
| | | :deep(.el-pagination) { |
| | | --el-pagination-button-color: rgba(15, 23, 42, 0.72); |
| | | --el-pagination-button-bg-color: transparent; |
| | | --el-pagination-hover-color: var(--lux-primary); |
| | | } |
| | | |
| | | :deep(.el-pagination .btn-next), |
| | | :deep(.el-pagination .btn-prev) { |
| | | border-radius: 10px; |
| | | transition: background-color 0.18s ease, transform 0.18s ease; |
| | | } |
| | | |
| | | :deep(.el-pagination .btn-next:hover), |
| | | :deep(.el-pagination .btn-prev:hover) { |
| | | background-color: rgba(47, 111, 237, 0.06); |
| | | transform: translateY(-1px); |
| | | } |
| | | |
| | | /* ååºå¼ */ |
| | | @media (max-width: 960px) { |
| | | .filter-form { |
| | | flex-direction: column; |
| | | align-items: flex-start; |
| | | } |
| | | |
| | | .filter-actions { |
| | | justify-content: flex-start; |
| | | } |
| | | |
| | | .kpi-strip { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | |
| | | .panel-head { |
| | | flex-wrap: wrap; |
| | | } |
| | | } |
| | | |
| | | /* æå å¨ç» */ |
| | | .lux-collapse-enter-active, |
| | | .lux-collapse-leave-active { |
| | | transition: max-height 0.22s ease, opacity 0.18s ease; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .lux-collapse-enter-from, |
| | | .lux-collapse-leave-to { |
| | | max-height: 0; |
| | | opacity: 0; |
| | | } |
| | | |
| | | .lux-collapse-enter-to, |
| | | .lux-collapse-leave-from { |
| | | max-height: 600px; |
| | | opacity: 1; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog v-model="isShow" |
| | | title="ç¼è¾å·¥èºè·¯çº¿" |
| | | width="400" |
| | | @close="closeModal"> |
| | | <el-form label-width="140px" |
| | | :model="formState" |
| | | label-position="top" |
| | | ref="formRef"> |
| | | <el-form-item label="产ååç§°" |
| | | prop="productModelId" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©äº§å', |
| | | trigger: 'change', |
| | | } |
| | | ]"> |
| | | <el-button type="primary" |
| | | @click="showProductSelectDialog = true"> |
| | | {{ formState.productName && formState.productModelName |
| | | ? `${formState.productName} - ${formState.productModelName}` |
| | | : 'éæ©äº§å' }} |
| | | </el-button> |
| | | </el-form-item> |
| | | <el-form-item label="BOM" |
| | | prop="bomId" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©BOM', |
| | | trigger: 'change', |
| | | } |
| | | ]"> |
| | | <el-select v-model="formState.bomId" |
| | | placeholder="è¯·éæ©BOM" |
| | | clearable |
| | | :disabled="!formState.productModelId || bomOptions.length === 0" |
| | | style="width: 100%"> |
| | | <el-option v-for="item in bomOptions" |
| | | :key="item.id" |
| | | :label="item.bomNo || `BOM-${item.id}`" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨" |
| | | prop="description"> |
| | | <el-input v-model="formState.description" |
| | | type="textarea" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <!-- 产åéæ©å¼¹çª --> |
| | | <ProductSelectDialog v-model="showProductSelectDialog" |
| | | @confirm="handleProductSelect" |
| | | single /> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" |
| | | @click="handleSubmit">确认</el-button> |
| | | <el-button @click="closeModal">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { |
| | | ref, |
| | | computed, |
| | | getCurrentInstance, |
| | | onMounted, |
| | | nextTick, |
| | | watch, |
| | | } from "vue"; |
| | | import { update } from "@/api/productionManagement/processRoute.js"; |
| | | import { getByModel } from "@/api/productionManagement/productBom.js"; |
| | | import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; |
| | | |
| | | const props = defineProps({ |
| | | visible: { |
| | | type: Boolean, |
| | | required: true, |
| | | }, |
| | | |
| | | record: { |
| | | type: Object, |
| | | required: true, |
| | | }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:visible", "completed"]); |
| | | |
| | | // ååºå¼æ°æ®ï¼æ¿ä»£é项å¼ç dataï¼ |
| | | const formState = ref({ |
| | | productId: undefined, |
| | | productModelId: undefined, |
| | | productName: "", |
| | | productModelName: "", |
| | | bomId: undefined, |
| | | description: "", |
| | | }); |
| | | |
| | | const isShow = computed({ |
| | | get() { |
| | | return props.visible; |
| | | }, |
| | | set(val) { |
| | | emit("update:visible", val); |
| | | }, |
| | | }); |
| | | |
| | | const showProductSelectDialog = ref(false); |
| | | const bomOptions = ref([]); |
| | | |
| | | let { proxy } = getCurrentInstance(); |
| | | |
| | | const closeModal = () => { |
| | | isShow.value = false; |
| | | }; |
| | | |
| | | // è®¾ç½®è¡¨åæ°æ® |
| | | const setFormData = () => { |
| | | if (props.record) { |
| | | formState.value = { |
| | | ...props.record, |
| | | productId: props.record.productId, |
| | | productModelId: props.record.productModelId, |
| | | productName: props.record.productName || "", |
| | | // 注æï¼recordä¸çåæ®µæ¯modelï¼éè¦æ å°å°productModelName |
| | | productModelName: |
| | | props.record.model || props.record.productModelName || "", |
| | | bomId: props.record.bomId, |
| | | description: props.record.description || "", |
| | | }; |
| | | // 妿æäº§ååå·IDï¼å è½½BOMå表 |
| | | if (props.record.productModelId) { |
| | | loadBomList(props.record.productModelId); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // å è½½BOMå表 |
| | | const loadBomList = async productModelId => { |
| | | if (!productModelId) { |
| | | bomOptions.value = []; |
| | | return; |
| | | } |
| | | try { |
| | | const res = await getByModel(productModelId); |
| | | // å¤çè¿åçBOMæ°æ®ï¼å¯è½æ¯æ°ç»ã对象æå
å«dataåæ®µ |
| | | let bomList = []; |
| | | if (Array.isArray(res)) { |
| | | bomList = res; |
| | | } else if (res && res.data) { |
| | | bomList = Array.isArray(res.data) ? res.data : [res.data]; |
| | | } else if (res && typeof res === "object") { |
| | | bomList = [res]; |
| | | } |
| | | bomOptions.value = bomList; |
| | | } catch (error) { |
| | | console.error("å è½½BOMå表失败ï¼", error); |
| | | bomOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | // 产åéæ©å¤ç |
| | | const handleProductSelect = async products => { |
| | | if (products && products.length > 0) { |
| | | const product = products[0]; |
| | | // å
æ¥è¯¢BOMå表ï¼å¿
éï¼ |
| | | try { |
| | | const res = await getByModel(product.id); |
| | | // å¤çè¿åçBOMæ°æ®ï¼å¯è½æ¯æ°ç»ã对象æå
å«dataåæ®µ |
| | | let bomList = []; |
| | | if (Array.isArray(res)) { |
| | | bomList = res; |
| | | } else if (res && res.data) { |
| | | bomList = Array.isArray(res.data) ? res.data : [res.data]; |
| | | } else if (res && typeof res === "object") { |
| | | bomList = [res]; |
| | | } |
| | | |
| | | if (bomList.length > 0) { |
| | | formState.value.productModelId = product.id; |
| | | formState.value.productName = product.productName; |
| | | formState.value.productModelName = product.model; |
| | | // 妿å½åéæ©çBOMä¸å¨æ°å表ä¸ï¼åéç½®BOMéæ© |
| | | const currentBomExists = bomList.some( |
| | | bom => bom.id === formState.value.bomId |
| | | ); |
| | | if (!currentBomExists) { |
| | | formState.value.bomId = undefined; |
| | | } |
| | | bomOptions.value = bomList; |
| | | showProductSelectDialog.value = false; |
| | | // 触å表åéªè¯æ´æ° |
| | | proxy.$refs["formRef"]?.validateField("productModelId"); |
| | | } else { |
| | | proxy.$modal.msgError("è¯¥äº§åæ²¡æBOMï¼è¯·å
å建BOM"); |
| | | } |
| | | } catch (error) { |
| | | // 妿æ¥å£è¿å404æå
¶ä»é误ï¼è¯´ææ²¡æBOM |
| | | proxy.$modal.msgError("è¯¥äº§åæ²¡æBOMï¼è¯·å
å建BOM"); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const handleSubmit = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | // éªè¯æ¯å¦éæ©äºäº§ååBOM |
| | | if (!formState.value.productModelId) { |
| | | proxy.$modal.msgError("è¯·éæ©äº§å"); |
| | | return; |
| | | } |
| | | if (!formState.value.bomId) { |
| | | proxy.$modal.msgError("è¯·éæ©BOM"); |
| | | return; |
| | | } |
| | | update(formState.value).then(res => { |
| | | // å
³éæ¨¡ææ¡ |
| | | isShow.value = false; |
| | | // åç¥ç¶ç»ä»¶å·²å®æ |
| | | emit("completed"); |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | defineExpose({ |
| | | closeModal, |
| | | handleSubmit, |
| | | isShow, |
| | | }); |
| | | |
| | | // çå¬å¼¹çªæå¼ï¼åå§åè¡¨åæ°æ® |
| | | watch( |
| | | () => props.visible, |
| | | visible => { |
| | | if (visible && props.record) { |
| | | nextTick(() => { |
| | | setFormData(); |
| | | }); |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ); |
| | | |
| | | onMounted(() => { |
| | | if (props.visible && props.record) { |
| | | setFormData(); |
| | | } |
| | | }); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="isShow" |
| | | title="å·¥èºè·¯çº¿é¡¹ç®" |
| | | width="800px" |
| | | @close="closeModal" |
| | | > |
| | | <div class="operate-button"> |
| | | <el-button |
| | | type="primary" |
| | | @click="isShowProductSelectDialog = true" |
| | | class="mb5" |
| | | style="margin-bottom: 10px;" |
| | | > |
| | | éæ©äº§å |
| | | </el-button> |
| | | |
| | | <el-switch |
| | | v-model="isTable" |
| | | inline-prompt |
| | | active-text="è¡¨æ ¼" |
| | | inactive-text="å表" |
| | | @change="handleViewChange" |
| | | /> |
| | | </div> |
| | | |
| | | <el-table |
| | | v-if="isTable" |
| | | ref="multipleTable" |
| | | v-loading="tableLoading" |
| | | border |
| | | :data="routeItems" |
| | | :header-cell-style="{ background: '#F0F1F5', color: '#333333' }" |
| | | row-key="id" |
| | | tooltip-effect="dark" |
| | | class="lims-table" |
| | | style="cursor: move;" |
| | | > |
| | | <el-table-column align="center" label="åºå·" width="60"> |
| | | <template #default="scope"> |
| | | {{ scope.$index + 1 }} |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column |
| | | v-for="(item, index) in tableColumn" |
| | | :key="index" |
| | | :label="item.label" |
| | | :width="item.width" |
| | | show-overflow-tooltip |
| | | > |
| | | <template #default="scope" v-if="item.dataType === 'action'"> |
| | | <el-button |
| | | v-for="(op, opIndex) in item.operation" |
| | | :key="opIndex" |
| | | :type="op.type" |
| | | :link="op.link" |
| | | size="small" |
| | | @click.stop="op.clickFun(scope.row)" |
| | | > |
| | | {{ op.name }} |
| | | </el-button> |
| | | </template> |
| | | |
| | | <template #default="scope" v-else> |
| | | <template v-if="item.prop === 'processId'"> |
| | | <el-select |
| | | v-model="scope.row[item.prop]" |
| | | style="width: 100%;" |
| | | @mousedown.stop |
| | | > |
| | | <el-option |
| | | v-for="process in processOptions" |
| | | :key="process.id" |
| | | :label="process.name" |
| | | :value="process.id" |
| | | /> |
| | | </el-select> |
| | | </template> |
| | | <template v-else> |
| | | {{ scope.row[item.prop] || '-' }} |
| | | </template> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <!-- ä½¿ç¨æ®édivæ¿ä»£el-steps --> |
| | | <div |
| | | v-else |
| | | ref="stepsContainer" |
| | | class="mb5 custom-steps" |
| | | style="padding: 10px 0; display: flex; flex-wrap: nowrap; gap: 20px; align-items: flex-start;" |
| | | > |
| | | <div |
| | | v-for="(item, index) in routeItems" |
| | | :key="item.id" |
| | | class="custom-step draggable-step" |
| | | :data-id="item.id" |
| | | style="cursor: move; flex: 0 0 auto; min-width: 220px;" |
| | | > |
| | | <div class="step-content"> |
| | | <div class="step-number">{{ index + 1 }}</div> |
| | | <el-card |
| | | :header="item.productName" |
| | | class="step-card" |
| | | style="cursor: move;" |
| | | > |
| | | <div class="step-card-content"> |
| | | <p>{{ item.model }}</p> |
| | | <p>{{ item.unit }}</p> |
| | | <el-select |
| | | v-model="item.processId" |
| | | style="width: 100%;" |
| | | @mousedown.stop |
| | | > |
| | | <el-option |
| | | v-for="process in processOptions" |
| | | :key="process.id" |
| | | :label="process.name" |
| | | :value="process.id" |
| | | /> |
| | | </el-select> |
| | | </div> |
| | | <template #footer> |
| | | <div class="step-card-footer"> |
| | | <el-button type="danger" link size="small" @click.stop="removeItemByID(item.id)">å é¤</el-button> |
| | | </div> |
| | | </template> |
| | | </el-card> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="handleSubmit">确认</el-button> |
| | | <el-button @click="closeModal">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <ProductSelectDialog |
| | | v-model="isShowProductSelectDialog" |
| | | @confirm="handelSelectProducts" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, getCurrentInstance, onMounted, onUnmounted, nextTick } from "vue"; |
| | | import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; |
| | | import { findProcessRouteItemList, addOrUpdateProcessRouteItem } from "@/api/productionManagement/processRouteItem.js"; |
| | | import { processList } from "@/api/productionManagement/productionProcess.js"; |
| | | import Sortable from 'sortablejs'; |
| | | |
| | | const props = defineProps({ |
| | | visible: { |
| | | type: Boolean, |
| | | required: true, |
| | | default: false |
| | | }, |
| | | record: { |
| | | type: Object, |
| | | required: true, |
| | | default: () => ({}) |
| | | } |
| | | }); |
| | | |
| | | const emit = defineEmits(['update:visible', 'completed']); |
| | | |
| | | const processOptions = ref([]); |
| | | const tableLoading = ref(false); |
| | | const isShowProductSelectDialog = ref(false); |
| | | const routeItems = ref([]); |
| | | let tableSortable = null; |
| | | let stepsSortable = null; |
| | | const multipleTable = ref(null); |
| | | const stepsContainer = ref(null); |
| | | const isTable = ref(true); |
| | | |
| | | const isShow = computed({ |
| | | get() { |
| | | return props.visible; |
| | | }, |
| | | set(val) { |
| | | emit('update:visible', val); |
| | | } |
| | | }); |
| | | |
| | | const tableColumn = ref([ |
| | | { label: "产ååç§°", prop: "productName", width: 180 }, |
| | | { label: "è§æ ¼åç§°", prop: "model", width: 150 }, |
| | | { label: "åä½", prop: "unit", width: 80 }, |
| | | { label: "å·¥åºåç§°", prop: "processId", width: 180 }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 100, |
| | | operation: [ |
| | | { |
| | | name: "å é¤", |
| | | type: "danger", |
| | | link: true, |
| | | clickFun: (row) => { |
| | | const idx = routeItems.value.findIndex(item => item.id === row.id); |
| | | if (idx > -1) { |
| | | removeItem(idx) |
| | | } |
| | | } |
| | | } |
| | | ] |
| | | } |
| | | ]); |
| | | |
| | | const removeItem = (index) => { |
| | | routeItems.value.splice(index, 1); |
| | | nextTick(() => initSortable()); |
| | | }; |
| | | |
| | | const removeItemByID = (id) => { |
| | | const idx = routeItems.value.findIndex(item => item.id === id); |
| | | if (idx > -1) { |
| | | routeItems.value.splice(idx, 1); |
| | | nextTick(() => initSortable()); |
| | | } |
| | | }; |
| | | |
| | | const closeModal = () => { |
| | | isShow.value = false; |
| | | }; |
| | | |
| | | const handelSelectProducts = (products) => { |
| | | destroySortable(); |
| | | |
| | | const newData = products.map(({ id, ...product }) => ({ |
| | | ...product, |
| | | productModelId: id, |
| | | routeId: props.record.id, |
| | | id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, |
| | | processId: undefined |
| | | })); |
| | | |
| | | console.log('éæ©äº§ååæ°ç»:', routeItems.value); |
| | | routeItems.value.push(...newData); |
| | | routeItems.value = [...routeItems.value]; |
| | | console.log('éæ©äº§ååæ°ç»:', routeItems.value); |
| | | |
| | | // å»¶è¿åå§åï¼ç¡®ä¿DOMå®å
¨æ¸²æ |
| | | nextTick(() => { |
| | | // 强å¶éæ°æ¸²æç»ä»¶ |
| | | if (proxy?.$forceUpdate) { |
| | | proxy.$forceUpdate(); |
| | | } |
| | | |
| | | const temp = [...routeItems.value]; |
| | | routeItems.value = []; |
| | | nextTick(() => { |
| | | routeItems.value = temp; |
| | | initSortable(); |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const findProcessRouteItems = () => { |
| | | tableLoading.value = true; |
| | | findProcessRouteItemList({ routeId: props.record.id }) |
| | | .then(res => { |
| | | tableLoading.value = false; |
| | | routeItems.value = res.data.map(item => ({ |
| | | ...item, |
| | | processId: item.processId === 0 ? undefined : item.processId |
| | | })); |
| | | // å»¶è¿åå§åï¼ç¡®ä¿DOMå®å
¨æ¸²æ |
| | | nextTick(() => { |
| | | setTimeout(() => initSortable(), 100); |
| | | }); |
| | | }) |
| | | .catch(err => { |
| | | tableLoading.value = false; |
| | | console.error("è·åå表失败ï¼", err); |
| | | }); |
| | | }; |
| | | |
| | | const findProcessList = () => { |
| | | processList({}) |
| | | .then(res => { |
| | | processOptions.value = res.data; |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åå·¥åºå¤±è´¥ï¼", err); |
| | | }); |
| | | }; |
| | | |
| | | const { proxy } = getCurrentInstance() || {}; |
| | | |
| | | const handleSubmit = () => { |
| | | const hasEmptyProcess = routeItems.value.some(item => !item.processId); |
| | | if (hasEmptyProcess) { |
| | | proxy?.$modal?.msgError("请为ææé¡¹ç®éæ©å·¥åº"); |
| | | return; |
| | | } |
| | | |
| | | addOrUpdateProcessRouteItem({ |
| | | routeId: props.record.id, |
| | | processRouteItem: routeItems.value.map(({ id, ...item }) => item) |
| | | }) |
| | | .then(res => { |
| | | isShow.value = false; |
| | | emit('completed'); |
| | | proxy?.$modal?.msgSuccess("æäº¤æå"); |
| | | }) |
| | | .catch(err => { |
| | | proxy?.$modal?.msgError(`æäº¤å¤±è´¥ï¼${err.msg || "ç½ç»å¼å¸¸"}`); |
| | | }); |
| | | }; |
| | | |
| | | const destroySortable = () => { |
| | | if (tableSortable) { |
| | | tableSortable.destroy(); |
| | | tableSortable = null; |
| | | } |
| | | if (stepsSortable) { |
| | | stepsSortable.destroy(); |
| | | stepsSortable = null; |
| | | } |
| | | }; |
| | | |
| | | const initSortable = () => { |
| | | destroySortable(); |
| | | |
| | | if (isTable.value) { |
| | | if (!multipleTable.value) return; |
| | | const tbody = multipleTable.value.$el.querySelector('.el-table__body tbody') || |
| | | multipleTable.value.$el.querySelector('.el-table__body-wrapper > table > tbody'); |
| | | if (!tbody) return; |
| | | |
| | | tableSortable = new Sortable(tbody, { |
| | | animation: 150, |
| | | ghostClass: 'sortable-ghost', |
| | | handle: '.el-table__row', |
| | | filter: '.el-button, .el-select', |
| | | onEnd: (evt) => { |
| | | if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return; |
| | | |
| | | // ä½¿ç¨æ°ç» splice æ¹æ³éæ°æåºï¼ä¸è¡¨æ ¼æ¨¡å¼ä¿æä¸è´ |
| | | const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0]; |
| | | routeItems.value.splice(evt.newIndex, 0, moveItem); |
| | | routeItems.value = [...routeItems.value]; |
| | | console.log('æåºåæ°ç»:', routeItems.value); |
| | | } |
| | | }); |
| | | } else { |
| | | if (!stepsContainer.value) return; |
| | | |
| | | // ä¿®æ¹ï¼ç´æ¥ä½¿ç¨stepsContainer.valueä½ä¸ºææ½å®¹å¨ |
| | | const stepsList = stepsContainer.value; |
| | | if (!stepsList) { |
| | | console.warn('æªæ¾å°æ¥éª¤æ¡ææ½å®¹å¨'); |
| | | return; |
| | | } |
| | | |
| | | // ä¿®æ¹ï¼ç®åææ½é
ç½® |
| | | stepsSortable = new Sortable(stepsList, { |
| | | animation: 150, |
| | | ghostClass: 'sortable-ghost', |
| | | draggable: '.draggable-step', // 坿æ½å
ç´ |
| | | handle: '.draggable-step, .step-card', // ææ½ææ |
| | | filter: '.el-button, .el-select, .el-input', // è¿æ»¤æé®/éæ©å¨ |
| | | forceFallback: true, |
| | | fallbackClass: 'sortable-fallback', |
| | | preventOnFilter: true, |
| | | scroll: true, |
| | | scrollSensitivity: 30, |
| | | scrollSpeed: 10, |
| | | bubbleScroll: true, |
| | | onEnd: (evt) => { |
| | | if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return; |
| | | |
| | | // ä½¿ç¨æ°ç» splice æ¹æ³éæ°æåº |
| | | const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0]; |
| | | routeItems.value.splice(evt.newIndex, 0, moveItem); |
| | | routeItems.value = [...routeItems.value]; |
| | | } |
| | | }); |
| | | |
| | | // è°è¯ï¼æå°å®¹å¨åå®ä¾ï¼ç¡®è®¤ç»å®æå |
| | | console.log('æ¥éª¤æ¡ææ½å®¹å¨:', stepsList); |
| | | console.log('Sortableå®ä¾:', stepsSortable); |
| | | } |
| | | }; |
| | | |
| | | const handleViewChange = () => { |
| | | destroySortable(); |
| | | // å»¶è¿åå§åï¼ç¡®ä¿è§å¾åæ¢åDOMå®å
¨æ¸²æ |
| | | nextTick(() => { |
| | | setTimeout(() => initSortable(), 100); |
| | | }); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | findProcessRouteItems(); |
| | | findProcessList(); |
| | | }); |
| | | |
| | | onUnmounted(() => { |
| | | destroySortable(); |
| | | }); |
| | | |
| | | defineExpose({ |
| | | closeModal, |
| | | handleSubmit, |
| | | isShow |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | :deep(.sortable-ghost) { |
| | | opacity: 0.6; |
| | | background-color: #f5f7fa !important; |
| | | } |
| | | |
| | | :deep(.el-table__row) { |
| | | transition: background-color 0.2s; |
| | | } |
| | | |
| | | :deep(.el-table__row:hover) { |
| | | background-color: #f9fafc !important; |
| | | } |
| | | |
| | | :deep(.el-card__footer){ |
| | | padding: 0 !important; |
| | | } |
| | | |
| | | .operate-button { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | /* ä¿®æ¹ï¼èªå®ä¹æ¥éª¤æ¡å®¹å¨æ ·å¼ */ |
| | | .custom-steps { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: flex-start; |
| | | gap: 20px; |
| | | min-height: 100px; |
| | | } |
| | | |
| | | /* ä¿®æ¹ï¼èªå®ä¹æ¥éª¤é¡¹æ ·å¼ */ |
| | | .custom-step { |
| | | cursor: move !important; |
| | | padding: 8px; |
| | | position: relative; |
| | | transition: all 0.2s ease; |
| | | flex: 0 0 auto; |
| | | min-width: 220px; |
| | | touch-action: none; |
| | | } |
| | | |
| | | /* ææ½æ¬æµ®æ ·å¼ï¼æç¤ºå¯ææ½ */ |
| | | .custom-step:hover { |
| | | background-color: rgba(64, 158, 255, 0.05); |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | .sortable-ghost { |
| | | opacity: 0.4; |
| | | background-color: #f5f7fa !important; |
| | | border: 2px dashed #409eff; |
| | | margin: 10px; |
| | | transform: scale(1.02); |
| | | } |
| | | |
| | | .sortable-fallback { |
| | | opacity: 0.9; |
| | | background-color: #f5f7fa; |
| | | border: 1px solid #409eff; |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
| | | transform: rotate(2deg); |
| | | margin: 10px; |
| | | } |
| | | |
| | | .step-card { |
| | | cursor: move !important; |
| | | transition: box-shadow 0.2s ease; |
| | | user-select: none; |
| | | -webkit-user-select: none; |
| | | pointer-events: auto; |
| | | margin: 10px; |
| | | height: 240px; |
| | | } |
| | | |
| | | .step-card:hover { |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .step-content { |
| | | width: 220px; |
| | | user-select: none; |
| | | } |
| | | |
| | | .step-card-content { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | } |
| | | |
| | | .step-card-footer { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | align-items: center; |
| | | padding: 10px; |
| | | } |
| | | |
| | | /* èªå®ä¹åºå·æ ·å¼ä¼å */ |
| | | .step-number { |
| | | font-weight: bold; |
| | | text-align: center; |
| | | width: 36px; |
| | | height: 36px; |
| | | line-height: 36px; |
| | | margin: 0 auto 10px; |
| | | background: #409eff; |
| | | color: #fff; |
| | | border-radius: 50%; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="isShow" |
| | | title="æ°å¢å·¥èºè·¯çº¿" |
| | | width="400" |
| | | @close="closeModal" |
| | | > |
| | | <el-form label-width="140px" :model="formState" label-position="top" ref="formRef"> |
| | | <el-form-item |
| | | label="产ååç§°" |
| | | prop="productModelId" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©äº§å', |
| | | trigger: 'change', |
| | | } |
| | | ]" |
| | | > |
| | | <el-button type="primary" @click="showProductSelectDialog = true"> |
| | | {{ formState.productName && formState.productModelName |
| | | ? `${formState.productName} - ${formState.productModelName}` |
| | | : 'éæ©äº§å' }} |
| | | </el-button> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="BOM" |
| | | prop="bomId" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©BOM', |
| | | trigger: 'change', |
| | | } |
| | | ]" |
| | | > |
| | | <el-select |
| | | v-model="formState.bomId" |
| | | placeholder="è¯·éæ©BOM" |
| | | clearable |
| | | :disabled="!formState.productModelId || bomOptions.length === 0" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in bomOptions" |
| | | :key="item.id" |
| | | :label="item.bomNo || `BOM-${item.id}`" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="夿³¨" prop="description"> |
| | | <el-input v-model="formState.description" type="textarea" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <!-- 产åéæ©å¼¹çª --> |
| | | <ProductSelectDialog |
| | | v-model="showProductSelectDialog" |
| | | @confirm="handleProductSelect" |
| | | single |
| | | /> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="handleSubmit">确认</el-button> |
| | | <el-button @click="closeModal">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {ref, computed, getCurrentInstance} from "vue"; |
| | | import {add} from "@/api/productionManagement/processRoute.js"; |
| | | import {getByModel} from "@/api/productionManagement/productBom.js"; |
| | | import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; |
| | | |
| | | const props = defineProps({ |
| | | visible: { |
| | | type: Boolean, |
| | | required: true, |
| | | }, |
| | | }); |
| | | |
| | | const emit = defineEmits(['update:visible', 'completed']); |
| | | |
| | | // ååºå¼æ°æ®ï¼æ¿ä»£é项å¼ç dataï¼ |
| | | const formState = ref({ |
| | | productId: undefined, |
| | | productModelId: undefined, |
| | | productName: "", |
| | | productModelName: "", |
| | | bomId: undefined, |
| | | description: '', |
| | | }); |
| | | |
| | | const isShow = computed({ |
| | | get() { |
| | | return props.visible; |
| | | }, |
| | | set(val) { |
| | | emit('update:visible', val); |
| | | }, |
| | | }); |
| | | |
| | | const showProductSelectDialog = ref(false); |
| | | const bomOptions = ref([]); |
| | | |
| | | let { proxy } = getCurrentInstance() |
| | | |
| | | const closeModal = () => { |
| | | // éç½®è¡¨åæ°æ® |
| | | formState.value = { |
| | | productId: undefined, |
| | | productModelId: undefined, |
| | | productName: "", |
| | | productModelName: "", |
| | | bomId: undefined, |
| | | description: '', |
| | | }; |
| | | bomOptions.value = []; |
| | | isShow.value = false; |
| | | }; |
| | | |
| | | // 产åéæ©å¤ç |
| | | const handleProductSelect = async (products) => { |
| | | if (products && products.length > 0) { |
| | | const product = products[0]; |
| | | // å
æ¥è¯¢BOMå表ï¼å¿
éï¼ |
| | | try { |
| | | const res = await getByModel(product.id); |
| | | // å¤çè¿åçBOMæ°æ®ï¼å¯è½æ¯æ°ç»ã对象æå
å«dataåæ®µ |
| | | let bomList = []; |
| | | if (Array.isArray(res)) { |
| | | bomList = res; |
| | | } else if (res && res.data) { |
| | | bomList = Array.isArray(res.data) ? res.data : [res.data]; |
| | | } else if (res && typeof res === 'object') { |
| | | bomList = [res]; |
| | | } |
| | | |
| | | if (bomList.length > 0) { |
| | | formState.value.productModelId = product.id; |
| | | formState.value.productName = product.productName; |
| | | formState.value.productModelName = product.model; |
| | | formState.value.bomId = undefined; // éç½®BOMéæ© |
| | | bomOptions.value = bomList; |
| | | showProductSelectDialog.value = false; |
| | | // 触å表åéªè¯æ´æ° |
| | | proxy.$refs["formRef"]?.validateField('productModelId'); |
| | | } else { |
| | | proxy.$modal.msgError("è¯¥äº§åæ²¡æBOMï¼è¯·å
å建BOM"); |
| | | } |
| | | } catch (error) { |
| | | // 妿æ¥å£è¿å404æå
¶ä»é误ï¼è¯´ææ²¡æBOM |
| | | proxy.$modal.msgError("è¯¥äº§åæ²¡æBOMï¼è¯·å
å建BOM"); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const handleSubmit = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | // éªè¯æ¯å¦éæ©äºäº§ååBOM |
| | | if (!formState.value.productModelId) { |
| | | proxy.$modal.msgError("è¯·éæ©äº§å"); |
| | | return; |
| | | } |
| | | if (!formState.value.bomId) { |
| | | proxy.$modal.msgError("è¯·éæ©BOM"); |
| | | return; |
| | | } |
| | | add(formState.value).then(res => { |
| | | // å
³éæ¨¡ææ¡ |
| | | isShow.value = false; |
| | | // åç¥ç¶ç»ä»¶å·²å®æ |
| | | emit('completed'); |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | }) |
| | | } |
| | | }) |
| | | }; |
| | | |
| | | |
| | | defineExpose({ |
| | | closeModal, |
| | | handleSubmit, |
| | | isShow, |
| | | }); |
| | | </script> |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="route-header"> |
| | | <div class="add-route-btn" |
| | | @click="handleAddRoute"> |
| | | <el-icon> |
| | | <Plus /> |
| | | </el-icon> |
| | | <span>æ°å¢å·¥èºè·¯çº¿</span> |
| | | </div> |
| | | <div class="search_form"> |
| | | <el-form :model="searchForm" |
| | | :inline="true"> |
| | | <el-form-item label="è§æ ¼åç§°:"> |
| | | <el-input v-model="searchForm.model" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | prefix-icon="Search" |
| | | style="width: 200px;" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" |
| | | @click="handleQuery">æç´¢</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | <div class="route-card-list"> |
| | | <div v-for="route in routeList" |
| | | :key="route.id" |
| | | class="route-card"> |
| | | <div class="card-header"> |
| | | <div class="route-info"> |
| | | <span class="route-name"><el-icon style="margin-right: 8px;line-height: 30px;"> |
| | | <ScaleToOriginal /> |
| | | </el-icon>{{route.routeCode }}<el-tag style="margin-left: 8px" |
| | | :type="!route.status ? 'warning' : 'success'">{{ !route.status ? 'è稿' : 'æ¹å' }}</el-tag></span> |
| | | <!-- <span class="route-code">{{ route.routeCode }}</span> --> |
| | | </div> |
| | | <div class="route-actions"> |
| | | <el-button v-if="!route.status" |
| | | link |
| | | type="success" |
| | | @click="handleApproveRoute(route)"> |
| | | <el-icon> |
| | | <Check /> |
| | | </el-icon> |
| | | æ¹å |
| | | </el-button> |
| | | <el-button v-if="route.status" |
| | | link |
| | | type="warning" |
| | | @click="handleRevokeApproveRoute(route)"> |
| | | <el-icon> |
| | | <Close /> |
| | | </el-icon> |
| | | æ¤éæ¹å |
| | | </el-button> |
| | | <el-button link |
| | | type="primary" |
| | | @click="handleEditRoute(route)"> |
| | | <el-icon> |
| | | <Edit /> |
| | | </el-icon> |
| | | ç¼è¾ |
| | | </el-button> |
| | | <el-button link |
| | | type="danger" |
| | | @click="handleDeleteRoute(route)"> |
| | | <el-icon> |
| | | <Delete /> |
| | | </el-icon> |
| | | å é¤ |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <div class="card-body"> |
| | | <div class="route-meta"> |
| | | <span class="meta-item"> |
| | | <el-icon> |
| | | <Box /> |
| | | </el-icon> |
| | | <span class="meta-label">产å:</span> |
| | | <span class="meta-value">{{ route.productName }} - {{ route.productModelName }}</span> |
| | | </span> |
| | | <span class="meta-item"> |
| | | <el-icon> |
| | | <Document /> |
| | | </el-icon> |
| | | <span class="meta-label">BOM:</span> |
| | | <span class="meta-value">{{ route.bomNo || '-' }}</span> |
| | | </span> |
| | | <span class="meta-item"> |
| | | <el-icon> |
| | | <Document /> |
| | | </el-icon> |
| | | <span class="meta-label">夿³¨:</span> |
| | | <span class="meta-value">{{ route.description || 'ææ æè¿°' }}</span> |
| | | </span> |
| | | </div> |
| | | <div class="expand-btn-wrapper"> |
| | | <el-button class="expand-btn" |
| | | :class="{ expanded: route.expanded }" |
| | | type="primary" |
| | | text |
| | | @click="toggleExpand(route)"> |
| | | <span class="btn-text">{{ route.expanded ? 'æ¶èµ·å·¥åºè·¯çº¿' : 'å±å¼å·¥åºè·¯çº¿' }}</span> |
| | | <el-icon class="expand-icon"> |
| | | <component :is="route.expanded ? 'ArrowUp' : 'ArrowDown'" /> |
| | | </el-icon> |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <div v-if="route.expanded" |
| | | class="process-route"> |
| | | <div class="process-flow"> |
| | | <div v-for="(process, index) in route.processList" |
| | | :key="process.id" |
| | | class="process-flow-item" |
| | | draggable="true" |
| | | @dragstart="handleDragStart($event, index, route.id)" |
| | | @dragover="handleDragOver($event)" |
| | | @drop="handleDrop($event, index, route.id)" |
| | | @dragend="handleDragEnd"> |
| | | <div class="process-node" |
| | | :class="{ expanded: process.expanded }"> |
| | | <div class="process-node-header"> |
| | | <div class="process-number">{{ index + 1 }}</div> |
| | | <div class="process-actions"> |
| | | <el-button link |
| | | type="primary" |
| | | @click="handleEditProcessSelect(route, index, process)"> |
| | | <el-icon> |
| | | <Edit /> |
| | | </el-icon> |
| | | </el-button> |
| | | <el-button link |
| | | type="danger" |
| | | @click="handleDeleteProcess(route.id, process)"> |
| | | <el-icon> |
| | | <Delete /> |
| | | </el-icon> |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <div class="process-node-body"> |
| | | <!-- <div class="process-code">{{ process.processId }}</div> --> |
| | | <div class="process-name">{{ process.processName }}</div> |
| | | <!-- <div class="process-desc">{{ process.remark || 'ææ æè¿°' }}</div> --> |
| | | </div> |
| | | <div class="process-node-footer"> |
| | | <!-- <el-tag size="small" |
| | | :type="process.status === '1' ? 'success' : 'info'"> |
| | | {{ process.status === '1' ? 'å¯ç¨' : 'åç¨' }} |
| | | </el-tag> --> |
| | | <el-button type="primary" |
| | | link |
| | | size="small" |
| | | @click="toggleProcessParams(process)"> |
| | | {{ process.expanded ? 'æ¶èµ·åæ°' : 'å±å¼åæ°' }} |
| | | ({{ process.paramCount }}) |
| | | </el-button> |
| | | </div> |
| | | <div v-if="process.expanded" |
| | | class="process-params-section"> |
| | | <div class="params-header"> |
| | | <span>åæ°å表</span> |
| | | <el-button type="primary" |
| | | link |
| | | size="small" |
| | | @click="handleAddParam(route.id, process)"> |
| | | <el-icon> |
| | | <Plus /> |
| | | </el-icon>æ°å¢ |
| | | </el-button> |
| | | </div> |
| | | <div class="params-list"> |
| | | <div v-for="param in process.paramList" |
| | | :key="param.id" |
| | | class="param-item"> |
| | | <div class="param-info"> |
| | | <span class="param-code">{{ param.paramName }}</span> |
| | | <!-- <span class="param-name">{{ param.paramName }}</span> --> |
| | | <!-- <el-tag size="small" |
| | | style="margin-right: 20px;" |
| | | :type="getParamTypeTag(param.parameterType)"> |
| | | {{ param.parameterType }} |
| | | </el-tag> --> |
| | | <span v-if="param.valueMode==1" |
| | | class="param-value">æ åå¼ï¼{{ param.standardValue || "-" }} {{ param.unit }}</span> |
| | | <span v-else |
| | | class="param-value">æ åå¼ï¼{{ param.minValue || "-" }}-{{ param.maxValue || "-" }} {{ param.unit }}</span> |
| | | </div> |
| | | <div class="param-actions"> |
| | | <el-button link |
| | | type="primary" |
| | | size="small" |
| | | @click="handleEditParam(route.id, process, param)"> |
| | | ç¼è¾ |
| | | </el-button> |
| | | <el-button link |
| | | type="danger" |
| | | size="small" |
| | | @click="handleDeleteParam(route.id, process, param)"> |
| | | å é¤ |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <el-empty v-if="!process.paramList || process.paramList.length === 0" |
| | | description="ææ åæ°" |
| | | :image-size="50" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div v-if="index < route.processList.length - 1" |
| | | class="flow-arrow"> |
| | | <el-icon> |
| | | <Right /> |
| | | </el-icon> |
| | | </div> |
| | | </div> |
| | | <div class="add-process-node" |
| | | @click="handleSelectProcess(route, index)"> |
| | | <el-icon> |
| | | <Plus /> |
| | | </el-icon> |
| | | <span>æ°å¢å·¥åº</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <div style="text-align: right" |
| | | class="mb10"> |
| | | <el-button type="primary" |
| | | @click="showNewModal">æ°å¢å·¥èºè·¯çº¿</el-button> |
| | | <el-button type="danger" |
| | | @click="handleDelete" |
| | | :disabled="selectedRows.length === 0" |
| | | plain>å é¤å·¥èºè·¯çº¿</el-button> |
| | | </div> |
| | | <PIMTable rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | :total="page.total" /> |
| | | </div> |
| | | <!-- å页æ§ä»¶ --> |
| | | <div class="pagination-container"> |
| | | <el-pagination v-model:current-page="routePage.current" |
| | | v-model:page-size="routePage.size" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :total="routePage.total" |
| | | @size-change="handleRouteSizeChange" |
| | | @current-change="handleRouteCurrentChange" /> |
| | | </div> |
| | | <!-- å·¥èºè·¯çº¿æ°å¢/ç¼è¾å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="routeDialogVisible" |
| | | :title="isRouteEdit ? 'ç¼è¾å·¥èºè·¯çº¿' : 'æ°å¢å·¥èºè·¯çº¿'" |
| | | width="500px"> |
| | | <el-form :model="routeForm" |
| | | :rules="routeRules" |
| | | ref="routeFormRef" |
| | | label-width="120px"> |
| | | <el-form-item label="产ååç§°" |
| | | prop="productModelId"> |
| | | <el-button type="primary" |
| | | @click="handleProcessProductSelectClick2"> |
| | | {{ routeForm.productName && routeForm.productModelName |
| | | ? `${routeForm.productName} - ${routeForm.productModelName}` |
| | | : 'éæ©äº§å' }} |
| | | </el-button> |
| | | </el-form-item> |
| | | <el-form-item label="BOM" |
| | | prop="bomId"> |
| | | <el-select v-model="routeForm.bomId" |
| | | placeholder="è¯·éæ©BOM" |
| | | clearable |
| | | :disabled="!routeForm.productModelId || bomOptions.length === 0" |
| | | style="width: 100%"> |
| | | <el-option v-for="item in bomOptions" |
| | | :key="item.id" |
| | | :label="item.bomNo || `BOM-${item.id}`" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="路线ç¼ç " |
| | | prop="routeCode"> |
| | | <el-input v-model="routeForm.routeCode" |
| | | disabled |
| | | placeholder="èªå¨çæ" /> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨" |
| | | prop="description"> |
| | | <el-input v-model="routeForm.description" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="请è¾å
¥è·¯çº¿æè¿°" /> |
| | | </el-form-item> |
| | | <!-- <el-form-item label="ç¶æ" |
| | | prop="status"> |
| | | <el-radio-group v-model="routeForm.status"> |
| | | <el-radio label="1">å¯ç¨</el-radio> |
| | | <el-radio label="0">åç¨</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> --> |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="routeDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="handleRouteSubmit">ç¡®å®</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 产åéæ©å¼¹çª --> |
| | | <ProductSelectDialog v-model="showProductSelectDialog" |
| | | @confirm="handleProductSelect" |
| | | single /> |
| | | <!-- å·¥åºæ°å¢/ç¼è¾å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="processDialogVisible" |
| | | :title="isProcessEdit ? 'ç¼è¾å·¥åº' : 'æ°å¢å·¥åº'" |
| | | width="500px"> |
| | | <el-form :model="processForm" |
| | | :rules="processRules" |
| | | ref="processFormRef" |
| | | label-width="120px"> |
| | | <el-form-item label="å·¥åºç¼ç " |
| | | prop="no"> |
| | | <el-input v-model="processForm.no" |
| | | placeholder="请è¾å
¥å·¥åºç¼ç " /> |
| | | </el-form-item> |
| | | <el-form-item label="å·¥åºåç§°" |
| | | prop="name"> |
| | | <el-input v-model="processForm.name" |
| | | placeholder="请è¾å
¥å·¥åºåç§°" /> |
| | | </el-form-item> |
| | | <el-form-item label="å·¥åºæè¿°" |
| | | prop="remark"> |
| | | <el-input v-model="processForm.remark" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="请è¾å
¥å·¥åºæè¿°" /> |
| | | </el-form-item> |
| | | <el-form-item label="ç¶æ" |
| | | prop="status"> |
| | | <el-radio-group v-model="processForm.status"> |
| | | <el-radio :label="true">å¯ç¨</el-radio> |
| | | <el-radio :label="false">åç¨</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="processDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="handleProcessSubmit">ç¡®å®</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 鿩工åºå¯¹è¯æ¡ --> |
| | | <el-dialog v-model="selectProcessDialogVisible" |
| | | title="鿩工åº" |
| | | width="1000px"> |
| | | <div class="process-select-container"> |
| | | <!-- 左侧工åºå表 --> |
| | | <div class="process-list-area"> |
| | | <div class="area-title">å¯éå·¥åº</div> |
| | | <div class="search-box"> |
| | | <el-input v-model="processSearchKeyword" |
| | | placeholder="请è¾å
¥å·¥åºåç§°æç´¢" |
| | | clearable |
| | | size="small" |
| | | @input="handleProcessSearch"> |
| | | <template #prefix> |
| | | <el-icon> |
| | | <Search /> |
| | | </el-icon> |
| | | </template> |
| | | </el-input> |
| | | </div> |
| | | <el-table :data="filteredProcessList" |
| | | height="360" |
| | | border |
| | | highlight-current-row |
| | | @current-change="handleProcessSelect"> |
| | | <el-table-column prop="no" |
| | | label="å·¥åºç¼å·" |
| | | width="100" /> |
| | | <el-table-column prop="name" |
| | | label="å·¥åºåç§°" /> |
| | | <el-table-column prop="remark" |
| | | label="å·¥åºæè¿°" /> |
| | | <el-table-column prop="status" |
| | | label="ç¶æ" |
| | | width="80"> |
| | | <template #default="scope"> |
| | | <el-tag size="small" |
| | | :type="scope.row.status ? 'success' : 'info'"> |
| | | {{ scope.row.status ? 'å¯ç¨' : 'åç¨' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | <!-- å³ä¾§å·¥åºè¯¦æ
--> |
| | | <div class="process-detail-area"> |
| | | <div class="area-title">å·¥åºè¯¦æ
</div> |
| | | <el-form v-if="selectedProcessItem" |
| | | :model="processForm" |
| | | label-width="100px" |
| | | class="process-detail-form"> |
| | | <el-form-item label="å·¥åºç¼å·"> |
| | | <span class="detail-text">{{ selectedProcessItem.no }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="å·¥åºåç§°"> |
| | | <span class="detail-text">{{ selectedProcessItem.name }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="å·¥åºæè¿°"> |
| | | <span class="detail-text">{{ selectedProcessItem.remark || '-' }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="ç¶æ"> |
| | | <el-tag size="small" |
| | | :type="selectedProcessItem.status ? 'success' : 'info'"> |
| | | {{ selectedProcessItem.status ? 'å¯ç¨' : 'åç¨' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="æ¯å¦è´¨æ£"> |
| | | <el-tag size="small" |
| | | :type="selectedProcessItem.isQuality ? 'success' : 'info'"> |
| | | {{ selectedProcessItem.isQuality ? 'è´¨æ£' : 'éè´¨æ£' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="产ååç§°" |
| | | prop="productModelId"> |
| | | <el-button type="primary" |
| | | @click="handleProcessProductSelectClick"> |
| | | {{ processForm.productName && processForm.model |
| | | ? `${processForm.productName} - ${processForm.model}` |
| | | : 'éæ©äº§å' }} |
| | | </el-button> |
| | | </el-form-item> |
| | | <el-form-item label="åä½" |
| | | prop="unit"> |
| | | <el-input v-model="processForm.unit" |
| | | :placeholder="processForm.productModelId ? 'æ ¹æ®éæ©ç产åèªå¨å¸¦åº' : '请å
éæ©äº§å' " |
| | | clearable |
| | | :disabled="true" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¯å¦è´¨æ£" |
| | | prop="isQuality"> |
| | | <el-switch v-model="processForm.isQuality" |
| | | :active-value="true" |
| | | inactive-value="false" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <el-empty v-else |
| | | description="请ä»å·¦ä¾§éæ©å·¥åº" /> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="selectProcessDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | :disabled="!selectedProcessItem || !processForm.productModelId" |
| | | @click="handleProcessSelectSubmit">ç¡®å®</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- åæ°æ°å¢/ç¼è¾å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="paramDialogVisible" |
| | | :title="isParamEdit ? 'ç¼è¾åæ°' : 'æ°å¢åæ°'" |
| | | width="500px"> |
| | | <el-form :model="paramForm" |
| | | :rules="paramRules" |
| | | ref="paramFormRef" |
| | | label-width="120px"> |
| | | <el-form-item label="åæ°ç¼å·" |
| | | prop="parameterCode"> |
| | | <el-input v-model="paramForm.parameterCode" |
| | | placeholder="请è¾å
¥åæ°ç¼å·" /> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°åç§°" |
| | | prop="parameterName"> |
| | | <el-input v-model="paramForm.parameterName" |
| | | placeholder="请è¾å
¥åæ°åç§°" /> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°æ¨¡å¼" |
| | | prop="parameterType2"> |
| | | <el-select v-model="paramForm.parameterType2" |
| | | placeholder="è¯·éæ©åæ°æ¨¡å¼"> |
| | | <el-option label="åå¼" |
| | | value="1" /> |
| | | <el-option label="åºé´" |
| | | value="2" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°ç±»å" |
| | | prop="parameterType"> |
| | | <el-select v-model="paramForm.parameterType" |
| | | @change="handleParamTypeChange" |
| | | placeholder="è¯·éæ©åæ°ç±»å"> |
| | | <el-option label="æ°å¼æ ¼å¼" |
| | | value="æ°å¼æ ¼å¼" /> |
| | | <el-option label="ææ¬æ ¼å¼" |
| | | value="ææ¬æ ¼å¼" /> |
| | | <el-option label="䏿é项" |
| | | value="䏿é项" /> |
| | | <el-option label="æ¶é´æ ¼å¼" |
| | | value="æ¶é´æ ¼å¼" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item v-if="paramForm.parameterType === '䏿é项'" |
| | | label="æ°æ®åå
¸" |
| | | prop="parameterFormat"> |
| | | <el-select v-model="paramForm.parameterFormat" |
| | | placeholder="è¯·éæ©æ°æ®åå
¸"> |
| | | <el-option v-for="item in dictTypes" |
| | | :key="item.dictType" |
| | | :label="item.dictName" |
| | | :value="item.dictType" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item v-else-if="paramForm.parameterType === 'æ¶é´æ ¼å¼'" |
| | | label="æ¶é´æ ¼å¼" |
| | | prop="parameterFormat"> |
| | | <el-select v-model="paramForm.parameterFormat" |
| | | placeholder="è¯·éæ©æ¶é´æ ¼å¼"> |
| | | <el-option label="YYYY-MM-DD HH:mm:ss" |
| | | value="YYYY-MM-DD HH:mm:ss" /> |
| | | <el-option label="YYYY-MM-DD" |
| | | value="YYYY-MM-DD" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item v-else |
| | | label="åæ°æ ¼å¼" |
| | | prop="parameterFormat"> |
| | | <el-input v-model="paramForm.parameterFormat" |
| | | placeholder="请è¾å
¥åæ°æ ¼å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ åå¼" |
| | | prop="standardValue"> |
| | | <el-input v-model="paramForm.standardValue" |
| | | placeholder="请è¾å
¥æ åå¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="åä½" |
| | | prop="unit"> |
| | | <el-input v-model="paramForm.unit" |
| | | placeholder="请è¾å
¥åä½" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="paramDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="handleParamSubmit">ç¡®å®</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- éæ©åæ°å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="selectParamDialogVisible" |
| | | title="鿩忰" |
| | | width="1000px"> |
| | | <div class="param-select-container"> |
| | | <!-- 左侧忰å表 --> |
| | | <div class="param-list-area"> |
| | | <div class="area-title">å¯éåæ°</div> |
| | | <div class="search-box"> |
| | | <el-input v-model="paramSearchKeyword" |
| | | placeholder="请è¾å
¥åæ°åç§°æç´¢" |
| | | clearable |
| | | size="small" |
| | | @input="handleParamSearch"> |
| | | <template #prefix> |
| | | <el-icon> |
| | | <Search /> |
| | | </el-icon> |
| | | </template> |
| | | </el-input> |
| | | </div> |
| | | <el-table :data="filteredParamList" |
| | | height="300" |
| | | border |
| | | highlight-current-row |
| | | @current-change="handleParamSelect"> |
| | | <el-table-column prop="paramName" |
| | | label="åæ°åç§°" /> |
| | | <el-table-column prop="paramType" |
| | | label="åæ°ç±»å"> |
| | | <template #default="scope"> |
| | | <el-tag size="small" |
| | | :type="getParamTypeTag(scope.row.paramType)"> |
| | | {{ getParamTypeText(scope.row.paramType) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <!-- å页æ§ä»¶ --> |
| | | <div class="pagination-container" |
| | | style="margin-top: 10px;"> |
| | | <el-pagination v-model:current-page="paramPage.current" |
| | | v-model:page-size="paramPage.size" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :total="paramPage.total" |
| | | @size-change="handleParamSizeChange" |
| | | @current-change="handleParamCurrentChange" |
| | | size="small" /> |
| | | </div> |
| | | </div> |
| | | <!-- å³ä¾§åæ°è¯¦æ
--> |
| | | <div class="param-detail-area"> |
| | | <div class="area-title">åæ°è¯¦æ
</div> |
| | | <el-form v-if="selectedParam" |
| | | :model="selectedParam" |
| | | label-width="100px" |
| | | class="param-detail-form"> |
| | | <el-form-item label="åæ°åç§°"> |
| | | <span class="detail-text">{{ selectedParam.paramName }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°æ¨¡å¼"> |
| | | <el-tag size="small" |
| | | :type="selectedParam.valueMode == '1' ? 'success' : 'warning'"> |
| | | {{ selectedParam.valueMode == '1' ? 'åå¼' : 'åºé´' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°ç±»å"> |
| | | <el-tag size="small" |
| | | :type="getParamTypeTag(selectedParam.paramType)"> |
| | | {{ getParamTypeText(selectedParam.paramType) }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°æ ¼å¼"> |
| | | <span class="detail-text">{{ selectedParam.paramFormat || '-' }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="åä½"> |
| | | <span class="detail-text">{{ selectedParam.unit || '-' }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="æ åå¼" |
| | | v-if="selectedParam.valueMode == '1' && selectedParam.paramType == '1'"> |
| | | <el-input v-model="selectedParam.standardValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥é»è®¤å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æå°å¼" |
| | | v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'"> |
| | | <el-input v-model="selectedParam.minValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æå°å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æå¤§å¼" |
| | | v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'"> |
| | | <el-input v-model="selectedParam.maxValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æå¤§å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æåº"> |
| | | <el-input v-model="selectedParam.sort" |
| | | type="number" |
| | | placeholder="请è¾å
¥æåº" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¯å¦å¿
å¡«"> |
| | | <el-switch v-model="selectedParam.isRequired" |
| | | :active-value="1" |
| | | :inactive-value="0" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <el-empty v-else |
| | | description="请ä»å·¦ä¾§éæ©åæ°" /> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="selectParamDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | :disabled="!selectedParam" |
| | | @click="handleParamSelectSubmit">ç¡®å®</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- ç¼è¾åæ°å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="editParamDialogVisible" |
| | | title="ç¼è¾åæ°" |
| | | width="600px"> |
| | | <el-form :model="editParamForm" |
| | | :rules="editParamRules" |
| | | ref="editParamFormRef" |
| | | label-width="120px"> |
| | | <el-form-item label="åæ°åç§°"> |
| | | <span class="detail-text">{{ editParamForm.paramName }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°æ¨¡å¼"> |
| | | <el-tag size="small" |
| | | :type="editParamForm.valueMode == '1' ? 'success' : 'warning'"> |
| | | {{ editParamForm.valueMode == '1' ? 'åå¼' : 'åºé´' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°ç±»å"> |
| | | <el-tag size="small" |
| | | :type="getParamTypeTag(editParamForm.paramType)"> |
| | | {{ getParamTypeText(editParamForm.paramType) }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°æ ¼å¼"> |
| | | <span class="detail-text">{{ editParamForm.paramFormat || '-' }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="åä½"> |
| | | <span class="detail-text">{{ editParamForm.unit || '-' }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="æ åå¼" |
| | | v-if="editParamForm.valueMode == '1' && editParamForm.paramType == '1'" |
| | | prop="standardValue"> |
| | | <el-input v-model="editParamForm.standardValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æ åå¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æå°å¼" |
| | | v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'" |
| | | prop="minValue"> |
| | | <el-input v-model="editParamForm.minValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æå°å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æå¤§å¼" |
| | | v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'" |
| | | prop="maxValue"> |
| | | <el-input v-model="editParamForm.maxValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æå¤§å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æåº" |
| | | prop="sort"> |
| | | <el-input v-model="editParamForm.sort" |
| | | type="number" |
| | | placeholder="请è¾å
¥æåº" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¯å¦å¿
å¡«" |
| | | prop="isRequired"> |
| | | <el-switch v-model="editParamForm.isRequired" |
| | | :active-value="1" |
| | | :inactive-value="0" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="editParamDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="handleEditParamSubmit">ç¡®å®</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <new-process v-if="isShowNewModal" |
| | | v-model:visible="isShowNewModal" |
| | | @completed="getList" /> |
| | | <edit-process v-if="isShowEditModal" |
| | | v-model:visible="isShowEditModal" |
| | | :record="record" |
| | | @completed="getList" /> |
| | | <route-item-form v-if="isShowItemModal" |
| | | v-model:visible="isShowItemModal" |
| | | :record="record" |
| | | @completed="getList" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, getCurrentInstance, onMounted } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { onMounted, ref, reactive, toRefs, getCurrentInstance } from "vue"; |
| | | import NewProcess from "@/views/productionManagement/processRoute/New.vue"; |
| | | import EditProcess from "@/views/productionManagement/processRoute/Edit.vue"; |
| | | import RouteItemForm from "@/views/productionManagement/processRoute/ItemsForm.vue"; |
| | | import { |
| | | Plus, |
| | | Edit, |
| | | Delete, |
| | | ArrowUp, |
| | | ArrowDown, |
| | | Right, |
| | | Search, |
| | | Check, |
| | | Close, |
| | | Box, |
| | | Document, |
| | | } from "@element-plus/icons-vue"; |
| | | import { listType } from "@/api/system/dict/type"; |
| | | import { getByModel } from "@/api/productionManagement/productBom.js"; |
| | | import { add, update, del } from "@/api/productionManagement/processRoute.js"; |
| | | import { |
| | | addOrUpdateProcessRouteItem, |
| | | batchDeleteProcessRouteItem, |
| | | sortProcessRouteItem, |
| | | findProcessRouteItemList, |
| | | getProcessParamList, |
| | | addProcessRouteItemParam, |
| | | editProcessRouteItemParam, |
| | | delProcessRouteItemParam, |
| | | } from "@/api/productionManagement/processRouteItem.js"; |
| | | import { list as getProcessListApi } from "@/api/productionManagement/productionProcess.js"; |
| | | import { getBaseParamList } from "@/api/basicData/parameterMaintenance.js"; |
| | | import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; |
| | | listPage, |
| | | del, |
| | | update, |
| | | } from "@/api/productionManagement/processRoute.js"; |
| | | import { useRouter } from "vue-router"; |
| | | import { ElMessageBox, ElMessage } from "element-plus"; |
| | | |
| | | // å·¥èºè·¯çº¿å表 |
| | | const routeList = ref([]); |
| | | const dictTypes = ref([]); |
| | | |
| | | // å·¥èºè·¯çº¿å页 |
| | | const routePage = reactive({ |
| | | const router = useRouter(); |
| | | const data = reactive({ |
| | | searchForm: { |
| | | model: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "å·¥èºè·¯çº¿ç¼å·", |
| | | prop: "processRouteCode", |
| | | }, |
| | | { |
| | | label: "产ååç§°", |
| | | prop: "productName", |
| | | }, |
| | | { |
| | | label: "è§æ ¼åç§°", |
| | | prop: "model", |
| | | }, |
| | | { |
| | | label: "BOMç¼å·", |
| | | prop: "bomNo", |
| | | }, |
| | | { |
| | | label: "æè¿°", |
| | | prop: "description", |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 280, |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: row => { |
| | | showEditModal(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "路线项ç®", |
| | | type: "text", |
| | | clickFun: row => { |
| | | showItemModal(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "æ¹å", |
| | | type: "primary", |
| | | text: true, |
| | | showHide: row => { |
| | | return !row.status; |
| | | }, |
| | | clickFun: row => { |
| | | handleApproveRoute(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "åæ¶æ¹å", |
| | | type: "warning", |
| | | text: true, |
| | | showHide: row => { |
| | | return row.status; |
| | | }, |
| | | clickFun: row => { |
| | | handleRevokeApproveRoute(row); |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | | const selectedRows = ref([]); |
| | | const tableLoading = ref(false); |
| | | const isShowNewModal = ref(false); |
| | | const isShowEditModal = ref(false); |
| | | const isShowItemModal = ref(false); |
| | | const record = ref({}); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | size: 100, |
| | | total: 0, |
| | | }); |
| | | |
| | | // è·åå
¨å±å®ä¾ |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | // 产åéæ©åBOMç¸å
³ |
| | | const showProductSelectDialog = ref(false); |
| | | const bomOptions = ref([]); |
| | | |
| | | // å·¥èºè·¯çº¿å¯¹è¯æ¡ |
| | | const routeDialogVisible = ref(false); |
| | | const isRouteEdit = ref(false); |
| | | const routeFormRef = ref(null); |
| | | const routeForm = reactive({ |
| | | id: null, |
| | | productModelId: null, |
| | | productName: "", |
| | | productModelName: "", |
| | | bomId: null, |
| | | routeCode: "", |
| | | description: "", |
| | | status: true, |
| | | }); |
| | | const routeRules = { |
| | | productModelId: [ |
| | | { required: true, message: "è¯·éæ©äº§å", trigger: "change" }, |
| | | ], |
| | | bomId: [{ required: true, message: "è¯·éæ©BOM", trigger: "change" }], |
| | | // æ¥è¯¢å表 |
| | | /** æç´¢æé®æä½ */ |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | // å·¥åºå¯¹è¯æ¡ |
| | | const processDialogVisible = ref(false); |
| | | const isProcessEdit = ref(false); |
| | | const processFormRef = ref(null); |
| | | const currentRouteId = ref(null); |
| | | const processForm = reactive({ |
| | | id: null, |
| | | no: "", |
| | | name: "", |
| | | remark: "", |
| | | status: true, |
| | | }); |
| | | const processRules = { |
| | | no: [{ required: true, message: "请è¾å
¥å·¥åºç¼ç ", trigger: "blur" }], |
| | | name: [{ required: true, message: "请è¾å
¥å·¥åºåç§°", trigger: "blur" }], |
| | | const pagination = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | |
| | | // 鿩工åºå¯¹è¯æ¡ |
| | | const selectProcessDialogVisible = ref(false); |
| | | const availableProcessList = ref([]); |
| | | const filteredProcessList = ref([]); |
| | | const selectedProcessItem = ref(null); |
| | | const processSearchKeyword = ref(""); |
| | | const currentRouteIndex = ref(null); |
| | | |
| | | // åæ°å¯¹è¯æ¡ |
| | | const paramDialogVisible = ref(false); |
| | | const isParamEdit = ref(false); |
| | | const paramFormRef = ref(null); |
| | | const currentProcessId = ref(null); |
| | | const paramForm = reactive({ |
| | | id: null, |
| | | parameterCode: "", |
| | | parameterName: "", |
| | | parameterType2: "1", |
| | | parameterType: "", |
| | | parameterFormat: "", |
| | | standardValue: "", |
| | | unit: "", |
| | | }); |
| | | const paramRules = { |
| | | parameterCode: [ |
| | | { required: true, message: "请è¾å
¥åæ°ç¼å·", trigger: "blur" }, |
| | | ], |
| | | parameterName: [ |
| | | { required: true, message: "请è¾å
¥åæ°åç§°", trigger: "blur" }, |
| | | ], |
| | | parameterType: [ |
| | | { required: true, message: "è¯·éæ©åæ°ç±»å", trigger: "change" }, |
| | | ], |
| | | }; |
| | | |
| | | // éæ©åæ°å¯¹è¯æ¡ |
| | | const selectParamDialogVisible = ref(false); |
| | | const availableParamList = ref([]); |
| | | const filteredParamList = ref([]); |
| | | const selectedParam = ref(null); |
| | | const paramSearchKeyword = ref(""); |
| | | |
| | | // å¯éåæ°å页 |
| | | const paramPage = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | |
| | | // ç¼è¾åæ°å¯¹è¯æ¡ |
| | | const editParamDialogVisible = ref(false); |
| | | const editParamFormRef = ref(null); |
| | | const editParamForm = reactive({ |
| | | id: null, |
| | | processId: null, |
| | | paramId: null, |
| | | paramName: "", |
| | | valueMode: "1", |
| | | standardValue: null, |
| | | minValue: null, |
| | | maxValue: null, |
| | | sort: 1, |
| | | isRequired: 0, |
| | | }); |
| | | const editParamRules = reactive({ |
| | | standardValue: [ |
| | | { |
| | | required: true, |
| | | message: "请è¾å
¥æ åå¼", |
| | | trigger: "blur", |
| | | validator: (rule, value, callback) => { |
| | | if (value === null || value === undefined || value === "") { |
| | | callback(new Error("请è¾å
¥æ åå¼")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }, |
| | | }, |
| | | ], |
| | | minValue: [ |
| | | { |
| | | required: true, |
| | | message: "请è¾å
¥æå°å¼", |
| | | trigger: "blur", |
| | | validator: (rule, value, callback) => { |
| | | if (value === null || value === undefined || value === "") { |
| | | callback(new Error("请è¾å
¥æå°å¼")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }, |
| | | }, |
| | | ], |
| | | maxValue: [ |
| | | { |
| | | required: true, |
| | | message: "请è¾å
¥æå¤§å¼", |
| | | trigger: "blur", |
| | | validator: (rule, value, callback) => { |
| | | if (value === null || value === undefined || value === "") { |
| | | callback(new Error("请è¾å
¥æå¤§å¼")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }, |
| | | }, |
| | | ], |
| | | sort: [ |
| | | { |
| | | required: true, |
| | | message: "请è¾å
¥æåº", |
| | | trigger: "blur", |
| | | validator: (rule, value, callback) => { |
| | | if (value === null || value === undefined || value === "") { |
| | | callback(new Error("请è¾å
¥æåº")); |
| | | } else if (isNaN(value) || value < 1) { |
| | | callback(new Error("æåºå¿
é¡»æ¯å¤§äº0çæ´æ°")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }, |
| | | }, |
| | | ], |
| | | }); |
| | | |
| | | // ææ½ç¸å
³ |
| | | const draggedItem = ref(null); |
| | | const draggedRouteId = ref(null); |
| | | |
| | | // è·åå·¥èºè·¯çº¿å表 |
| | | const getRouteList = () => { |
| | | // 导å
¥ listPage æ¹æ³ |
| | | import("@/api/productionManagement/processRoute.js").then(({ listPage }) => { |
| | | listPage({ pageNum: routePage.current, pageSize: routePage.size }) |
| | | .then(res => { |
| | | // å¤çè¿åçæ°æ®ï¼æ å°å°é¡µé¢éè¦çæ ¼å¼ |
| | | routeList.value = (res.data?.records || []).map(item => ({ |
| | | id: item.id, |
| | | productModelId: item.productModelId, |
| | | productName: item.productName, |
| | | productModelName: item.model || item.productModelName, |
| | | bomId: item.bomId, |
| | | bomNo: item.bomNo, |
| | | routeCode: item.processRouteCode || item.routeCode, |
| | | description: item.description || item.description, |
| | | status: item.status, |
| | | expanded: false, |
| | | processList: (item.processList || []).map(process => ({ |
| | | ...process, |
| | | processId: process.processId || process.id, |
| | | expanded: false, |
| | | })), |
| | | })); |
| | | // æ´æ°åé¡µæ»æ° |
| | | routePage.total = res.data?.total || 0; |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åå·¥èºè·¯çº¿å表失败ï¼", err); |
| | | routeList.value = []; |
| | | routePage.total = 0; |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | // å±å¼/æ¶èµ·å·¥èºè·¯çº¿ |
| | | const toggleExpand = route => { |
| | | route.expanded = !route.expanded; |
| | | if (route.expanded) { |
| | | // è°ç¨æ¥å£è·åå·¥åºå表 |
| | | findProcessRouteItemList({ routeId: route.id }) |
| | | .then(res => { |
| | | route.processList = (res.data || []).map(process => ({ |
| | | ...process, |
| | | processId: process.processId || process.id, |
| | | expanded: false, |
| | | })); |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åå·¥åºå表失败ï¼", err); |
| | | route.processList = []; |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // å±å¼/æ¶èµ·å·¥åºåæ° |
| | | const toggleProcessParams = process => { |
| | | process.expanded = !process.expanded; |
| | | if (process.expanded && process.id) { |
| | | // è°ç¨æ¥å£è·ååæ°å表 |
| | | getProcessParamList({ |
| | | routeItemId: process.id, |
| | | pageNum: 1, |
| | | pageSize: 1000, |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | const params = { ...searchForm.value, ...page }; |
| | | params.entryDate = undefined; |
| | | listPage(params) |
| | | .then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records.map(item => ({ |
| | | ...item, |
| | | })); |
| | | page.total = res.data.total; |
| | | }) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | process.paramList = res.data?.records || []; |
| | | process.paramCount = process.paramList.length; |
| | | } else { |
| | | ElMessage.error(res.msg || "è·ååæ°å表失败"); |
| | | process.paramList = []; |
| | | process.paramCount = 0; |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·ååæ°å表失败ï¼", err); |
| | | ElMessage.error("è·ååæ°å表失败"); |
| | | process.paramList = []; |
| | | process.paramCount = 0; |
| | | }); |
| | | } |
| | | .catch(err => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | const toggleProcessParams2 = process => { |
| | | if (process.expanded && process.id) { |
| | | // è°ç¨æ¥å£è·ååæ°å表 |
| | | getProcessParamList({ |
| | | routeItemId: process.id, |
| | | pageNum: 1, |
| | | pageSize: 1000, |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = selection => { |
| | | selectedRows.value = selection; |
| | | }; |
| | | |
| | | // æå¼æ°å¢å¼¹æ¡ |
| | | const showNewModal = () => { |
| | | isShowNewModal.value = true; |
| | | }; |
| | | |
| | | const showEditModal = row => { |
| | | isShowEditModal.value = true; |
| | | record.value = row; |
| | | }; |
| | | |
| | | const showItemModal = row => { |
| | | router.push({ |
| | | path: "/productionManagement/processRouteItem", |
| | | query: { |
| | | id: row.id, |
| | | processRouteCode: row.processRouteCode || "", |
| | | productName: row.productName || "", |
| | | model: row.model || "", |
| | | bomNo: row.bomNo || "", |
| | | bomId: row.bomId || null, |
| | | description: row.description || "", |
| | | type: "route", |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // å é¤ |
| | | function handleDelete() { |
| | | const ids = selectedRows.value.map(item => item.id); |
| | | proxy.$modal |
| | | .confirm("æ¯å¦ç¡®è®¤å é¤å·²å¾éçæ°æ®é¡¹ï¼") |
| | | .then(function () { |
| | | return del(ids); |
| | | }) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | process.paramList = res.data?.records || []; |
| | | process.paramCount = process.paramList.length; |
| | | } else { |
| | | ElMessage.error(res.msg || "è·ååæ°å表失败"); |
| | | process.paramList = []; |
| | | process.paramCount = 0; |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·ååæ°å表失败ï¼", err); |
| | | ElMessage.error("è·ååæ°å表失败"); |
| | | process.paramList = []; |
| | | process.paramCount = 0; |
| | | }); |
| | | } |
| | | }; |
| | | // å·¥èºè·¯çº¿æä½ |
| | | const handleAddRoute = () => { |
| | | isRouteEdit.value = false; |
| | | routeForm.id = null; |
| | | routeForm.productModelId = null; |
| | | routeForm.productName = ""; |
| | | routeForm.productModelName = ""; |
| | | routeForm.bomId = null; |
| | | routeForm.routeCode = ""; |
| | | routeForm.description = ""; |
| | | routeForm.status = false; |
| | | bomOptions.value = []; |
| | | routeDialogVisible.value = true; |
| | | }; |
| | | .then(() => { |
| | | getList(); |
| | | proxy.$modal.msgSuccess("å 餿å"); |
| | | }) |
| | | .catch(() => {}); |
| | | } |
| | | |
| | | const handleEditRoute = route => { |
| | | isRouteEdit.value = true; |
| | | routeForm.id = route.id; |
| | | routeForm.productModelId = route.productModelId; |
| | | routeForm.productName = route.productName; |
| | | routeForm.productModelName = route.productModelName; |
| | | routeForm.bomId = route.bomId; |
| | | routeForm.routeCode = route.routeCode; |
| | | routeForm.description = route.description; |
| | | routeForm.status = route.status; |
| | | routeDialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleDeleteRoute = route => { |
| | | ElMessageBox.confirm("ç¡®å®è¦å é¤è¯¥å·¥èºè·¯çº¿åï¼", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }).then(() => { |
| | | del(route.id) |
| | | .then(res => { |
| | | ElMessage.success("å 餿å"); |
| | | getRouteList(); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("å é¤å¤±è´¥"); |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const handleRouteSubmit = () => { |
| | | routeFormRef.value.validate(valid => { |
| | | if (valid) { |
| | | // æå»ºæäº¤æ°æ® |
| | | const submitData = { |
| | | ...routeForm, |
| | | // 注æï¼API ææçåæ®µåå¯è½ä¸è¡¨ååæ®µåä¸å |
| | | productId: routeForm.productModelId, |
| | | productModelId: routeForm.productModelId, |
| | | description: routeForm.description, |
| | | }; |
| | | |
| | | if (isRouteEdit.value) { |
| | | // ç¼è¾æä½ |
| | | update(submitData) |
| | | .then(res => { |
| | | ElMessage.success("ç¼è¾æå"); |
| | | routeDialogVisible.value = false; |
| | | getRouteList(); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("ç¼è¾å¤±è´¥"); |
| | | }); |
| | | } else { |
| | | // æ°å¢æä½ |
| | | add(submitData) |
| | | .then(res => { |
| | | ElMessage.success("æ°å¢æå"); |
| | | routeDialogVisible.value = false; |
| | | getRouteList(); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("æ°å¢å¤±è´¥"); |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | const isform2 = ref(null); |
| | | const handleProcessProductSelectClick = () => { |
| | | isform2.value = true; |
| | | showProductSelectDialog.value = true; |
| | | }; |
| | | const handleProcessProductSelectClick2 = () => { |
| | | isform2.value = false; |
| | | showProductSelectDialog.value = true; |
| | | }; |
| | | |
| | | // 产åéæ©å¤ç |
| | | const handleProductSelect = async products => { |
| | | if (isform2.value) { |
| | | // 帮æåå·¥åºä¸çéæ©äº§åçåè°,并䏿忮µå è¿processForm |
| | | if (products && products.length > 0) { |
| | | const product = products[0]; |
| | | console.log("product:", product); |
| | | // æproductä¸çåæ®µæ·»å å°processFormä¸ |
| | | // Object.assign(processForm, product); |
| | | processForm.productModelId = product.id; |
| | | processForm.productName = product.productName; |
| | | processForm.model = product.model; |
| | | processForm.unit = product.unit || ""; |
| | | console.log("processForm:", processForm); |
| | | |
| | | // 触å表åéªè¯æ´æ° |
| | | proxy.$refs["processFormRef"]?.validateField("productModelId"); |
| | | } |
| | | } else { |
| | | if (products && products.length > 0) { |
| | | const product = products[0]; |
| | | // å
æ¥è¯¢BOMå表ï¼å¿
éï¼ |
| | | try { |
| | | const res = await getByModel(product.id); |
| | | // å¤çè¿åçBOMæ°æ®ï¼å¯è½æ¯æ°ç»ã对象æå
å«dataåæ®µ |
| | | let bomList = []; |
| | | if (Array.isArray(res)) { |
| | | bomList = res; |
| | | } else if (res && res.data) { |
| | | bomList = Array.isArray(res.data) ? res.data : [res.data]; |
| | | } else if (res && typeof res === "object") { |
| | | bomList = [res]; |
| | | } |
| | | console.log("bomList:", bomList); |
| | | if (bomList.length > 0) { |
| | | routeForm.productModelId = product.id; |
| | | routeForm.productName = product.productName; |
| | | routeForm.productModelName = product.model; |
| | | routeForm.bomId = undefined; // éç½®BOMéæ© |
| | | bomOptions.value = bomList; |
| | | showProductSelectDialog.value = false; |
| | | // 触å表åéªè¯æ´æ° |
| | | proxy.$refs["routeFormRef"]?.validateField("productModelId"); |
| | | } else { |
| | | proxy.$modal.msgError("è¯¥äº§åæ²¡æBOMï¼è¯·å
å建BOM"); |
| | | } |
| | | } catch (error) { |
| | | // 妿æ¥å£è¿å404æå
¶ä»é误ï¼è¯´ææ²¡æBOM |
| | | proxy.$modal.msgError("è¯¥äº§åæ²¡æBOMï¼è¯·å
å建BOM"); |
| | | } |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // æ¹åå·¥èºè·¯çº¿ |
| | | const handleApproveRoute = route => { |
| | | ElMessageBox.confirm("ç¡®å®è¦æ¹å该工èºè·¯çº¿åï¼", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | |
| | | update({ id: route.id, status: true }) |
| | | .then(res => { |
| | | ElMessage.success("æ¹åæå"); |
| | | getRouteList(); |
| | | getList(); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("æ¹å失败"); |
| | |
| | | }); |
| | | }; |
| | | |
| | | // åæ¶æ¹åå·¥èºè·¯çº¿ |
| | | const handleRevokeApproveRoute = route => { |
| | | ElMessageBox.confirm("ç¡®å®è¦æ¤éæ¹å该工èºè·¯çº¿åï¼", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | |
| | | update({ id: route.id, status: false }) |
| | | .then(res => { |
| | | ElMessage.success("æ¤éæ¹åæå"); |
| | | getRouteList(); |
| | | getList(); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("æ¤éæ¹å失败"); |
| | | }); |
| | | }); |
| | | }; |
| | | // å·¥åºæä½ |
| | | const handleSelectProcess = (route, index) => { |
| | | console.log("route:", route); |
| | | currentRouteId.value = route.id; |
| | | currentRouteIndex.value = index; |
| | | // éç½®æç´¢åéæ©ç¶æ |
| | | filteredProcessList.value = availableProcessList.value; |
| | | processSearchKeyword.value = ""; |
| | | selectedProcessItem.value = null; |
| | | selectProcessDialogVisible.value = true; |
| | | }; |
| | | const dragSort = ref(0); |
| | | const currentId = ref(null); |
| | | // ä¿®æ¹å·¥åº |
| | | const handleEditProcessSelect = (route, index, process) => { |
| | | console.log("route:", route); |
| | | console.log("process:", process); |
| | | currentId.value = process.id; |
| | | currentRouteId.value = route.id; |
| | | currentRouteIndex.value = index; |
| | | // éç½®æç´¢åéæ©ç¶æ |
| | | filteredProcessList.value = availableProcessList.value; |
| | | processSearchKeyword.value = ""; |
| | | // 设置éä¸çå·¥åº |
| | | filteredProcessList.value.map(item => { |
| | | if (item.id === process.processId) { |
| | | selectedProcessItem.value = item; |
| | | } |
| | | }); |
| | | dragSort.value = process.dragSort; |
| | | // selectedProcessItem.value = process; |
| | | // å¡«å
产åéæ©è¡¨å |
| | | processForm.productModelId = process.productModelId; |
| | | processForm.productName = process.productName; |
| | | processForm.model = process.model; |
| | | processForm.processId = process.no; |
| | | // processForm.name = process.name; |
| | | processForm.unit = process.unit || ""; |
| | | processForm.isQuality = process.isQuality || false; |
| | | selectProcessDialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleEditProcess = (routeId, process) => { |
| | | currentRouteId.value = routeId; |
| | | isProcessEdit.value = true; |
| | | processForm.id = process.id; |
| | | processForm.no = process.no; |
| | | processForm.name = process.name; |
| | | processForm.remark = process.remark; |
| | | processForm.status = process.status; |
| | | processDialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleDeleteProcess = (routeId, process) => { |
| | | ElMessageBox.confirm("ç¡®å®è¦å é¤è¯¥å·¥åºåï¼", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }).then(() => { |
| | | // è°ç¨APIå é¤å·¥åº |
| | | batchDeleteProcessRouteItem([process.id]) |
| | | .then(res => { |
| | | ElMessage.success("å 餿å"); |
| | | // è°ç¨æ¥å£æ´æ°å·¥åºå表 |
| | | findProcessRouteItemList({ routeId: routeId }) |
| | | .then(res => { |
| | | const route = routeList.value.find(r => r.id === routeId); |
| | | if (route) { |
| | | route.processList = (res.data || []).map(process => ({ |
| | | ...process, |
| | | processId: process.processId || process.id, |
| | | expanded: false, |
| | | })); |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åå·¥åºå表失败ï¼", err); |
| | | }); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("å é¤å¤±è´¥"); |
| | | console.error("å é¤å·¥åºå¤±è´¥ï¼", err); |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const handleProcessSubmit = () => { |
| | | processFormRef.value.validate(valid => { |
| | | if (valid) { |
| | | ElMessage.success(isProcessEdit.value ? "ç¼è¾æå" : "æ°å¢æå"); |
| | | processDialogVisible.value = false; |
| | | // è°ç¨æ¥å£æ´æ°å·¥åºå表 |
| | | if (currentRouteId.value) { |
| | | findProcessRouteItemList({ routeId: currentRouteId.value }) |
| | | .then(res => { |
| | | const route = routeList.value.find( |
| | | r => r.id === currentRouteId.value |
| | | ); |
| | | if (route) { |
| | | route.processList = (res.data || []).map(process => ({ |
| | | ...process, |
| | | processId: process.processId || process.id, |
| | | expanded: false, |
| | | })); |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åå·¥åºå表失败ï¼", err); |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // 鿩工åºç¸å
³æ¹æ³ |
| | | const handleProcessSearch = () => { |
| | | const keyword = processSearchKeyword.value.trim().toLowerCase(); |
| | | if (!keyword) { |
| | | filteredProcessList.value = availableProcessList.value; |
| | | } else { |
| | | filteredProcessList.value = availableProcessList.value.filter( |
| | | item => |
| | | (item.name && item.name.toLowerCase().includes(keyword)) || |
| | | (item.no && item.no.toLowerCase().includes(keyword)) |
| | | ); |
| | | } |
| | | }; |
| | | |
| | | const handleProcessSelect = row => { |
| | | selectedProcessItem.value = row; |
| | | // é置产åéæ©è¡¨å |
| | | processForm.productModelId = undefined; |
| | | processForm.productName = ""; |
| | | processForm.productModelName = ""; |
| | | processForm.unit = ""; |
| | | processForm.isQuality = row.isQuality || false; |
| | | }; |
| | | |
| | | // å¤çå·¥åºéæ©æ¶ç产åéæ© |
| | | const handleProcessProductSelect = async products => { |
| | | if (products && products.length > 0) { |
| | | const product = products[0]; |
| | | processForm.productModelId = product.id; |
| | | processForm.productName = product.productName; |
| | | processForm.productModelName = product.model; |
| | | processForm.unit = product.unit || ""; |
| | | showProductSelectDialog.value = false; |
| | | } |
| | | }; |
| | | |
| | | const handleProcessSelectSubmit = () => { |
| | | if (!selectedProcessItem.value) { |
| | | ElMessage.warning("请å
éæ©ä¸ä¸ªå·¥åº"); |
| | | return; |
| | | } |
| | | |
| | | if (!processForm.productModelId) { |
| | | ElMessage.warning("è¯·éæ©äº§å"); |
| | | return; |
| | | } |
| | | |
| | | // æå»ºè¯·æ±åæ° |
| | | const params = { |
| | | routeId: currentRouteId.value, |
| | | processId: selectedProcessItem.value.id, |
| | | dragSort: routePage.total + 1, |
| | | ...processForm, |
| | | }; |
| | | |
| | | // 妿æ¯ä¿®æ¹æä½ï¼æ·»å idåæ° |
| | | if (selectedProcessItem.value.id) { |
| | | params.id = currentId.value; |
| | | params.dragSort = dragSort.value; |
| | | } |
| | | |
| | | // è°ç¨APIæ·»å å·¥åºæä¿®æ¹å·¥åº |
| | | addOrUpdateProcessRouteItem(params) |
| | | .then(res => { |
| | | ElMessage.success( |
| | | selectedProcessItem.value.id ? "ä¿®æ¹å·¥åºæå" : "æ·»å å·¥åºæå" |
| | | ); |
| | | selectProcessDialogVisible.value = false; |
| | | // è°ç¨æ¥å£æ´æ°å·¥åºå表 |
| | | findProcessRouteItemList({ routeId: currentRouteId.value }) |
| | | .then(res => { |
| | | const route = routeList.value.find( |
| | | r => r.id === currentRouteId.value |
| | | ); |
| | | if (route) { |
| | | route.processList = (res.data || []).map(process => ({ |
| | | ...process, |
| | | processId: process.processId || process.id, |
| | | expanded: false, |
| | | })); |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åå·¥åºå表失败ï¼", err); |
| | | }); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error( |
| | | selectedProcessItem.value.id ? "ä¿®æ¹å·¥åºå¤±è´¥" : "æ·»å å·¥åºå¤±è´¥" |
| | | ); |
| | | console.error( |
| | | selectedProcessItem.value.id ? "ä¿®æ¹å·¥åºå¤±è´¥ï¼" : "æ·»å å·¥åºå¤±è´¥ï¼", |
| | | err |
| | | ); |
| | | }); |
| | | }; |
| | | |
| | | // åæ°æä½ |
| | | const handleAddParam = (routeId, process) => { |
| | | currentRouteId.value = routeId; |
| | | currentProcessId.value = process.id; |
| | | selectedParam.value = null; |
| | | paramSearchKeyword.value = ""; |
| | | paramPage.current = 1; |
| | | // è·åå¯éåæ°å表 |
| | | getBaseParamList({ |
| | | paramName: paramSearchKeyword.value, |
| | | current: paramPage.current, |
| | | size: paramPage.size, |
| | | }).then(res => { |
| | | if (res.code === 200) { |
| | | filteredParamList.value = res.data?.records || []; |
| | | paramPage.total = res.data?.total || 0; |
| | | } else { |
| | | ElMessage.error(res.msg || "æ¥è¯¢å¤±è´¥"); |
| | | } |
| | | }); |
| | | selectParamDialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleEditParam = (routeId, process, param) => { |
| | | currentRouteId.value = routeId; |
| | | currentProcessId.value = process.id; |
| | | editParamForm.id = param.id; |
| | | editParamForm.processId = process.id; |
| | | editParamForm.paramId = param.paramId; |
| | | editParamForm.paramName = param.parameterName || param.paramName; |
| | | editParamForm.valueMode = param.parameterType2 || param.valueMode || "1"; |
| | | editParamForm.standardValue = param.standardValue; |
| | | editParamForm.minValue = param.minValue; |
| | | editParamForm.maxValue = param.maxValue; |
| | | editParamForm.sort = param.sort || 1; |
| | | editParamForm.isRequired = param.isRequired || 0; |
| | | editParamForm.paramType = param.parameterType || param.paramType; |
| | | editParamForm.paramFormat = param.parameterFormat || param.paramFormat; |
| | | editParamForm.unit = param.unit || param.unit; |
| | | editParamDialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleDeleteParam = (routeId, process, param) => { |
| | | ElMessageBox.confirm("ç¡®å®è¦å é¤è¯¥åæ°åï¼", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }).then(() => { |
| | | // è°ç¨APIå é¤åæ° |
| | | delProcessRouteItemParam(param.id) |
| | | .then(res => { |
| | | ElMessage.success("å 餿å"); |
| | | // å·æ°åæ°å表 |
| | | toggleProcessParams2(process); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("å é¤åæ°å¤±è´¥"); |
| | | console.error("å é¤åæ°å¤±è´¥ï¼", err); |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const handleParamSubmit = () => { |
| | | paramFormRef.value.validate(valid => { |
| | | if (valid) { |
| | | ElMessage.success(isParamEdit.value ? "ç¼è¾æå" : "æ°å¢æå"); |
| | | paramDialogVisible.value = false; |
| | | getRouteList(); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const handleParamTypeChange = () => { |
| | | if (paramForm.parameterType === "æ°å¼æ ¼å¼") { |
| | | paramForm.parameterFormat = "#.0000"; |
| | | } else if (paramForm.parameterType === "æ¶é´æ ¼å¼") { |
| | | paramForm.parameterFormat = "YYYY-MM-DD HH:mm:ss"; |
| | | } else { |
| | | paramForm.parameterFormat = ""; |
| | | } |
| | | }; |
| | | |
| | | const getParamTypeTag = type => { |
| | | const typeMap = { |
| | | 1: "primary", |
| | | 2: "info", |
| | | 3: "warning", |
| | | 4: "success", |
| | | }; |
| | | return typeMap[type] || "default"; |
| | | }; |
| | | |
| | | const getParamTypeText = type => { |
| | | const typeMap = { |
| | | 1: "æ°å¼æ ¼å¼", |
| | | 2: "ææ¬æ ¼å¼", |
| | | 3: "䏿é项", |
| | | 4: "æ¶é´æ ¼å¼", |
| | | }; |
| | | return typeMap[type] || "æªç¥åæ°ç±»å"; |
| | | }; |
| | | |
| | | // 鿩忰ç¸å
³æ¹æ³ |
| | | const handleParamSearch = () => { |
| | | // éç½®å页 |
| | | paramPage.current = 1; |
| | | // éæ°å è½½æ°æ® |
| | | getBaseParamList({ |
| | | paramName: paramSearchKeyword.value, |
| | | current: paramPage.current, |
| | | size: paramPage.size, |
| | | }).then(res => { |
| | | if (res.code === 200) { |
| | | filteredParamList.value = res.data?.records || []; |
| | | paramPage.total = res.data?.total || 0; |
| | | } else { |
| | | ElMessage.error(res.msg || "æ¥è¯¢å¤±è´¥"); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const handleParamSelect = row => { |
| | | selectedParam.value = row; |
| | | }; |
| | | |
| | | // å¤çå页大å°åå |
| | | const handleParamSizeChange = size => { |
| | | paramPage.size = size; |
| | | getBaseParamList({ |
| | | paramName: paramSearchKeyword.value, |
| | | current: paramPage.current, |
| | | size: paramPage.size, |
| | | }).then(res => { |
| | | if (res.code === 200) { |
| | | filteredParamList.value = res.data?.records || []; |
| | | paramPage.total = res.data?.total || 0; |
| | | } else { |
| | | ElMessage.error(res.msg || "æ¥è¯¢å¤±è´¥"); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // å¤çå½å页ç åå |
| | | const handleParamCurrentChange = current => { |
| | | paramPage.current = current; |
| | | getBaseParamList({ |
| | | paramName: paramSearchKeyword.value, |
| | | current: paramPage.current, |
| | | size: paramPage.size, |
| | | }).then(res => { |
| | | if (res.code === 200) { |
| | | filteredParamList.value = res.data?.records || []; |
| | | paramPage.total = res.data?.total || 0; |
| | | } else { |
| | | ElMessage.error(res.msg || "æ¥è¯¢å¤±è´¥"); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // å·¥èºè·¯çº¿å页å¤ç |
| | | const handleRouteSizeChange = size => { |
| | | routePage.size = size; |
| | | getRouteList(); |
| | | }; |
| | | |
| | | const handleRouteCurrentChange = current => { |
| | | routePage.current = current; |
| | | getRouteList(); |
| | | }; |
| | | |
| | | const handleParamSelectSubmit = () => { |
| | | if (!selectedParam.value) { |
| | | ElMessage.warning("请å
éæ©ä¸ä¸ªåæ°"); |
| | | return; |
| | | } |
| | | |
| | | // æ¾å°å¯¹åºçå·¥èºè·¯çº¿åå·¥åº |
| | | const route = routeList.value.find(r => r.id === currentRouteId.value); |
| | | const process = route?.processList.find(p => p.id === currentProcessId.value); |
| | | |
| | | if (route && process) { |
| | | // æ£æ¥åæ°æ¯å¦å·²åå¨ |
| | | // const exists = process.paramList?.some( |
| | | // p => |
| | | // p.paramId === selectedParam.value.id || |
| | | // p.parameterCode === selectedParam.value.paramCode |
| | | // ); |
| | | // if (exists) { |
| | | // ElMessage.warning("è¯¥åæ°å·²åå¨äºå·¥åºä¸"); |
| | | // return; |
| | | // } |
| | | |
| | | // å¤æåæ°ç±»åï¼åªææ°å¼ç±»åæä¼ æ åå¼ãæå¤§å¼åæå°å¼ |
| | | const isNumericMode = selectedParam.value.valueMode === 1; |
| | | |
| | | // è°ç¨APIæ°å¢åæ° |
| | | addProcessRouteItemParam({ |
| | | routeItemId: process.id, |
| | | paramId: selectedParam.value.id, |
| | | standardValue: isNumericMode |
| | | ? selectedParam.value.standardValue || "" |
| | | : "", |
| | | minValue: isNumericMode ? selectedParam.value.minValue || 0 : null, |
| | | maxValue: isNumericMode ? selectedParam.value.maxValue || 0 : null, |
| | | isRequired: selectedParam.value.isRequired || 0, |
| | | }) |
| | | .then(res => { |
| | | ElMessage.success("æ·»å åæ°æå"); |
| | | selectParamDialogVisible.value = false; |
| | | // å·æ°åæ°å表 |
| | | toggleProcessParams2(process); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("æ·»å åæ°å¤±è´¥"); |
| | | console.error("æ·»å åæ°å¤±è´¥ï¼", err); |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | const handleEditParamSubmit = () => { |
| | | editParamFormRef.value.validate(valid => { |
| | | if (valid) { |
| | | // å¤æåæ°ç±»åï¼åªææ°å¼ç±»åæä¼ æ åå¼ãæå¤§å¼åæå°å¼ |
| | | const isNumericMode = editParamForm.valueMode == 1; |
| | | |
| | | // è°ç¨APIä¿®æ¹åæ° |
| | | editProcessRouteItemParam({ |
| | | id: editParamForm.id, |
| | | routeItemId: currentProcessId.value, |
| | | paramId: editParamForm.paramId, |
| | | standardValue: isNumericMode ? editParamForm.standardValue || "" : "", |
| | | minValue: isNumericMode ? editParamForm.minValue || 0 : null, |
| | | maxValue: isNumericMode ? editParamForm.maxValue || 0 : null, |
| | | isRequired: editParamForm.isRequired || 0, |
| | | }) |
| | | .then(res => { |
| | | ElMessage.success("ç¼è¾æå"); |
| | | editParamDialogVisible.value = false; |
| | | // æ¾å°å¯¹åºçå·¥èºè·¯çº¿åå·¥åº |
| | | const route = routeList.value.find( |
| | | r => r.id === currentRouteId.value |
| | | ); |
| | | const process = route?.processList.find( |
| | | p => p.id === currentProcessId.value |
| | | ); |
| | | // å·æ°åæ°å表 |
| | | if (process) { |
| | | toggleProcessParams2(process); |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("ç¼è¾åæ°å¤±è´¥"); |
| | | console.error("ç¼è¾åæ°å¤±è´¥ï¼", err); |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // ææ½æåº |
| | | const handleDragStart = (event, index, routeId) => { |
| | | draggedItem.value = index; |
| | | draggedRouteId.value = routeId; |
| | | event.dataTransfer.effectAllowed = "move"; |
| | | }; |
| | | |
| | | const handleDragOver = event => { |
| | | event.preventDefault(); |
| | | event.dataTransfer.dropEffect = "move"; |
| | | }; |
| | | |
| | | const handleDrop = (event, dropIndex, routeId) => { |
| | | event.preventDefault(); |
| | | if (draggedItem.value === null || draggedItem.value === dropIndex) return; |
| | | |
| | | const route = routeList.value.find(r => r.id === routeId); |
| | | if (route && route.processList) { |
| | | const draggedProcess = route.processList[draggedItem.value]; |
| | | |
| | | // è®¡ç®æ°çæåºå¼ |
| | | const newDragSort = dropIndex + 1; |
| | | |
| | | // è°ç¨APIæåºå·¥åº |
| | | sortProcessRouteItem({ |
| | | id: draggedProcess.id, |
| | | dragSort: newDragSort, |
| | | }) |
| | | .then(res => { |
| | | // è°ç¨æ¥å£è·åææ°çå·¥åºå表 |
| | | findProcessRouteItemList({ routeId: routeId }) |
| | | .then(res => { |
| | | if (route) { |
| | | route.processList = (res.data || []).map(process => ({ |
| | | ...process, |
| | | processId: process.processId || process.id, |
| | | expanded: false, |
| | | })); |
| | | } |
| | | ElMessage.success("æåºæå"); |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åå·¥åºå表失败ï¼", err); |
| | | ElMessage.success("æåºæå"); |
| | | }); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("æåºå¤±è´¥"); |
| | | console.error("æåºå·¥åºå¤±è´¥ï¼", err); |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | const handleDragEnd = () => { |
| | | draggedItem.value = null; |
| | | draggedRouteId.value = null; |
| | | }; |
| | | |
| | | // è·åæ°æ®åå
¸ |
| | | const getDictTypes = () => { |
| | | listType({ pageNum: 1, pageSize: 1000 }).then(res => { |
| | | dictTypes.value = res.rows || []; |
| | | }); |
| | | }; |
| | | |
| | | getRouteList(); |
| | | getDictTypes(); |
| | | |
| | | // 页é¢å è½½æ¶è·åå·¥åºå表 |
| | | onMounted(() => { |
| | | getProcessListApi() |
| | | .then(res => { |
| | | // å¤çè¿åçæ°æ®ï¼æ å°å°é¡µé¢éè¦çæ ¼å¼ |
| | | availableProcessList.value = (res.data || []).map(item => ({ |
| | | id: item.id, |
| | | no: item.no || item.no, |
| | | name: item.name || item.name, |
| | | remark: item.remark || item.remark, |
| | | status: item.status, |
| | | isQuality: item.isQuality, |
| | | })); |
| | | filteredProcessList.value = availableProcessList.value; |
| | | }) |
| | | .catch(() => { |
| | | ElMessage.error("è·åå·¥åºå表失败"); |
| | | }); |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .app-container { |
| | | padding: 20px; |
| | | padding-bottom: 80px; |
| | | background-color: #f0f2f5; |
| | | min-height: calc(100vh - 84px); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .route-header { |
| | | margin-bottom: 20px; |
| | | |
| | | .add-route-btn { |
| | | width: 100%; |
| | | display: inline-flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-width: 120px; |
| | | height: 100px; |
| | | border: 2px dashed #dcdfe6; |
| | | border-radius: 12px; |
| | | background: #fafafa; |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | color: #909399; |
| | | padding: 0 20px; |
| | | |
| | | .el-icon { |
| | | font-size: 24px; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | span { |
| | | font-size: 13px; |
| | | } |
| | | |
| | | &:hover { |
| | | border-color: #409eff; |
| | | background: #ecf5ff; |
| | | color: #409eff; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .route-card-list { |
| | | display: grid; |
| | | grid-template-columns: repeat(1, 1fr); |
| | | gap: 20px; |
| | | max-height: calc(100vh - 240px); |
| | | overflow-y: auto; |
| | | padding-right: 10px; |
| | | } |
| | | |
| | | /* èªå®ä¹æ»å¨æ¡æ ·å¼ */ |
| | | .route-card-list::-webkit-scrollbar { |
| | | width: 8px; |
| | | } |
| | | |
| | | .route-card-list::-webkit-scrollbar-track { |
| | | background: #f1f1f1; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .route-card-list::-webkit-scrollbar-thumb { |
| | | background: #c1c1c1; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .route-card-list::-webkit-scrollbar-thumb:hover { |
| | | background: #a8a8a8; |
| | | } |
| | | |
| | | .route-card { |
| | | background: #fff; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | overflow: hidden; |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 20px 40px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | background: #f8f9fa; |
| | | |
| | | .route-info { |
| | | display: flex; |
| | | // flex-direction: column; |
| | | // justify-content: center; |
| | | // items-align: center; |
| | | gap: 4px; |
| | | |
| | | .route-code { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | font-family: "Courier New", monospace; |
| | | line-height: 30px; |
| | | } |
| | | |
| | | .route-name { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | } |
| | | |
| | | .route-actions { |
| | | display: flex; |
| | | gap: 8px; |
| | | |
| | | // .el-button { |
| | | // color: #409eff; |
| | | // } |
| | | } |
| | | } |
| | | |
| | | .card-body { |
| | | padding: 16px 40px; |
| | | |
| | | .route-desc { |
| | | font-size: 14px; |
| | | color: #606266; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .route-meta { |
| | | display: flex; |
| | | gap: 24px; |
| | | margin-bottom: 12px; |
| | | padding: 10px 14px; |
| | | background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%); |
| | | border-radius: 8px; |
| | | border-left: 3px solid #409eff; |
| | | |
| | | .meta-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | font-size: 13px; |
| | | margin-right: 40px; |
| | | |
| | | .el-icon { |
| | | font-size: 14px; |
| | | color: #409eff; |
| | | } |
| | | |
| | | .meta-label { |
| | | color: #909399; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .meta-value { |
| | | color: #303133; |
| | | font-weight: 600; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .expand-btn-wrapper { |
| | | display: flex; |
| | | justify-content: center; |
| | | margin-top: 8px; |
| | | |
| | | .expand-btn { |
| | | padding: 8px 20px; |
| | | border-radius: 20px; |
| | | background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%); |
| | | border: 1px solid #b3d8ff; |
| | | transition: all 0.3s ease; |
| | | |
| | | .btn-text { |
| | | font-size: 13px; |
| | | font-weight: 500; |
| | | color: #409eff; |
| | | margin-right: 6px; |
| | | } |
| | | |
| | | .expand-icon { |
| | | font-size: 14px; |
| | | color: #409eff; |
| | | transition: transform 0.3s ease; |
| | | } |
| | | |
| | | &:hover { |
| | | background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%); |
| | | border-color: #409eff; |
| | | box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3); |
| | | |
| | | .btn-text, |
| | | .expand-icon { |
| | | color: #fff; |
| | | } |
| | | } |
| | | |
| | | &.expanded { |
| | | background: linear-gradient(135deg, #f0f9eb 0%, #e1f3d8 100%); |
| | | border-color: #a5d69a; |
| | | |
| | | .btn-text, |
| | | .expand-icon { |
| | | color: #67c23a; |
| | | } |
| | | |
| | | &:hover { |
| | | background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%); |
| | | border-color: #67c23a; |
| | | box-shadow: 0 4px 12px rgba(103, 194, 58, 0.3); |
| | | |
| | | .btn-text, |
| | | .expand-icon { |
| | | color: #fff; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .process-route { |
| | | padding: 0 20px 20px; |
| | | background: #f5f7fa; |
| | | border-top: 1px solid #ebeef5; |
| | | |
| | | .process-flow { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | gap: 8px; |
| | | padding: 20px 0; |
| | | overflow-x: auto; |
| | | overflow-y: hidden; |
| | | |
| | | .process-flow-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | |
| | | .process-node { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 16px; |
| | | border: 2px solid #ebeef5; |
| | | cursor: move; |
| | | transition: all 0.3s ease; |
| | | // min-width: 180px; |
| | | // max-width: 220px; |
| | | width: 300px; |
| | | |
| | | &.expanded { |
| | | width: 400px; |
| | | } |
| | | |
| | | &:hover { |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| | | transform: translateY(-2px); |
| | | border-color: #409eff; |
| | | } |
| | | |
| | | &:active { |
| | | cursor: grabbing; |
| | | } |
| | | |
| | | .process-node-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 12px; |
| | | |
| | | .process-number { |
| | | width: 28px; |
| | | height: 28px; |
| | | border-radius: 50%; |
| | | background: #409eff; |
| | | color: #ffffff; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .process-actions { |
| | | display: flex; |
| | | gap: 4px; |
| | | } |
| | | } |
| | | |
| | | .process-node-body { |
| | | text-align: center; |
| | | margin-bottom: 12px; |
| | | |
| | | .process-code { |
| | | font-size: 11px; |
| | | color: #909399; |
| | | font-family: "Courier New", monospace; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .process-name { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 6px; |
| | | } |
| | | |
| | | .process-desc { |
| | | font-size: 12px; |
| | | color: #606266; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | display: -webkit-box; |
| | | -webkit-line-clamp: 2; |
| | | -webkit-box-orient: vertical; |
| | | } |
| | | } |
| | | |
| | | .process-node-footer { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | align-items: center; |
| | | padding-top: 10px; |
| | | border-top: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .process-params-section { |
| | | margin-top: 12px; |
| | | padding-top: 12px; |
| | | border-top: 1px solid #ebeef5; |
| | | |
| | | .params-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 8px; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .params-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 6px; |
| | | max-height: 200px; |
| | | overflow-y: auto; |
| | | |
| | | .param-item { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 6px 8px; |
| | | background: #fafafa; |
| | | border-radius: 4px; |
| | | border-left: 2px solid #409eff; |
| | | font-size: 12px; |
| | | |
| | | .param-info { |
| | | display: flex; |
| | | flex-direction: row; |
| | | align-items: center; |
| | | gap: 6px; |
| | | flex: 1; |
| | | min-width: 0; |
| | | |
| | | .param-code { |
| | | font-size: 11px; |
| | | color: #e6a23c; |
| | | font-family: "Courier New", monospace; |
| | | margin-right: 20px; |
| | | } |
| | | |
| | | .param-name { |
| | | font-size: 12px; |
| | | color: #303133; |
| | | font-weight: 500; |
| | | margin-right: 20px; |
| | | } |
| | | |
| | | .param-value { |
| | | font-size: 11px; |
| | | color: #606266; |
| | | } |
| | | } |
| | | |
| | | .param-actions { |
| | | display: flex; |
| | | gap: 4px; |
| | | flex-shrink: 0; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .flow-arrow { |
| | | display: flex; |
| | | align-items: center; |
| | | color: #c0c4cc; |
| | | font-size: 24px; |
| | | padding: 0 4px; |
| | | |
| | | .el-icon { |
| | | font-size: 20px; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .add-process-node { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-width: 100px; |
| | | height: 137px; |
| | | border: 2px dashed #dcdfe6; |
| | | border-radius: 12px; |
| | | background: #fafafa; |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | color: #909399; |
| | | // margin-left: 10px; |
| | | |
| | | .el-icon { |
| | | font-size: 24px; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | span { |
| | | font-size: 13px; |
| | | } |
| | | |
| | | &:hover { |
| | | border-color: #409eff; |
| | | background: #ecf5ff; |
| | | color: #409eff; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // ææ½æ¶çæ ·å¼ |
| | | .process-flow-item.dragging { |
| | | opacity: 0.5; |
| | | transform: scale(0.98); |
| | | } |
| | | |
| | | // 鿩工åºå¯¹è¯æ¡æ ·å¼ |
| | | .process-select-container { |
| | | display: flex; |
| | | gap: 20px; |
| | | height: 450px; |
| | | |
| | | .process-list-area { |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | .area-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 12px; |
| | | padding-bottom: 8px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .search-box { |
| | | margin-bottom: 12px; |
| | | |
| | | .el-input { |
| | | width: 100%; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .process-detail-area { |
| | | width: 380px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | padding: 16px; |
| | | |
| | | .area-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 16px; |
| | | padding-bottom: 8px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .process-detail-form { |
| | | .el-form-item { |
| | | margin-bottom: 12px; |
| | | |
| | | .el-form-item__label { |
| | | color: #606266; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | |
| | | .detail-text { |
| | | color: #303133; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // éæ©åæ°å¯¹è¯æ¡æ ·å¼ |
| | | .param-select-container { |
| | | display: flex; |
| | | gap: 20px; |
| | | height: 450px; |
| | | |
| | | .param-list-area { |
| | | // flex: 1; |
| | | width: 380px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | .area-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 12px; |
| | | padding-bottom: 8px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .search-box { |
| | | margin-bottom: 12px; |
| | | |
| | | .el-input { |
| | | width: 100%; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .param-detail-area { |
| | | // width: 380px; |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | padding: 16px; |
| | | |
| | | .area-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 16px; |
| | | padding-bottom: 8px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .param-detail-form { |
| | | .el-form-item { |
| | | margin-bottom: 12px; |
| | | |
| | | .el-form-item__label { |
| | | color: #606266; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | |
| | | .detail-text { |
| | | color: #303133; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // å页æ§ä»¶æ ·å¼ |
| | | .pagination-container { |
| | | position: fixed; |
| | | bottom: 0; |
| | | left: 0; |
| | | right: 0; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding: 16px 20px; |
| | | background-color: #fff !important; |
| | | border-top: 1px solid #ebeef5; |
| | | box-shadow: 0 -2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | z-index: 100; |
| | | |
| | | .el-pagination { |
| | | .el-pagination__sizes { |
| | | margin-right: 16px; |
| | | } |
| | | |
| | | .el-pagination__jump { |
| | | margin-left: 16px; |
| | | } |
| | | |
| | | .el-pagination__total { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .el-pagination__button { |
| | | border-radius: 4px; |
| | | transition: all 0.3s ease; |
| | | |
| | | &:hover:not(:disabled) { |
| | | color: #409eff; |
| | | border-color: #409eff; |
| | | } |
| | | } |
| | | |
| | | .el-pagination__button--active { |
| | | background-color: #409eff; |
| | | border-color: #409eff; |
| | | color: #fff; |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | | <style scoped></style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="route-header"> |
| | | <div class="add-route-btn" |
| | | @click="handleAddRoute"> |
| | | <el-icon> |
| | | <Plus /> |
| | | </el-icon> |
| | | <span>æ°å¢å·¥èºè·¯çº¿</span> |
| | | </div> |
| | | </div> |
| | | <div class="route-card-list"> |
| | | <div v-for="route in routeList" |
| | | :key="route.id" |
| | | class="route-card"> |
| | | <div class="card-header"> |
| | | <div class="route-info"> |
| | | <span class="route-name"><el-icon style="margin-right: 8px;line-height: 30px;"> |
| | | <ScaleToOriginal /> |
| | | </el-icon>{{route.routeCode }}<el-tag style="margin-left: 8px" |
| | | :type="!route.status ? 'warning' : 'success'">{{ !route.status ? 'è稿' : 'æ¹å' }}</el-tag></span> |
| | | <!-- <span class="route-code">{{ route.routeCode }}</span> --> |
| | | </div> |
| | | <div class="route-actions"> |
| | | <el-button v-if="!route.status" |
| | | link |
| | | type="success" |
| | | @click="handleApproveRoute(route)"> |
| | | <el-icon> |
| | | <Check /> |
| | | </el-icon> |
| | | æ¹å |
| | | </el-button> |
| | | <el-button v-if="route.status" |
| | | link |
| | | type="warning" |
| | | @click="handleRevokeApproveRoute(route)"> |
| | | <el-icon> |
| | | <Close /> |
| | | </el-icon> |
| | | æ¤éæ¹å |
| | | </el-button> |
| | | <el-button link |
| | | type="primary" |
| | | @click="handleEditRoute(route)"> |
| | | <el-icon> |
| | | <Edit /> |
| | | </el-icon> |
| | | ç¼è¾ |
| | | </el-button> |
| | | <el-button link |
| | | type="danger" |
| | | @click="handleDeleteRoute(route)"> |
| | | <el-icon> |
| | | <Delete /> |
| | | </el-icon> |
| | | å é¤ |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <div class="card-body"> |
| | | <div class="route-meta"> |
| | | <span class="meta-item"> |
| | | <el-icon> |
| | | <Box /> |
| | | </el-icon> |
| | | <span class="meta-label">产å:</span> |
| | | <span class="meta-value">{{ route.productName }} - {{ route.productModelName }}</span> |
| | | </span> |
| | | <span class="meta-item"> |
| | | <el-icon> |
| | | <Document /> |
| | | </el-icon> |
| | | <span class="meta-label">BOM:</span> |
| | | <span class="meta-value">{{ route.bomNo || '-' }}</span> |
| | | </span> |
| | | <span class="meta-item"> |
| | | <el-icon> |
| | | <Document /> |
| | | </el-icon> |
| | | <span class="meta-label">夿³¨:</span> |
| | | <span class="meta-value">{{ route.description || 'ææ æè¿°' }}</span> |
| | | </span> |
| | | </div> |
| | | <div class="expand-btn-wrapper"> |
| | | <el-button class="expand-btn" |
| | | :class="{ expanded: route.expanded }" |
| | | type="primary" |
| | | text |
| | | @click="toggleExpand(route)"> |
| | | <span class="btn-text">{{ route.expanded ? 'æ¶èµ·å·¥åºè·¯çº¿' : 'å±å¼å·¥åºè·¯çº¿' }}</span> |
| | | <el-icon class="expand-icon"> |
| | | <component :is="route.expanded ? 'ArrowUp' : 'ArrowDown'" /> |
| | | </el-icon> |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <div v-if="route.expanded" |
| | | class="process-route"> |
| | | <div class="process-flow"> |
| | | <div v-for="(process, index) in route.processList" |
| | | :key="process.id" |
| | | class="process-flow-item" |
| | | draggable="true" |
| | | @dragstart="handleDragStart($event, index, route.id)" |
| | | @dragover="handleDragOver($event)" |
| | | @drop="handleDrop($event, index, route.id)" |
| | | @dragend="handleDragEnd"> |
| | | <div class="process-node" |
| | | :class="{ expanded: process.expanded }"> |
| | | <div class="process-node-header"> |
| | | <div class="process-number">{{ index + 1 }}</div> |
| | | <div class="process-actions"> |
| | | <el-button link |
| | | type="primary" |
| | | @click="handleEditProcessSelect(route, index, process)"> |
| | | <el-icon> |
| | | <Edit /> |
| | | </el-icon> |
| | | </el-button> |
| | | <el-button link |
| | | type="danger" |
| | | @click="handleDeleteProcess(route.id, process)"> |
| | | <el-icon> |
| | | <Delete /> |
| | | </el-icon> |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <div class="process-node-body"> |
| | | <!-- <div class="process-code">{{ process.processId }}</div> --> |
| | | <div class="process-name">{{ process.processName }}</div> |
| | | <!-- <div class="process-desc">{{ process.remark || 'ææ æè¿°' }}</div> --> |
| | | </div> |
| | | <div class="process-node-footer"> |
| | | <!-- <el-tag size="small" |
| | | :type="process.status === '1' ? 'success' : 'info'"> |
| | | {{ process.status === '1' ? 'å¯ç¨' : 'åç¨' }} |
| | | </el-tag> --> |
| | | <el-button type="primary" |
| | | link |
| | | size="small" |
| | | @click="toggleProcessParams(process)"> |
| | | {{ process.expanded ? 'æ¶èµ·åæ°' : 'å±å¼åæ°' }} |
| | | ({{ process.paramCount }}) |
| | | </el-button> |
| | | </div> |
| | | <div v-if="process.expanded" |
| | | class="process-params-section"> |
| | | <div class="params-header"> |
| | | <span>åæ°å表</span> |
| | | <el-button type="primary" |
| | | link |
| | | size="small" |
| | | @click="handleAddParam(route.id, process)"> |
| | | <el-icon> |
| | | <Plus /> |
| | | </el-icon>æ°å¢ |
| | | </el-button> |
| | | </div> |
| | | <div class="params-list"> |
| | | <div v-for="param in process.paramList" |
| | | :key="param.id" |
| | | class="param-item"> |
| | | <div class="param-info"> |
| | | <span class="param-code">{{ param.paramName }}</span> |
| | | <!-- <span class="param-name">{{ param.paramName }}</span> --> |
| | | <!-- <el-tag size="small" |
| | | style="margin-right: 20px;" |
| | | :type="getParamTypeTag(param.parameterType)"> |
| | | {{ param.parameterType }} |
| | | </el-tag> --> |
| | | <span v-if="param.valueMode==1" |
| | | class="param-value">æ åå¼ï¼{{ param.standardValue || "-" }} {{ param.unit }}</span> |
| | | <span v-else |
| | | class="param-value">æ åå¼ï¼{{ param.minValue || "-" }}-{{ param.maxValue || "-" }} {{ param.unit }}</span> |
| | | </div> |
| | | <div class="param-actions"> |
| | | <el-button link |
| | | type="primary" |
| | | size="small" |
| | | @click="handleEditParam(route.id, process, param)"> |
| | | ç¼è¾ |
| | | </el-button> |
| | | <el-button link |
| | | type="danger" |
| | | size="small" |
| | | @click="handleDeleteParam(route.id, process, param)"> |
| | | å é¤ |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <el-empty v-if="!process.paramList || process.paramList.length === 0" |
| | | description="ææ åæ°" |
| | | :image-size="50" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div v-if="index < route.processList.length - 1" |
| | | class="flow-arrow"> |
| | | <el-icon> |
| | | <Right /> |
| | | </el-icon> |
| | | </div> |
| | | </div> |
| | | <div class="add-process-node" |
| | | @click="handleSelectProcess(route, index)"> |
| | | <el-icon> |
| | | <Plus /> |
| | | </el-icon> |
| | | <span>æ°å¢å·¥åº</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- å页æ§ä»¶ --> |
| | | <div class="pagination-container"> |
| | | <el-pagination v-model:current-page="routePage.current" |
| | | v-model:page-size="routePage.size" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :total="routePage.total" |
| | | @size-change="handleRouteSizeChange" |
| | | @current-change="handleRouteCurrentChange" /> |
| | | </div> |
| | | <!-- å·¥èºè·¯çº¿æ°å¢/ç¼è¾å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="routeDialogVisible" |
| | | :title="isRouteEdit ? 'ç¼è¾å·¥èºè·¯çº¿' : 'æ°å¢å·¥èºè·¯çº¿'" |
| | | width="500px"> |
| | | <el-form :model="routeForm" |
| | | :rules="routeRules" |
| | | ref="routeFormRef" |
| | | label-width="120px"> |
| | | <el-form-item label="产ååç§°" |
| | | prop="productModelId"> |
| | | <el-button type="primary" |
| | | @click="handleProcessProductSelectClick2"> |
| | | {{ routeForm.productName && routeForm.productModelName |
| | | ? `${routeForm.productName} - ${routeForm.productModelName}` |
| | | : 'éæ©äº§å' }} |
| | | </el-button> |
| | | </el-form-item> |
| | | <el-form-item label="BOM" |
| | | prop="bomId"> |
| | | <el-select v-model="routeForm.bomId" |
| | | placeholder="è¯·éæ©BOM" |
| | | clearable |
| | | :disabled="!routeForm.productModelId || bomOptions.length === 0" |
| | | style="width: 100%"> |
| | | <el-option v-for="item in bomOptions" |
| | | :key="item.id" |
| | | :label="item.bomNo || `BOM-${item.id}`" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="路线ç¼ç " |
| | | prop="routeCode"> |
| | | <el-input v-model="routeForm.routeCode" |
| | | disabled |
| | | placeholder="èªå¨çæ" /> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨" |
| | | prop="description"> |
| | | <el-input v-model="routeForm.description" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="请è¾å
¥è·¯çº¿æè¿°" /> |
| | | </el-form-item> |
| | | <!-- <el-form-item label="ç¶æ" |
| | | prop="status"> |
| | | <el-radio-group v-model="routeForm.status"> |
| | | <el-radio label="1">å¯ç¨</el-radio> |
| | | <el-radio label="0">åç¨</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> --> |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="routeDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="handleRouteSubmit">ç¡®å®</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 产åéæ©å¼¹çª --> |
| | | <ProductSelectDialog v-model="showProductSelectDialog" |
| | | @confirm="handleProductSelect" |
| | | single /> |
| | | <!-- å·¥åºæ°å¢/ç¼è¾å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="processDialogVisible" |
| | | :title="isProcessEdit ? 'ç¼è¾å·¥åº' : 'æ°å¢å·¥åº'" |
| | | width="500px"> |
| | | <el-form :model="processForm" |
| | | :rules="processRules" |
| | | ref="processFormRef" |
| | | label-width="120px"> |
| | | <el-form-item label="å·¥åºç¼ç " |
| | | prop="no"> |
| | | <el-input v-model="processForm.no" |
| | | placeholder="请è¾å
¥å·¥åºç¼ç " /> |
| | | </el-form-item> |
| | | <el-form-item label="å·¥åºåç§°" |
| | | prop="name"> |
| | | <el-input v-model="processForm.name" |
| | | placeholder="请è¾å
¥å·¥åºåç§°" /> |
| | | </el-form-item> |
| | | <el-form-item label="å·¥åºæè¿°" |
| | | prop="remark"> |
| | | <el-input v-model="processForm.remark" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="请è¾å
¥å·¥åºæè¿°" /> |
| | | </el-form-item> |
| | | <el-form-item label="ç¶æ" |
| | | prop="status"> |
| | | <el-radio-group v-model="processForm.status"> |
| | | <el-radio :label="true">å¯ç¨</el-radio> |
| | | <el-radio :label="false">åç¨</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="processDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="handleProcessSubmit">ç¡®å®</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 鿩工åºå¯¹è¯æ¡ --> |
| | | <el-dialog v-model="selectProcessDialogVisible" |
| | | title="鿩工åº" |
| | | width="1000px"> |
| | | <div class="process-select-container"> |
| | | <!-- 左侧工åºå表 --> |
| | | <div class="process-list-area"> |
| | | <div class="area-title">å¯éå·¥åº</div> |
| | | <div class="search-box"> |
| | | <el-input v-model="processSearchKeyword" |
| | | placeholder="请è¾å
¥å·¥åºåç§°æç´¢" |
| | | clearable |
| | | size="small" |
| | | @input="handleProcessSearch"> |
| | | <template #prefix> |
| | | <el-icon> |
| | | <Search /> |
| | | </el-icon> |
| | | </template> |
| | | </el-input> |
| | | </div> |
| | | <el-table :data="filteredProcessList" |
| | | height="360" |
| | | border |
| | | highlight-current-row |
| | | @current-change="handleProcessSelect"> |
| | | <el-table-column prop="no" |
| | | label="å·¥åºç¼å·" |
| | | width="100" /> |
| | | <el-table-column prop="name" |
| | | label="å·¥åºåç§°" /> |
| | | <el-table-column prop="remark" |
| | | label="å·¥åºæè¿°" /> |
| | | <el-table-column prop="status" |
| | | label="ç¶æ" |
| | | width="80"> |
| | | <template #default="scope"> |
| | | <el-tag size="small" |
| | | :type="scope.row.status ? 'success' : 'info'"> |
| | | {{ scope.row.status ? 'å¯ç¨' : 'åç¨' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | <!-- å³ä¾§å·¥åºè¯¦æ
--> |
| | | <div class="process-detail-area"> |
| | | <div class="area-title">å·¥åºè¯¦æ
</div> |
| | | <el-form v-if="selectedProcessItem" |
| | | :model="processForm" |
| | | label-width="100px" |
| | | class="process-detail-form"> |
| | | <el-form-item label="å·¥åºç¼å·"> |
| | | <span class="detail-text">{{ selectedProcessItem.no }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="å·¥åºåç§°"> |
| | | <span class="detail-text">{{ selectedProcessItem.name }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="å·¥åºæè¿°"> |
| | | <span class="detail-text">{{ selectedProcessItem.remark || '-' }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="ç¶æ"> |
| | | <el-tag size="small" |
| | | :type="selectedProcessItem.status ? 'success' : 'info'"> |
| | | {{ selectedProcessItem.status ? 'å¯ç¨' : 'åç¨' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="æ¯å¦è´¨æ£"> |
| | | <el-tag size="small" |
| | | :type="selectedProcessItem.isQuality ? 'success' : 'info'"> |
| | | {{ selectedProcessItem.isQuality ? 'è´¨æ£' : 'éè´¨æ£' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="产ååç§°" |
| | | prop="productModelId"> |
| | | <el-button type="primary" |
| | | @click="handleProcessProductSelectClick"> |
| | | {{ processForm.productName && processForm.model |
| | | ? `${processForm.productName} - ${processForm.model}` |
| | | : 'éæ©äº§å' }} |
| | | </el-button> |
| | | </el-form-item> |
| | | <el-form-item label="åä½" |
| | | prop="unit"> |
| | | <el-input v-model="processForm.unit" |
| | | :placeholder="processForm.productModelId ? 'æ ¹æ®éæ©ç产åèªå¨å¸¦åº' : '请å
éæ©äº§å' " |
| | | clearable |
| | | :disabled="true" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¯å¦è´¨æ£" |
| | | prop="isQuality"> |
| | | <el-switch v-model="processForm.isQuality" |
| | | :active-value="true" |
| | | inactive-value="false" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <el-empty v-else |
| | | description="请ä»å·¦ä¾§éæ©å·¥åº" /> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="selectProcessDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | :disabled="!selectedProcessItem || !processForm.productModelId" |
| | | @click="handleProcessSelectSubmit">ç¡®å®</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- åæ°æ°å¢/ç¼è¾å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="paramDialogVisible" |
| | | :title="isParamEdit ? 'ç¼è¾åæ°' : 'æ°å¢åæ°'" |
| | | width="500px"> |
| | | <el-form :model="paramForm" |
| | | :rules="paramRules" |
| | | ref="paramFormRef" |
| | | label-width="120px"> |
| | | <el-form-item label="åæ°ç¼å·" |
| | | prop="parameterCode"> |
| | | <el-input v-model="paramForm.parameterCode" |
| | | placeholder="请è¾å
¥åæ°ç¼å·" /> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°åç§°" |
| | | prop="parameterName"> |
| | | <el-input v-model="paramForm.parameterName" |
| | | placeholder="请è¾å
¥åæ°åç§°" /> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°æ¨¡å¼" |
| | | prop="parameterType2"> |
| | | <el-select v-model="paramForm.parameterType2" |
| | | placeholder="è¯·éæ©åæ°æ¨¡å¼"> |
| | | <el-option label="åå¼" |
| | | value="1" /> |
| | | <el-option label="åºé´" |
| | | value="2" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°ç±»å" |
| | | prop="parameterType"> |
| | | <el-select v-model="paramForm.parameterType" |
| | | @change="handleParamTypeChange" |
| | | placeholder="è¯·éæ©åæ°ç±»å"> |
| | | <el-option label="æ°å¼æ ¼å¼" |
| | | value="æ°å¼æ ¼å¼" /> |
| | | <el-option label="ææ¬æ ¼å¼" |
| | | value="ææ¬æ ¼å¼" /> |
| | | <el-option label="䏿é项" |
| | | value="䏿é项" /> |
| | | <el-option label="æ¶é´æ ¼å¼" |
| | | value="æ¶é´æ ¼å¼" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item v-if="paramForm.parameterType === '䏿é项'" |
| | | label="æ°æ®åå
¸" |
| | | prop="parameterFormat"> |
| | | <el-select v-model="paramForm.parameterFormat" |
| | | placeholder="è¯·éæ©æ°æ®åå
¸"> |
| | | <el-option v-for="item in dictTypes" |
| | | :key="item.dictType" |
| | | :label="item.dictName" |
| | | :value="item.dictType" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item v-else-if="paramForm.parameterType === 'æ¶é´æ ¼å¼'" |
| | | label="æ¶é´æ ¼å¼" |
| | | prop="parameterFormat"> |
| | | <el-select v-model="paramForm.parameterFormat" |
| | | placeholder="è¯·éæ©æ¶é´æ ¼å¼"> |
| | | <el-option label="YYYY-MM-DD HH:mm:ss" |
| | | value="YYYY-MM-DD HH:mm:ss" /> |
| | | <el-option label="YYYY-MM-DD" |
| | | value="YYYY-MM-DD" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item v-else |
| | | label="åæ°æ ¼å¼" |
| | | prop="parameterFormat"> |
| | | <el-input v-model="paramForm.parameterFormat" |
| | | placeholder="请è¾å
¥åæ°æ ¼å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ åå¼" |
| | | prop="standardValue"> |
| | | <el-input v-model="paramForm.standardValue" |
| | | placeholder="请è¾å
¥æ åå¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="åä½" |
| | | prop="unit"> |
| | | <el-input v-model="paramForm.unit" |
| | | placeholder="请è¾å
¥åä½" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="paramDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="handleParamSubmit">ç¡®å®</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- éæ©åæ°å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="selectParamDialogVisible" |
| | | title="鿩忰" |
| | | width="1000px"> |
| | | <div class="param-select-container"> |
| | | <!-- 左侧忰å表 --> |
| | | <div class="param-list-area"> |
| | | <div class="area-title">å¯éåæ°</div> |
| | | <div class="search-box"> |
| | | <el-input v-model="paramSearchKeyword" |
| | | placeholder="请è¾å
¥åæ°åç§°æç´¢" |
| | | clearable |
| | | size="small" |
| | | @input="handleParamSearch"> |
| | | <template #prefix> |
| | | <el-icon> |
| | | <Search /> |
| | | </el-icon> |
| | | </template> |
| | | </el-input> |
| | | </div> |
| | | <el-table :data="filteredParamList" |
| | | height="300" |
| | | border |
| | | highlight-current-row |
| | | @current-change="handleParamSelect"> |
| | | <el-table-column prop="paramName" |
| | | label="åæ°åç§°" /> |
| | | <el-table-column prop="paramType" |
| | | label="åæ°ç±»å"> |
| | | <template #default="scope"> |
| | | <el-tag size="small" |
| | | :type="getParamTypeTag(scope.row.paramType)"> |
| | | {{ getParamTypeText(scope.row.paramType) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <!-- å页æ§ä»¶ --> |
| | | <div class="pagination-container" |
| | | style="margin-top: 10px;"> |
| | | <el-pagination v-model:current-page="paramPage.current" |
| | | v-model:page-size="paramPage.size" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :total="paramPage.total" |
| | | @size-change="handleParamSizeChange" |
| | | @current-change="handleParamCurrentChange" |
| | | size="small" /> |
| | | </div> |
| | | </div> |
| | | <!-- å³ä¾§åæ°è¯¦æ
--> |
| | | <div class="param-detail-area"> |
| | | <div class="area-title">åæ°è¯¦æ
</div> |
| | | <el-form v-if="selectedParam" |
| | | :model="selectedParam" |
| | | label-width="100px" |
| | | class="param-detail-form"> |
| | | <el-form-item label="åæ°åç§°"> |
| | | <span class="detail-text">{{ selectedParam.paramName }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°æ¨¡å¼"> |
| | | <el-tag size="small" |
| | | :type="selectedParam.valueMode == '1' ? 'success' : 'warning'"> |
| | | {{ selectedParam.valueMode == '1' ? 'åå¼' : 'åºé´' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°ç±»å"> |
| | | <el-tag size="small" |
| | | :type="getParamTypeTag(selectedParam.paramType)"> |
| | | {{ getParamTypeText(selectedParam.paramType) }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°æ ¼å¼"> |
| | | <span class="detail-text">{{ selectedParam.paramFormat || '-' }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="åä½"> |
| | | <span class="detail-text">{{ selectedParam.unit || '-' }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="æ åå¼" |
| | | v-if="selectedParam.valueMode == '1' && selectedParam.paramType == '1'"> |
| | | <el-input v-model="selectedParam.standardValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥é»è®¤å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æå°å¼" |
| | | v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'"> |
| | | <el-input v-model="selectedParam.minValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æå°å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æå¤§å¼" |
| | | v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'"> |
| | | <el-input v-model="selectedParam.maxValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æå¤§å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æåº"> |
| | | <el-input v-model="selectedParam.sort" |
| | | type="number" |
| | | placeholder="请è¾å
¥æåº" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¯å¦å¿
å¡«"> |
| | | <el-switch v-model="selectedParam.isRequired" |
| | | :active-value="1" |
| | | :inactive-value="0" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <el-empty v-else |
| | | description="请ä»å·¦ä¾§éæ©åæ°" /> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="selectParamDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | :disabled="!selectedParam" |
| | | @click="handleParamSelectSubmit">ç¡®å®</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- ç¼è¾åæ°å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="editParamDialogVisible" |
| | | title="ç¼è¾åæ°" |
| | | width="600px"> |
| | | <el-form :model="editParamForm" |
| | | :rules="editParamRules" |
| | | ref="editParamFormRef" |
| | | label-width="120px"> |
| | | <el-form-item label="åæ°åç§°"> |
| | | <span class="detail-text">{{ editParamForm.paramName }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°æ¨¡å¼"> |
| | | <el-tag size="small" |
| | | :type="editParamForm.valueMode == '1' ? 'success' : 'warning'"> |
| | | {{ editParamForm.valueMode == '1' ? 'åå¼' : 'åºé´' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°ç±»å"> |
| | | <el-tag size="small" |
| | | :type="getParamTypeTag(editParamForm.paramType)"> |
| | | {{ getParamTypeText(editParamForm.paramType) }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | <el-form-item label="åæ°æ ¼å¼"> |
| | | <span class="detail-text">{{ editParamForm.paramFormat || '-' }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="åä½"> |
| | | <span class="detail-text">{{ editParamForm.unit || '-' }}</span> |
| | | </el-form-item> |
| | | <el-form-item label="æ åå¼" |
| | | v-if="editParamForm.valueMode == '1' && editParamForm.paramType == '1'" |
| | | prop="standardValue"> |
| | | <el-input v-model="editParamForm.standardValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æ åå¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æå°å¼" |
| | | v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'" |
| | | prop="minValue"> |
| | | <el-input v-model="editParamForm.minValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æå°å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æå¤§å¼" |
| | | v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'" |
| | | prop="maxValue"> |
| | | <el-input v-model="editParamForm.maxValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æå¤§å¼" /> |
| | | </el-form-item> |
| | | <el-form-item label="æåº" |
| | | prop="sort"> |
| | | <el-input v-model="editParamForm.sort" |
| | | type="number" |
| | | placeholder="请è¾å
¥æåº" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¯å¦å¿
å¡«" |
| | | prop="isRequired"> |
| | | <el-switch v-model="editParamForm.isRequired" |
| | | :active-value="1" |
| | | :inactive-value="0" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="editParamDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="handleEditParamSubmit">ç¡®å®</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, getCurrentInstance, onMounted } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { |
| | | Plus, |
| | | Edit, |
| | | Delete, |
| | | ArrowUp, |
| | | ArrowDown, |
| | | Right, |
| | | Search, |
| | | Check, |
| | | Close, |
| | | Box, |
| | | Document, |
| | | } from "@element-plus/icons-vue"; |
| | | import { listType } from "@/api/system/dict/type"; |
| | | import { getByModel } from "@/api/productionManagement/productBom.js"; |
| | | import { add, update, del } from "@/api/productionManagement/processRoute.js"; |
| | | import { |
| | | addOrUpdateProcessRouteItem, |
| | | batchDeleteProcessRouteItem, |
| | | sortProcessRouteItem, |
| | | findProcessRouteItemList, |
| | | getProcessParamList, |
| | | addProcessRouteItemParam, |
| | | editProcessRouteItemParam, |
| | | delProcessRouteItemParam, |
| | | } from "@/api/productionManagement/processRouteItem.js"; |
| | | import { list as getProcessListApi } from "@/api/productionManagement/productionProcess.js"; |
| | | import { getBaseParamList } from "@/api/basicData/parameterMaintenance.js"; |
| | | import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; |
| | | |
| | | // å·¥èºè·¯çº¿å表 |
| | | const routeList = ref([]); |
| | | const dictTypes = ref([]); |
| | | |
| | | // å·¥èºè·¯çº¿å页 |
| | | const routePage = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | |
| | | // è·åå
¨å±å®ä¾ |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | // 产åéæ©åBOMç¸å
³ |
| | | const showProductSelectDialog = ref(false); |
| | | const bomOptions = ref([]); |
| | | |
| | | // å·¥èºè·¯çº¿å¯¹è¯æ¡ |
| | | const routeDialogVisible = ref(false); |
| | | const isRouteEdit = ref(false); |
| | | const routeFormRef = ref(null); |
| | | const routeForm = reactive({ |
| | | id: null, |
| | | productModelId: null, |
| | | productName: "", |
| | | productModelName: "", |
| | | bomId: null, |
| | | routeCode: "", |
| | | description: "", |
| | | status: true, |
| | | }); |
| | | const routeRules = { |
| | | productModelId: [ |
| | | { required: true, message: "è¯·éæ©äº§å", trigger: "change" }, |
| | | ], |
| | | bomId: [{ required: true, message: "è¯·éæ©BOM", trigger: "change" }], |
| | | }; |
| | | |
| | | // å·¥åºå¯¹è¯æ¡ |
| | | const processDialogVisible = ref(false); |
| | | const isProcessEdit = ref(false); |
| | | const processFormRef = ref(null); |
| | | const currentRouteId = ref(null); |
| | | const processForm = reactive({ |
| | | id: null, |
| | | no: "", |
| | | name: "", |
| | | remark: "", |
| | | status: true, |
| | | }); |
| | | const processRules = { |
| | | no: [{ required: true, message: "请è¾å
¥å·¥åºç¼ç ", trigger: "blur" }], |
| | | name: [{ required: true, message: "请è¾å
¥å·¥åºåç§°", trigger: "blur" }], |
| | | }; |
| | | |
| | | // 鿩工åºå¯¹è¯æ¡ |
| | | const selectProcessDialogVisible = ref(false); |
| | | const availableProcessList = ref([]); |
| | | const filteredProcessList = ref([]); |
| | | const selectedProcessItem = ref(null); |
| | | const processSearchKeyword = ref(""); |
| | | const currentRouteIndex = ref(null); |
| | | |
| | | // åæ°å¯¹è¯æ¡ |
| | | const paramDialogVisible = ref(false); |
| | | const isParamEdit = ref(false); |
| | | const paramFormRef = ref(null); |
| | | const currentProcessId = ref(null); |
| | | const paramForm = reactive({ |
| | | id: null, |
| | | parameterCode: "", |
| | | parameterName: "", |
| | | parameterType2: "1", |
| | | parameterType: "", |
| | | parameterFormat: "", |
| | | standardValue: "", |
| | | unit: "", |
| | | }); |
| | | const paramRules = { |
| | | parameterCode: [ |
| | | { required: true, message: "请è¾å
¥åæ°ç¼å·", trigger: "blur" }, |
| | | ], |
| | | parameterName: [ |
| | | { required: true, message: "请è¾å
¥åæ°åç§°", trigger: "blur" }, |
| | | ], |
| | | parameterType: [ |
| | | { required: true, message: "è¯·éæ©åæ°ç±»å", trigger: "change" }, |
| | | ], |
| | | }; |
| | | |
| | | // éæ©åæ°å¯¹è¯æ¡ |
| | | const selectParamDialogVisible = ref(false); |
| | | const availableParamList = ref([]); |
| | | const filteredParamList = ref([]); |
| | | const selectedParam = ref(null); |
| | | const paramSearchKeyword = ref(""); |
| | | |
| | | // å¯éåæ°å页 |
| | | const paramPage = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | |
| | | // ç¼è¾åæ°å¯¹è¯æ¡ |
| | | const editParamDialogVisible = ref(false); |
| | | const editParamFormRef = ref(null); |
| | | const editParamForm = reactive({ |
| | | id: null, |
| | | processId: null, |
| | | paramId: null, |
| | | paramName: "", |
| | | valueMode: "1", |
| | | standardValue: null, |
| | | minValue: null, |
| | | maxValue: null, |
| | | sort: 1, |
| | | isRequired: 0, |
| | | }); |
| | | const editParamRules = reactive({ |
| | | standardValue: [ |
| | | { |
| | | required: true, |
| | | message: "请è¾å
¥æ åå¼", |
| | | trigger: "blur", |
| | | validator: (rule, value, callback) => { |
| | | if (value === null || value === undefined || value === "") { |
| | | callback(new Error("请è¾å
¥æ åå¼")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }, |
| | | }, |
| | | ], |
| | | minValue: [ |
| | | { |
| | | required: true, |
| | | message: "请è¾å
¥æå°å¼", |
| | | trigger: "blur", |
| | | validator: (rule, value, callback) => { |
| | | if (value === null || value === undefined || value === "") { |
| | | callback(new Error("请è¾å
¥æå°å¼")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }, |
| | | }, |
| | | ], |
| | | maxValue: [ |
| | | { |
| | | required: true, |
| | | message: "请è¾å
¥æå¤§å¼", |
| | | trigger: "blur", |
| | | validator: (rule, value, callback) => { |
| | | if (value === null || value === undefined || value === "") { |
| | | callback(new Error("请è¾å
¥æå¤§å¼")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }, |
| | | }, |
| | | ], |
| | | sort: [ |
| | | { |
| | | required: true, |
| | | message: "请è¾å
¥æåº", |
| | | trigger: "blur", |
| | | validator: (rule, value, callback) => { |
| | | if (value === null || value === undefined || value === "") { |
| | | callback(new Error("请è¾å
¥æåº")); |
| | | } else if (isNaN(value) || value < 1) { |
| | | callback(new Error("æåºå¿
é¡»æ¯å¤§äº0çæ´æ°")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }, |
| | | }, |
| | | ], |
| | | }); |
| | | |
| | | // ææ½ç¸å
³ |
| | | const draggedItem = ref(null); |
| | | const draggedRouteId = ref(null); |
| | | |
| | | // è·åå·¥èºè·¯çº¿å表 |
| | | const getRouteList = () => { |
| | | // 导å
¥ listPage æ¹æ³ |
| | | import("@/api/productionManagement/processRoute.js").then(({ listPage }) => { |
| | | listPage({ pageNum: routePage.current, pageSize: routePage.size }) |
| | | .then(res => { |
| | | // å¤çè¿åçæ°æ®ï¼æ å°å°é¡µé¢éè¦çæ ¼å¼ |
| | | routeList.value = (res.data?.records || []).map(item => ({ |
| | | id: item.id, |
| | | productModelId: item.productModelId, |
| | | productName: item.productName, |
| | | productModelName: item.model || item.productModelName, |
| | | bomId: item.bomId, |
| | | bomNo: item.bomNo, |
| | | routeCode: item.processRouteCode || item.routeCode, |
| | | description: item.description || item.description, |
| | | status: item.status, |
| | | expanded: false, |
| | | processList: (item.processList || []).map(process => ({ |
| | | ...process, |
| | | processId: process.processId || process.id, |
| | | expanded: false, |
| | | })), |
| | | })); |
| | | // æ´æ°åé¡µæ»æ° |
| | | routePage.total = res.data?.total || 0; |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åå·¥èºè·¯çº¿å表失败ï¼", err); |
| | | routeList.value = []; |
| | | routePage.total = 0; |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | // å±å¼/æ¶èµ·å·¥èºè·¯çº¿ |
| | | const toggleExpand = route => { |
| | | route.expanded = !route.expanded; |
| | | if (route.expanded) { |
| | | // è°ç¨æ¥å£è·åå·¥åºå表 |
| | | findProcessRouteItemList({ routeId: route.id }) |
| | | .then(res => { |
| | | route.processList = (res.data || []).map(process => ({ |
| | | ...process, |
| | | processId: process.processId || process.id, |
| | | expanded: false, |
| | | })); |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åå·¥åºå表失败ï¼", err); |
| | | route.processList = []; |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // å±å¼/æ¶èµ·å·¥åºåæ° |
| | | const toggleProcessParams = process => { |
| | | process.expanded = !process.expanded; |
| | | if (process.expanded && process.id) { |
| | | // è°ç¨æ¥å£è·ååæ°å表 |
| | | getProcessParamList({ |
| | | routeItemId: process.id, |
| | | pageNum: 1, |
| | | pageSize: 1000, |
| | | }) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | process.paramList = res.data?.records || []; |
| | | process.paramCount = process.paramList.length; |
| | | } else { |
| | | ElMessage.error(res.msg || "è·ååæ°å表失败"); |
| | | process.paramList = []; |
| | | process.paramCount = 0; |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·ååæ°å表失败ï¼", err); |
| | | ElMessage.error("è·ååæ°å表失败"); |
| | | process.paramList = []; |
| | | process.paramCount = 0; |
| | | }); |
| | | } |
| | | }; |
| | | const toggleProcessParams2 = process => { |
| | | if (process.expanded && process.id) { |
| | | // è°ç¨æ¥å£è·ååæ°å表 |
| | | getProcessParamList({ |
| | | routeItemId: process.id, |
| | | pageNum: 1, |
| | | pageSize: 1000, |
| | | }) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | process.paramList = res.data?.records || []; |
| | | process.paramCount = process.paramList.length; |
| | | } else { |
| | | ElMessage.error(res.msg || "è·ååæ°å表失败"); |
| | | process.paramList = []; |
| | | process.paramCount = 0; |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·ååæ°å表失败ï¼", err); |
| | | ElMessage.error("è·ååæ°å表失败"); |
| | | process.paramList = []; |
| | | process.paramCount = 0; |
| | | }); |
| | | } |
| | | }; |
| | | // å·¥èºè·¯çº¿æä½ |
| | | const handleAddRoute = () => { |
| | | isRouteEdit.value = false; |
| | | routeForm.id = null; |
| | | routeForm.productModelId = null; |
| | | routeForm.productName = ""; |
| | | routeForm.productModelName = ""; |
| | | routeForm.bomId = null; |
| | | routeForm.routeCode = ""; |
| | | routeForm.description = ""; |
| | | routeForm.status = false; |
| | | bomOptions.value = []; |
| | | routeDialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleEditRoute = route => { |
| | | isRouteEdit.value = true; |
| | | routeForm.id = route.id; |
| | | routeForm.productModelId = route.productModelId; |
| | | routeForm.productName = route.productName; |
| | | routeForm.productModelName = route.productModelName; |
| | | routeForm.bomId = route.bomId; |
| | | routeForm.routeCode = route.routeCode; |
| | | routeForm.description = route.description; |
| | | routeForm.status = route.status; |
| | | routeDialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleDeleteRoute = route => { |
| | | ElMessageBox.confirm("ç¡®å®è¦å é¤è¯¥å·¥èºè·¯çº¿åï¼", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }).then(() => { |
| | | del(route.id) |
| | | .then(res => { |
| | | ElMessage.success("å 餿å"); |
| | | getRouteList(); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("å é¤å¤±è´¥"); |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const handleRouteSubmit = () => { |
| | | routeFormRef.value.validate(valid => { |
| | | if (valid) { |
| | | // æå»ºæäº¤æ°æ® |
| | | const submitData = { |
| | | ...routeForm, |
| | | // 注æï¼API ææçåæ®µåå¯è½ä¸è¡¨ååæ®µåä¸å |
| | | productId: routeForm.productModelId, |
| | | productModelId: routeForm.productModelId, |
| | | description: routeForm.description, |
| | | }; |
| | | |
| | | if (isRouteEdit.value) { |
| | | // ç¼è¾æä½ |
| | | update(submitData) |
| | | .then(res => { |
| | | ElMessage.success("ç¼è¾æå"); |
| | | routeDialogVisible.value = false; |
| | | getRouteList(); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("ç¼è¾å¤±è´¥"); |
| | | }); |
| | | } else { |
| | | // æ°å¢æä½ |
| | | add(submitData) |
| | | .then(res => { |
| | | ElMessage.success("æ°å¢æå"); |
| | | routeDialogVisible.value = false; |
| | | getRouteList(); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("æ°å¢å¤±è´¥"); |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | const isform2 = ref(null); |
| | | const handleProcessProductSelectClick = () => { |
| | | isform2.value = true; |
| | | showProductSelectDialog.value = true; |
| | | }; |
| | | const handleProcessProductSelectClick2 = () => { |
| | | isform2.value = false; |
| | | showProductSelectDialog.value = true; |
| | | }; |
| | | |
| | | // 产åéæ©å¤ç |
| | | const handleProductSelect = async products => { |
| | | if (isform2.value) { |
| | | // 帮æåå·¥åºä¸çéæ©äº§åçåè°,并䏿忮µå è¿processForm |
| | | if (products && products.length > 0) { |
| | | const product = products[0]; |
| | | console.log("product:", product); |
| | | // æproductä¸çåæ®µæ·»å å°processFormä¸ |
| | | // Object.assign(processForm, product); |
| | | processForm.productModelId = product.id; |
| | | processForm.productName = product.productName; |
| | | processForm.model = product.model; |
| | | processForm.unit = product.unit || ""; |
| | | console.log("processForm:", processForm); |
| | | |
| | | // 触å表åéªè¯æ´æ° |
| | | proxy.$refs["processFormRef"]?.validateField("productModelId"); |
| | | } |
| | | } else { |
| | | if (products && products.length > 0) { |
| | | const product = products[0]; |
| | | // å
æ¥è¯¢BOMå表ï¼å¿
éï¼ |
| | | try { |
| | | const res = await getByModel(product.id); |
| | | // å¤çè¿åçBOMæ°æ®ï¼å¯è½æ¯æ°ç»ã对象æå
å«dataåæ®µ |
| | | let bomList = []; |
| | | if (Array.isArray(res)) { |
| | | bomList = res; |
| | | } else if (res && res.data) { |
| | | bomList = Array.isArray(res.data) ? res.data : [res.data]; |
| | | } else if (res && typeof res === "object") { |
| | | bomList = [res]; |
| | | } |
| | | console.log("bomList:", bomList); |
| | | if (bomList.length > 0) { |
| | | routeForm.productModelId = product.id; |
| | | routeForm.productName = product.productName; |
| | | routeForm.productModelName = product.model; |
| | | routeForm.bomId = undefined; // éç½®BOMéæ© |
| | | bomOptions.value = bomList; |
| | | showProductSelectDialog.value = false; |
| | | // 触å表åéªè¯æ´æ° |
| | | proxy.$refs["routeFormRef"]?.validateField("productModelId"); |
| | | } else { |
| | | proxy.$modal.msgError("è¯¥äº§åæ²¡æBOMï¼è¯·å
å建BOM"); |
| | | } |
| | | } catch (error) { |
| | | // 妿æ¥å£è¿å404æå
¶ä»é误ï¼è¯´ææ²¡æBOM |
| | | proxy.$modal.msgError("è¯¥äº§åæ²¡æBOMï¼è¯·å
å建BOM"); |
| | | } |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const handleApproveRoute = route => { |
| | | ElMessageBox.confirm("ç¡®å®è¦æ¹å该工èºè·¯çº¿åï¼", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "info", |
| | | }).then(() => { |
| | | // è°ç¨ä¿®æ¹æ¥å£ï¼åªä¿®æ¹statusåæ®µä¸ºæ¹åç¶æ |
| | | update({ id: route.id, status: true }) |
| | | .then(res => { |
| | | ElMessage.success("æ¹åæå"); |
| | | getRouteList(); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("æ¹å失败"); |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const handleRevokeApproveRoute = route => { |
| | | ElMessageBox.confirm("ç¡®å®è¦æ¤éæ¹å该工èºè·¯çº¿åï¼", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }).then(() => { |
| | | // è°ç¨ä¿®æ¹æ¥å£ï¼åªä¿®æ¹statusåæ®µä¸ºèç¨¿ç¶æ |
| | | update({ id: route.id, status: false }) |
| | | .then(res => { |
| | | ElMessage.success("æ¤éæ¹åæå"); |
| | | getRouteList(); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("æ¤éæ¹å失败"); |
| | | }); |
| | | }); |
| | | }; |
| | | // å·¥åºæä½ |
| | | const handleSelectProcess = (route, index) => { |
| | | console.log("route:", route); |
| | | currentRouteId.value = route.id; |
| | | currentRouteIndex.value = index; |
| | | // éç½®æç´¢åéæ©ç¶æ |
| | | filteredProcessList.value = availableProcessList.value; |
| | | processSearchKeyword.value = ""; |
| | | selectedProcessItem.value = null; |
| | | selectProcessDialogVisible.value = true; |
| | | }; |
| | | const dragSort = ref(0); |
| | | const currentId = ref(null); |
| | | // ä¿®æ¹å·¥åº |
| | | const handleEditProcessSelect = (route, index, process) => { |
| | | console.log("route:", route); |
| | | console.log("process:", process); |
| | | currentId.value = process.id; |
| | | currentRouteId.value = route.id; |
| | | currentRouteIndex.value = index; |
| | | // éç½®æç´¢åéæ©ç¶æ |
| | | filteredProcessList.value = availableProcessList.value; |
| | | processSearchKeyword.value = ""; |
| | | // 设置éä¸çå·¥åº |
| | | filteredProcessList.value.map(item => { |
| | | if (item.id === process.processId) { |
| | | selectedProcessItem.value = item; |
| | | } |
| | | }); |
| | | dragSort.value = process.dragSort; |
| | | // selectedProcessItem.value = process; |
| | | // å¡«å
产åéæ©è¡¨å |
| | | processForm.productModelId = process.productModelId; |
| | | processForm.productName = process.productName; |
| | | processForm.model = process.model; |
| | | processForm.processId = process.no; |
| | | // processForm.name = process.name; |
| | | processForm.unit = process.unit || ""; |
| | | processForm.isQuality = process.isQuality || false; |
| | | selectProcessDialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleEditProcess = (routeId, process) => { |
| | | currentRouteId.value = routeId; |
| | | isProcessEdit.value = true; |
| | | processForm.id = process.id; |
| | | processForm.no = process.no; |
| | | processForm.name = process.name; |
| | | processForm.remark = process.remark; |
| | | processForm.status = process.status; |
| | | processDialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleDeleteProcess = (routeId, process) => { |
| | | ElMessageBox.confirm("ç¡®å®è¦å é¤è¯¥å·¥åºåï¼", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }).then(() => { |
| | | // è°ç¨APIå é¤å·¥åº |
| | | batchDeleteProcessRouteItem([process.id]) |
| | | .then(res => { |
| | | ElMessage.success("å 餿å"); |
| | | // è°ç¨æ¥å£æ´æ°å·¥åºå表 |
| | | findProcessRouteItemList({ routeId: routeId }) |
| | | .then(res => { |
| | | const route = routeList.value.find(r => r.id === routeId); |
| | | if (route) { |
| | | route.processList = (res.data || []).map(process => ({ |
| | | ...process, |
| | | processId: process.processId || process.id, |
| | | expanded: false, |
| | | })); |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åå·¥åºå表失败ï¼", err); |
| | | }); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("å é¤å¤±è´¥"); |
| | | console.error("å é¤å·¥åºå¤±è´¥ï¼", err); |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const handleProcessSubmit = () => { |
| | | processFormRef.value.validate(valid => { |
| | | if (valid) { |
| | | ElMessage.success(isProcessEdit.value ? "ç¼è¾æå" : "æ°å¢æå"); |
| | | processDialogVisible.value = false; |
| | | // è°ç¨æ¥å£æ´æ°å·¥åºå表 |
| | | if (currentRouteId.value) { |
| | | findProcessRouteItemList({ routeId: currentRouteId.value }) |
| | | .then(res => { |
| | | const route = routeList.value.find( |
| | | r => r.id === currentRouteId.value |
| | | ); |
| | | if (route) { |
| | | route.processList = (res.data || []).map(process => ({ |
| | | ...process, |
| | | processId: process.processId || process.id, |
| | | expanded: false, |
| | | })); |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åå·¥åºå表失败ï¼", err); |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // 鿩工åºç¸å
³æ¹æ³ |
| | | const handleProcessSearch = () => { |
| | | const keyword = processSearchKeyword.value.trim().toLowerCase(); |
| | | if (!keyword) { |
| | | filteredProcessList.value = availableProcessList.value; |
| | | } else { |
| | | filteredProcessList.value = availableProcessList.value.filter( |
| | | item => |
| | | (item.name && item.name.toLowerCase().includes(keyword)) || |
| | | (item.no && item.no.toLowerCase().includes(keyword)) |
| | | ); |
| | | } |
| | | }; |
| | | |
| | | const handleProcessSelect = row => { |
| | | selectedProcessItem.value = row; |
| | | // é置产åéæ©è¡¨å |
| | | processForm.productModelId = undefined; |
| | | processForm.productName = ""; |
| | | processForm.productModelName = ""; |
| | | processForm.unit = ""; |
| | | processForm.isQuality = row.isQuality || false; |
| | | }; |
| | | |
| | | // å¤çå·¥åºéæ©æ¶ç产åéæ© |
| | | const handleProcessProductSelect = async products => { |
| | | if (products && products.length > 0) { |
| | | const product = products[0]; |
| | | processForm.productModelId = product.id; |
| | | processForm.productName = product.productName; |
| | | processForm.productModelName = product.model; |
| | | processForm.unit = product.unit || ""; |
| | | showProductSelectDialog.value = false; |
| | | } |
| | | }; |
| | | |
| | | const handleProcessSelectSubmit = () => { |
| | | if (!selectedProcessItem.value) { |
| | | ElMessage.warning("请å
éæ©ä¸ä¸ªå·¥åº"); |
| | | return; |
| | | } |
| | | |
| | | if (!processForm.productModelId) { |
| | | ElMessage.warning("è¯·éæ©äº§å"); |
| | | return; |
| | | } |
| | | |
| | | // æå»ºè¯·æ±åæ° |
| | | const params = { |
| | | routeId: currentRouteId.value, |
| | | processId: selectedProcessItem.value.id, |
| | | dragSort: routePage.total + 1, |
| | | ...processForm, |
| | | }; |
| | | |
| | | // 妿æ¯ä¿®æ¹æä½ï¼æ·»å idåæ° |
| | | if (selectedProcessItem.value.id) { |
| | | params.id = currentId.value; |
| | | params.dragSort = dragSort.value; |
| | | } |
| | | |
| | | // è°ç¨APIæ·»å å·¥åºæä¿®æ¹å·¥åº |
| | | addOrUpdateProcessRouteItem(params) |
| | | .then(res => { |
| | | ElMessage.success( |
| | | selectedProcessItem.value.id ? "ä¿®æ¹å·¥åºæå" : "æ·»å å·¥åºæå" |
| | | ); |
| | | selectProcessDialogVisible.value = false; |
| | | // è°ç¨æ¥å£æ´æ°å·¥åºå表 |
| | | findProcessRouteItemList({ routeId: currentRouteId.value }) |
| | | .then(res => { |
| | | const route = routeList.value.find( |
| | | r => r.id === currentRouteId.value |
| | | ); |
| | | if (route) { |
| | | route.processList = (res.data || []).map(process => ({ |
| | | ...process, |
| | | processId: process.processId || process.id, |
| | | expanded: false, |
| | | })); |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åå·¥åºå表失败ï¼", err); |
| | | }); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error( |
| | | selectedProcessItem.value.id ? "ä¿®æ¹å·¥åºå¤±è´¥" : "æ·»å å·¥åºå¤±è´¥" |
| | | ); |
| | | console.error( |
| | | selectedProcessItem.value.id ? "ä¿®æ¹å·¥åºå¤±è´¥ï¼" : "æ·»å å·¥åºå¤±è´¥ï¼", |
| | | err |
| | | ); |
| | | }); |
| | | }; |
| | | |
| | | // åæ°æä½ |
| | | const handleAddParam = (routeId, process) => { |
| | | currentRouteId.value = routeId; |
| | | currentProcessId.value = process.id; |
| | | selectedParam.value = null; |
| | | paramSearchKeyword.value = ""; |
| | | paramPage.current = 1; |
| | | // è·åå¯éåæ°å表 |
| | | getBaseParamList({ |
| | | paramName: paramSearchKeyword.value, |
| | | current: paramPage.current, |
| | | size: paramPage.size, |
| | | }).then(res => { |
| | | if (res.code === 200) { |
| | | filteredParamList.value = res.data?.records || []; |
| | | paramPage.total = res.data?.total || 0; |
| | | } else { |
| | | ElMessage.error(res.msg || "æ¥è¯¢å¤±è´¥"); |
| | | } |
| | | }); |
| | | selectParamDialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleEditParam = (routeId, process, param) => { |
| | | currentRouteId.value = routeId; |
| | | currentProcessId.value = process.id; |
| | | editParamForm.id = param.id; |
| | | editParamForm.processId = process.id; |
| | | editParamForm.paramId = param.paramId; |
| | | editParamForm.paramName = param.parameterName || param.paramName; |
| | | editParamForm.valueMode = param.parameterType2 || param.valueMode || "1"; |
| | | editParamForm.standardValue = param.standardValue; |
| | | editParamForm.minValue = param.minValue; |
| | | editParamForm.maxValue = param.maxValue; |
| | | editParamForm.sort = param.sort || 1; |
| | | editParamForm.isRequired = param.isRequired || 0; |
| | | editParamForm.paramType = param.parameterType || param.paramType; |
| | | editParamForm.paramFormat = param.parameterFormat || param.paramFormat; |
| | | editParamForm.unit = param.unit || param.unit; |
| | | editParamDialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleDeleteParam = (routeId, process, param) => { |
| | | ElMessageBox.confirm("ç¡®å®è¦å é¤è¯¥åæ°åï¼", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }).then(() => { |
| | | // è°ç¨APIå é¤åæ° |
| | | delProcessRouteItemParam(param.id) |
| | | .then(res => { |
| | | ElMessage.success("å 餿å"); |
| | | // å·æ°åæ°å表 |
| | | toggleProcessParams2(process); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("å é¤åæ°å¤±è´¥"); |
| | | console.error("å é¤åæ°å¤±è´¥ï¼", err); |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const handleParamSubmit = () => { |
| | | paramFormRef.value.validate(valid => { |
| | | if (valid) { |
| | | ElMessage.success(isParamEdit.value ? "ç¼è¾æå" : "æ°å¢æå"); |
| | | paramDialogVisible.value = false; |
| | | getRouteList(); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const handleParamTypeChange = () => { |
| | | if (paramForm.parameterType === "æ°å¼æ ¼å¼") { |
| | | paramForm.parameterFormat = "#.0000"; |
| | | } else if (paramForm.parameterType === "æ¶é´æ ¼å¼") { |
| | | paramForm.parameterFormat = "YYYY-MM-DD HH:mm:ss"; |
| | | } else { |
| | | paramForm.parameterFormat = ""; |
| | | } |
| | | }; |
| | | |
| | | const getParamTypeTag = type => { |
| | | const typeMap = { |
| | | 1: "primary", |
| | | 2: "info", |
| | | 3: "warning", |
| | | 4: "success", |
| | | }; |
| | | return typeMap[type] || "default"; |
| | | }; |
| | | |
| | | const getParamTypeText = type => { |
| | | const typeMap = { |
| | | 1: "æ°å¼æ ¼å¼", |
| | | 2: "ææ¬æ ¼å¼", |
| | | 3: "䏿é项", |
| | | 4: "æ¶é´æ ¼å¼", |
| | | }; |
| | | return typeMap[type] || "æªç¥åæ°ç±»å"; |
| | | }; |
| | | |
| | | // 鿩忰ç¸å
³æ¹æ³ |
| | | const handleParamSearch = () => { |
| | | // éç½®å页 |
| | | paramPage.current = 1; |
| | | // éæ°å è½½æ°æ® |
| | | getBaseParamList({ |
| | | paramName: paramSearchKeyword.value, |
| | | current: paramPage.current, |
| | | size: paramPage.size, |
| | | }).then(res => { |
| | | if (res.code === 200) { |
| | | filteredParamList.value = res.data?.records || []; |
| | | paramPage.total = res.data?.total || 0; |
| | | } else { |
| | | ElMessage.error(res.msg || "æ¥è¯¢å¤±è´¥"); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const handleParamSelect = row => { |
| | | selectedParam.value = row; |
| | | }; |
| | | |
| | | // å¤çå页大å°åå |
| | | const handleParamSizeChange = size => { |
| | | paramPage.size = size; |
| | | getBaseParamList({ |
| | | paramName: paramSearchKeyword.value, |
| | | current: paramPage.current, |
| | | size: paramPage.size, |
| | | }).then(res => { |
| | | if (res.code === 200) { |
| | | filteredParamList.value = res.data?.records || []; |
| | | paramPage.total = res.data?.total || 0; |
| | | } else { |
| | | ElMessage.error(res.msg || "æ¥è¯¢å¤±è´¥"); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // å¤çå½å页ç åå |
| | | const handleParamCurrentChange = current => { |
| | | paramPage.current = current; |
| | | getBaseParamList({ |
| | | paramName: paramSearchKeyword.value, |
| | | current: paramPage.current, |
| | | size: paramPage.size, |
| | | }).then(res => { |
| | | if (res.code === 200) { |
| | | filteredParamList.value = res.data?.records || []; |
| | | paramPage.total = res.data?.total || 0; |
| | | } else { |
| | | ElMessage.error(res.msg || "æ¥è¯¢å¤±è´¥"); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // å·¥èºè·¯çº¿å页å¤ç |
| | | const handleRouteSizeChange = size => { |
| | | routePage.size = size; |
| | | getRouteList(); |
| | | }; |
| | | |
| | | const handleRouteCurrentChange = current => { |
| | | routePage.current = current; |
| | | getRouteList(); |
| | | }; |
| | | |
| | | const handleParamSelectSubmit = () => { |
| | | if (!selectedParam.value) { |
| | | ElMessage.warning("请å
éæ©ä¸ä¸ªåæ°"); |
| | | return; |
| | | } |
| | | |
| | | // æ¾å°å¯¹åºçå·¥èºè·¯çº¿åå·¥åº |
| | | const route = routeList.value.find(r => r.id === currentRouteId.value); |
| | | const process = route?.processList.find(p => p.id === currentProcessId.value); |
| | | |
| | | if (route && process) { |
| | | // æ£æ¥åæ°æ¯å¦å·²åå¨ |
| | | // const exists = process.paramList?.some( |
| | | // p => |
| | | // p.paramId === selectedParam.value.id || |
| | | // p.parameterCode === selectedParam.value.paramCode |
| | | // ); |
| | | // if (exists) { |
| | | // ElMessage.warning("è¯¥åæ°å·²åå¨äºå·¥åºä¸"); |
| | | // return; |
| | | // } |
| | | |
| | | // å¤æåæ°ç±»åï¼åªææ°å¼ç±»åæä¼ æ åå¼ãæå¤§å¼åæå°å¼ |
| | | const isNumericMode = selectedParam.value.valueMode === 1; |
| | | |
| | | // è°ç¨APIæ°å¢åæ° |
| | | addProcessRouteItemParam({ |
| | | routeItemId: process.id, |
| | | paramId: selectedParam.value.id, |
| | | standardValue: isNumericMode |
| | | ? selectedParam.value.standardValue || "" |
| | | : "", |
| | | minValue: isNumericMode ? selectedParam.value.minValue || 0 : null, |
| | | maxValue: isNumericMode ? selectedParam.value.maxValue || 0 : null, |
| | | isRequired: selectedParam.value.isRequired || 0, |
| | | }) |
| | | .then(res => { |
| | | ElMessage.success("æ·»å åæ°æå"); |
| | | selectParamDialogVisible.value = false; |
| | | // å·æ°åæ°å表 |
| | | toggleProcessParams2(process); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("æ·»å åæ°å¤±è´¥"); |
| | | console.error("æ·»å åæ°å¤±è´¥ï¼", err); |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | const handleEditParamSubmit = () => { |
| | | editParamFormRef.value.validate(valid => { |
| | | if (valid) { |
| | | // å¤æåæ°ç±»åï¼åªææ°å¼ç±»åæä¼ æ åå¼ãæå¤§å¼åæå°å¼ |
| | | const isNumericMode = editParamForm.valueMode == 1; |
| | | |
| | | // è°ç¨APIä¿®æ¹åæ° |
| | | editProcessRouteItemParam({ |
| | | id: editParamForm.id, |
| | | routeItemId: currentProcessId.value, |
| | | paramId: editParamForm.paramId, |
| | | standardValue: isNumericMode ? editParamForm.standardValue || "" : "", |
| | | minValue: isNumericMode ? editParamForm.minValue || 0 : null, |
| | | maxValue: isNumericMode ? editParamForm.maxValue || 0 : null, |
| | | isRequired: editParamForm.isRequired || 0, |
| | | }) |
| | | .then(res => { |
| | | ElMessage.success("ç¼è¾æå"); |
| | | editParamDialogVisible.value = false; |
| | | // æ¾å°å¯¹åºçå·¥èºè·¯çº¿åå·¥åº |
| | | const route = routeList.value.find( |
| | | r => r.id === currentRouteId.value |
| | | ); |
| | | const process = route?.processList.find( |
| | | p => p.id === currentProcessId.value |
| | | ); |
| | | // å·æ°åæ°å表 |
| | | if (process) { |
| | | toggleProcessParams2(process); |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("ç¼è¾åæ°å¤±è´¥"); |
| | | console.error("ç¼è¾åæ°å¤±è´¥ï¼", err); |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // ææ½æåº |
| | | const handleDragStart = (event, index, routeId) => { |
| | | draggedItem.value = index; |
| | | draggedRouteId.value = routeId; |
| | | event.dataTransfer.effectAllowed = "move"; |
| | | }; |
| | | |
| | | const handleDragOver = event => { |
| | | event.preventDefault(); |
| | | event.dataTransfer.dropEffect = "move"; |
| | | }; |
| | | |
| | | const handleDrop = (event, dropIndex, routeId) => { |
| | | event.preventDefault(); |
| | | if (draggedItem.value === null || draggedItem.value === dropIndex) return; |
| | | |
| | | const route = routeList.value.find(r => r.id === routeId); |
| | | if (route && route.processList) { |
| | | const draggedProcess = route.processList[draggedItem.value]; |
| | | |
| | | // è®¡ç®æ°çæåºå¼ |
| | | const newDragSort = dropIndex + 1; |
| | | |
| | | // è°ç¨APIæåºå·¥åº |
| | | sortProcessRouteItem({ |
| | | id: draggedProcess.id, |
| | | dragSort: newDragSort, |
| | | }) |
| | | .then(res => { |
| | | // è°ç¨æ¥å£è·åææ°çå·¥åºå表 |
| | | findProcessRouteItemList({ routeId: routeId }) |
| | | .then(res => { |
| | | if (route) { |
| | | route.processList = (res.data || []).map(process => ({ |
| | | ...process, |
| | | processId: process.processId || process.id, |
| | | expanded: false, |
| | | })); |
| | | } |
| | | ElMessage.success("æåºæå"); |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åå·¥åºå表失败ï¼", err); |
| | | ElMessage.success("æåºæå"); |
| | | }); |
| | | }) |
| | | .catch(err => { |
| | | ElMessage.error("æåºå¤±è´¥"); |
| | | console.error("æåºå·¥åºå¤±è´¥ï¼", err); |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | const handleDragEnd = () => { |
| | | draggedItem.value = null; |
| | | draggedRouteId.value = null; |
| | | }; |
| | | |
| | | // è·åæ°æ®åå
¸ |
| | | const getDictTypes = () => { |
| | | listType({ pageNum: 1, pageSize: 1000 }).then(res => { |
| | | dictTypes.value = res.rows || []; |
| | | }); |
| | | }; |
| | | |
| | | getRouteList(); |
| | | getDictTypes(); |
| | | |
| | | // 页é¢å è½½æ¶è·åå·¥åºå表 |
| | | onMounted(() => { |
| | | getProcessListApi() |
| | | .then(res => { |
| | | // å¤çè¿åçæ°æ®ï¼æ å°å°é¡µé¢éè¦çæ ¼å¼ |
| | | availableProcessList.value = (res.data || []).map(item => ({ |
| | | id: item.id, |
| | | no: item.no || item.no, |
| | | name: item.name || item.name, |
| | | remark: item.remark || item.remark, |
| | | status: item.status, |
| | | isQuality: item.isQuality, |
| | | })); |
| | | filteredProcessList.value = availableProcessList.value; |
| | | }) |
| | | .catch(() => { |
| | | ElMessage.error("è·åå·¥åºå表失败"); |
| | | }); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .app-container { |
| | | padding: 20px; |
| | | padding-bottom: 80px; |
| | | background-color: #f0f2f5; |
| | | min-height: calc(100vh - 84px); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .route-header { |
| | | margin-bottom: 20px; |
| | | |
| | | .add-route-btn { |
| | | width: 100%; |
| | | display: inline-flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-width: 120px; |
| | | height: 100px; |
| | | border: 2px dashed #dcdfe6; |
| | | border-radius: 12px; |
| | | background: #fafafa; |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | color: #909399; |
| | | padding: 0 20px; |
| | | |
| | | .el-icon { |
| | | font-size: 24px; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | span { |
| | | font-size: 13px; |
| | | } |
| | | |
| | | &:hover { |
| | | border-color: #409eff; |
| | | background: #ecf5ff; |
| | | color: #409eff; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .route-card-list { |
| | | display: grid; |
| | | grid-template-columns: repeat(1, 1fr); |
| | | gap: 20px; |
| | | max-height: calc(100vh - 240px); |
| | | overflow-y: auto; |
| | | padding-right: 10px; |
| | | } |
| | | |
| | | /* èªå®ä¹æ»å¨æ¡æ ·å¼ */ |
| | | .route-card-list::-webkit-scrollbar { |
| | | width: 8px; |
| | | } |
| | | |
| | | .route-card-list::-webkit-scrollbar-track { |
| | | background: #f1f1f1; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .route-card-list::-webkit-scrollbar-thumb { |
| | | background: #c1c1c1; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .route-card-list::-webkit-scrollbar-thumb:hover { |
| | | background: #a8a8a8; |
| | | } |
| | | |
| | | .route-card { |
| | | background: #fff; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | overflow: hidden; |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 20px 40px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | background: #f8f9fa; |
| | | |
| | | .route-info { |
| | | display: flex; |
| | | // flex-direction: column; |
| | | // justify-content: center; |
| | | // items-align: center; |
| | | gap: 4px; |
| | | |
| | | .route-code { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | font-family: "Courier New", monospace; |
| | | line-height: 30px; |
| | | } |
| | | |
| | | .route-name { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | } |
| | | |
| | | .route-actions { |
| | | display: flex; |
| | | gap: 8px; |
| | | |
| | | // .el-button { |
| | | // color: #409eff; |
| | | // } |
| | | } |
| | | } |
| | | |
| | | .card-body { |
| | | padding: 16px 40px; |
| | | |
| | | .route-desc { |
| | | font-size: 14px; |
| | | color: #606266; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .route-meta { |
| | | display: flex; |
| | | gap: 24px; |
| | | margin-bottom: 12px; |
| | | padding: 10px 14px; |
| | | background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%); |
| | | border-radius: 8px; |
| | | border-left: 3px solid #409eff; |
| | | |
| | | .meta-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | font-size: 13px; |
| | | margin-right: 40px; |
| | | |
| | | .el-icon { |
| | | font-size: 14px; |
| | | color: #409eff; |
| | | } |
| | | |
| | | .meta-label { |
| | | color: #909399; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .meta-value { |
| | | color: #303133; |
| | | font-weight: 600; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .expand-btn-wrapper { |
| | | display: flex; |
| | | justify-content: center; |
| | | margin-top: 8px; |
| | | |
| | | .expand-btn { |
| | | padding: 8px 20px; |
| | | border-radius: 20px; |
| | | background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%); |
| | | border: 1px solid #b3d8ff; |
| | | transition: all 0.3s ease; |
| | | |
| | | .btn-text { |
| | | font-size: 13px; |
| | | font-weight: 500; |
| | | color: #409eff; |
| | | margin-right: 6px; |
| | | } |
| | | |
| | | .expand-icon { |
| | | font-size: 14px; |
| | | color: #409eff; |
| | | transition: transform 0.3s ease; |
| | | } |
| | | |
| | | &:hover { |
| | | background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%); |
| | | border-color: #409eff; |
| | | box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3); |
| | | |
| | | .btn-text, |
| | | .expand-icon { |
| | | color: #fff; |
| | | } |
| | | } |
| | | |
| | | &.expanded { |
| | | background: linear-gradient(135deg, #f0f9eb 0%, #e1f3d8 100%); |
| | | border-color: #a5d69a; |
| | | |
| | | .btn-text, |
| | | .expand-icon { |
| | | color: #67c23a; |
| | | } |
| | | |
| | | &:hover { |
| | | background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%); |
| | | border-color: #67c23a; |
| | | box-shadow: 0 4px 12px rgba(103, 194, 58, 0.3); |
| | | |
| | | .btn-text, |
| | | .expand-icon { |
| | | color: #fff; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .process-route { |
| | | padding: 0 20px 20px; |
| | | background: #f5f7fa; |
| | | border-top: 1px solid #ebeef5; |
| | | |
| | | .process-flow { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | gap: 8px; |
| | | padding: 20px 0; |
| | | overflow-x: auto; |
| | | overflow-y: hidden; |
| | | |
| | | .process-flow-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | |
| | | .process-node { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 16px; |
| | | border: 2px solid #ebeef5; |
| | | cursor: move; |
| | | transition: all 0.3s ease; |
| | | // min-width: 180px; |
| | | // max-width: 220px; |
| | | width: 300px; |
| | | |
| | | &.expanded { |
| | | width: 400px; |
| | | } |
| | | |
| | | &:hover { |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| | | transform: translateY(-2px); |
| | | border-color: #409eff; |
| | | } |
| | | |
| | | &:active { |
| | | cursor: grabbing; |
| | | } |
| | | |
| | | .process-node-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 12px; |
| | | |
| | | .process-number { |
| | | width: 28px; |
| | | height: 28px; |
| | | border-radius: 50%; |
| | | background: #409eff; |
| | | color: #ffffff; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .process-actions { |
| | | display: flex; |
| | | gap: 4px; |
| | | } |
| | | } |
| | | |
| | | .process-node-body { |
| | | text-align: center; |
| | | margin-bottom: 12px; |
| | | |
| | | .process-code { |
| | | font-size: 11px; |
| | | color: #909399; |
| | | font-family: "Courier New", monospace; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .process-name { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 6px; |
| | | } |
| | | |
| | | .process-desc { |
| | | font-size: 12px; |
| | | color: #606266; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | display: -webkit-box; |
| | | -webkit-line-clamp: 2; |
| | | -webkit-box-orient: vertical; |
| | | } |
| | | } |
| | | |
| | | .process-node-footer { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | align-items: center; |
| | | padding-top: 10px; |
| | | border-top: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .process-params-section { |
| | | margin-top: 12px; |
| | | padding-top: 12px; |
| | | border-top: 1px solid #ebeef5; |
| | | |
| | | .params-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 8px; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .params-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 6px; |
| | | max-height: 200px; |
| | | overflow-y: auto; |
| | | |
| | | .param-item { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 6px 8px; |
| | | background: #fafafa; |
| | | border-radius: 4px; |
| | | border-left: 2px solid #409eff; |
| | | font-size: 12px; |
| | | |
| | | .param-info { |
| | | display: flex; |
| | | flex-direction: row; |
| | | align-items: center; |
| | | gap: 6px; |
| | | flex: 1; |
| | | min-width: 0; |
| | | |
| | | .param-code { |
| | | font-size: 11px; |
| | | color: #e6a23c; |
| | | font-family: "Courier New", monospace; |
| | | margin-right: 20px; |
| | | } |
| | | |
| | | .param-name { |
| | | font-size: 12px; |
| | | color: #303133; |
| | | font-weight: 500; |
| | | margin-right: 20px; |
| | | } |
| | | |
| | | .param-value { |
| | | font-size: 11px; |
| | | color: #606266; |
| | | } |
| | | } |
| | | |
| | | .param-actions { |
| | | display: flex; |
| | | gap: 4px; |
| | | flex-shrink: 0; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .flow-arrow { |
| | | display: flex; |
| | | align-items: center; |
| | | color: #c0c4cc; |
| | | font-size: 24px; |
| | | padding: 0 4px; |
| | | |
| | | .el-icon { |
| | | font-size: 20px; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .add-process-node { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-width: 100px; |
| | | height: 137px; |
| | | border: 2px dashed #dcdfe6; |
| | | border-radius: 12px; |
| | | background: #fafafa; |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | color: #909399; |
| | | // margin-left: 10px; |
| | | |
| | | .el-icon { |
| | | font-size: 24px; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | span { |
| | | font-size: 13px; |
| | | } |
| | | |
| | | &:hover { |
| | | border-color: #409eff; |
| | | background: #ecf5ff; |
| | | color: #409eff; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // ææ½æ¶çæ ·å¼ |
| | | .process-flow-item.dragging { |
| | | opacity: 0.5; |
| | | transform: scale(0.98); |
| | | } |
| | | |
| | | // 鿩工åºå¯¹è¯æ¡æ ·å¼ |
| | | .process-select-container { |
| | | display: flex; |
| | | gap: 20px; |
| | | height: 450px; |
| | | |
| | | .process-list-area { |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | .area-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 12px; |
| | | padding-bottom: 8px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .search-box { |
| | | margin-bottom: 12px; |
| | | |
| | | .el-input { |
| | | width: 100%; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .process-detail-area { |
| | | width: 380px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | padding: 16px; |
| | | |
| | | .area-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 16px; |
| | | padding-bottom: 8px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .process-detail-form { |
| | | .el-form-item { |
| | | margin-bottom: 12px; |
| | | |
| | | .el-form-item__label { |
| | | color: #606266; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | |
| | | .detail-text { |
| | | color: #303133; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // éæ©åæ°å¯¹è¯æ¡æ ·å¼ |
| | | .param-select-container { |
| | | display: flex; |
| | | gap: 20px; |
| | | height: 450px; |
| | | |
| | | .param-list-area { |
| | | // flex: 1; |
| | | width: 380px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | .area-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 12px; |
| | | padding-bottom: 8px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .search-box { |
| | | margin-bottom: 12px; |
| | | |
| | | .el-input { |
| | | width: 100%; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .param-detail-area { |
| | | // width: 380px; |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | padding: 16px; |
| | | |
| | | .area-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 16px; |
| | | padding-bottom: 8px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .param-detail-form { |
| | | .el-form-item { |
| | | margin-bottom: 12px; |
| | | |
| | | .el-form-item__label { |
| | | color: #606266; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | |
| | | .detail-text { |
| | | color: #303133; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // å页æ§ä»¶æ ·å¼ |
| | | .pagination-container { |
| | | position: fixed; |
| | | bottom: 0; |
| | | left: 0; |
| | | right: 0; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding: 16px 20px; |
| | | background-color: #fff !important; |
| | | border-top: 1px solid #ebeef5; |
| | | box-shadow: 0 -2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | z-index: 100; |
| | | |
| | | .el-pagination { |
| | | .el-pagination__sizes { |
| | | margin-right: 16px; |
| | | } |
| | | |
| | | .el-pagination__jump { |
| | | margin-left: 16px; |
| | | } |
| | | |
| | | .el-pagination__total { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .el-pagination__button { |
| | | border-radius: 4px; |
| | | transition: all 0.3s ease; |
| | | |
| | | &:hover:not(:disabled) { |
| | | color: #409eff; |
| | | border-color: #409eff; |
| | | } |
| | | } |
| | | |
| | | .el-pagination__button--active { |
| | | background-color: #409eff; |
| | | border-color: #409eff; |
| | | color: #fff; |
| | | } |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <PageHeader content="å·¥èºè·¯çº¿é¡¹ç®" /> |
| | | <!-- å·¥èºè·¯çº¿ä¿¡æ¯å±ç¤º --> |
| | | <el-card v-if="routeInfo.processRouteCode" |
| | | class="route-info-card" |
| | | shadow="hover"> |
| | | <div class="route-info"> |
| | | <div class="info-item"> |
| | | <div class="info-label-wrapper"> |
| | | <span class="info-label">å·¥èºè·¯çº¿ç¼å·</span> |
| | | </div> |
| | | <div class="info-value-wrapper"> |
| | | <span class="info-value">{{ routeInfo.processRouteCode }}</span> |
| | | </div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-label-wrapper"> |
| | | <span class="info-label">产ååç§°</span> |
| | | </div> |
| | | <div class="info-value-wrapper"> |
| | | <span class="info-value">{{ routeInfo.productName || '-' }}</span> |
| | | </div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-label-wrapper"> |
| | | <span class="info-label">è§æ ¼åç§°</span> |
| | | </div> |
| | | <div class="info-value-wrapper"> |
| | | <span class="info-value">{{ routeInfo.model || '-' }}</span> |
| | | </div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-label-wrapper"> |
| | | <span class="info-label">BOMç¼å·</span> |
| | | </div> |
| | | <div class="info-value-wrapper"> |
| | | <span class="info-value">{{ routeInfo.bomNo || '-' }}</span> |
| | | </div> |
| | | </div> |
| | | <div class="info-item full-width" |
| | | v-if="routeInfo.description"> |
| | | <div class="info-label-wrapper"> |
| | | <span class="info-label">æè¿°</span> |
| | | </div> |
| | | <div class="info-value-wrapper"> |
| | | <span class="info-value">{{ routeInfo.description }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | <!-- bomå±ç¤º --> |
| | | <!-- è¡¨æ ¼è§å¾ --> |
| | | <div v-if="viewMode === 'table'" |
| | | class="section-header"> |
| | | <div class="section-title">å·¥èºè·¯çº¿é¡¹ç®å表</div> |
| | | <div class="section-actions"> |
| | | <el-button icon="Grid" |
| | | @click="toggleView" |
| | | style="margin-right: 10px;"> |
| | | å¡çè§å¾ |
| | | </el-button> |
| | | <el-button type="primary" |
| | | @click="handleAdd">æ°å¢</el-button> |
| | | </div> |
| | | </div> |
| | | <el-table v-if="viewMode === 'table'" |
| | | ref="tableRef" |
| | | v-loading="tableLoading" |
| | | border |
| | | :data="tableData" |
| | | :header-cell-style="{ background: '#F0F1F5', color: '#333333' }" |
| | | row-key="id" |
| | | tooltip-effect="dark" |
| | | class="lims-table"> |
| | | <el-table-column align="center" |
| | | label="åºå·" |
| | | width="60" |
| | | type="index" /> |
| | | <el-table-column label="å·¥åºåç§°" |
| | | prop="processId" |
| | | width="200"> |
| | | <template #default="scope"> |
| | | {{ getProcessName(scope.row.processId) || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="产ååç§°" |
| | | prop="productName" |
| | | min-width="160" /> |
| | | <el-table-column label="è§æ ¼åç§°" |
| | | prop="model" |
| | | min-width="140" /> |
| | | <el-table-column label="åæ°å表" |
| | | min-width="160"> |
| | | <template #default="scope"> |
| | | <el-button type="primary" |
| | | link |
| | | size="small" |
| | | @click="handleViewParams(scope.row)">åæ°å表</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åä½" |
| | | prop="unit" |
| | | width="100" /> |
| | | <el-table-column label="æ¯å¦è´¨æ£" |
| | | prop="isQuality" |
| | | width="100"> |
| | | <template #default="scope"> |
| | | {{scope.row.isQuality ? "æ¯" : "å¦"}} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" |
| | | align="center" |
| | | fixed="right" |
| | | width="150"> |
| | | <template #default="scope"> |
| | | <el-button type="primary" |
| | | link |
| | | size="small" |
| | | @click="handleEdit(scope.row)" |
| | | :disabled="scope.row.isComplete">ç¼è¾</el-button> |
| | | <!-- <el-button type="info" |
| | | link |
| | | size="small" |
| | | @click="handleViewParams(scope.row)">åæ°å表</el-button> --> |
| | | <el-button type="danger" |
| | | link |
| | | size="small" |
| | | @click="handleDelete(scope.row)" |
| | | :disabled="scope.row.isComplete">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <!-- å¡çè§å¾ --> |
| | | <template v-else> |
| | | <div class="section-header"> |
| | | <div class="section-title">å·¥èºè·¯çº¿é¡¹ç®å表</div> |
| | | <div class="section-actions"> |
| | | <el-button icon="Menu" |
| | | @click="toggleView" |
| | | style="margin-right: 10px;"> |
| | | è¡¨æ ¼è§å¾ |
| | | </el-button> |
| | | <el-button type="primary" |
| | | @click="handleAdd">æ°å¢</el-button> |
| | | </div> |
| | | </div> |
| | | <div v-loading="tableLoading" |
| | | class="card-container"> |
| | | <div ref="cardsContainer" |
| | | class="cards-wrapper"> |
| | | <div v-for="(item, index) in tableData" |
| | | :key="item.id || index" |
| | | class="process-card" |
| | | :data-index="index"> |
| | | <!-- åºå·åå --> |
| | | <div class="card-header"> |
| | | <div class="card-number">{{ index + 1 }}</div> |
| | | <div class="card-process-name">{{ getProcessName(item.processId) || '-' }}</div> |
| | | </div> |
| | | <!-- 产åä¿¡æ¯ --> |
| | | <div class="card-content"> |
| | | <div v-if="item.productName" |
| | | class="product-info"> |
| | | <div class="product-name">{{ item.productName }}</div> |
| | | <div v-if="item.model" |
| | | class="product-model"> |
| | | {{ item.model }} |
| | | <!-- <span v-if="item.unit" class="product-unit">{{ item.unit }}</span> --> |
| | | </div> |
| | | <el-tag type="primary" |
| | | class="product-tag" |
| | | v-if="item.isQuality">è´¨æ£</el-tag> |
| | | </div> |
| | | <div v-else |
| | | class="product-info empty">ææ äº§åä¿¡æ¯</div> |
| | | </div> |
| | | <!-- æä½æé® --> |
| | | <div class="card-footer"> |
| | | <el-button type="primary" |
| | | link |
| | | size="small" |
| | | @click="handleEdit(item)" |
| | | :disabled="item.isComplete">ç¼è¾</el-button> |
| | | <el-button type="info" |
| | | link |
| | | size="small" |
| | | @click="handleViewParams(item)">åæ°å表</el-button> |
| | | <el-button type="danger" |
| | | link |
| | | size="small" |
| | | @click="handleDelete(item)" |
| | | :disabled="item.isComplete">å é¤</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <!-- æ°å¢/ç¼è¾å¼¹çª --> |
| | | <el-dialog v-model="dialogVisible" |
| | | :title="operationType === 'add' ? 'æ°å¢å·¥èºè·¯çº¿é¡¹ç®' : 'ç¼è¾å·¥èºè·¯çº¿é¡¹ç®'" |
| | | width="500px" |
| | | @close="closeDialog"> |
| | | <el-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="120px"> |
| | | <el-form-item label="å·¥åº" |
| | | prop="processId"> |
| | | <el-select v-model="form.processId" |
| | | placeholder="è¯·éæ©å·¥åº" |
| | | clearable |
| | | style="width: 100%"> |
| | | <el-option v-for="process in processOptions" |
| | | :key="process.id" |
| | | :label="process.name" |
| | | :value="process.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="产ååç§°" |
| | | prop="productModelId"> |
| | | <el-button type="primary" |
| | | @click="showProductSelectDialog = true"> |
| | | {{ form.productName && form.model |
| | | ? `${form.productName} - ${form.model}` |
| | | : 'éæ©äº§å' }} |
| | | </el-button> |
| | | </el-form-item> |
| | | <el-form-item label="åä½" |
| | | prop="unit"> |
| | | <el-input v-model="form.unit" |
| | | :placeholder="form.productModelId ? 'æ ¹æ®éæ©ç产åèªå¨å¸¦åº' : '请å
éæ©äº§å'" |
| | | clearable |
| | | :disabled="true" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¯å¦è´¨æ£" |
| | | prop="isQuality"> |
| | | <el-switch v-model="form.isQuality" |
| | | :active-value="true" |
| | | inactive-value="false" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="closeDialog">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="handleSubmit" |
| | | :loading="submitLoading">ç¡®å®</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 产åéæ©å¯¹è¯æ¡ --> |
| | | <ProductSelectDialog v-model="showProductSelectDialog" |
| | | @confirm="handleProductSelect" |
| | | single /> |
| | | <!-- åæ°åè¡¨å¯¹è¯æ¡ --> |
| | | <ProcessParamListDialog v-model="showParamListDialog" |
| | | :title="`${currentProcess ? getProcessName(currentProcess.processId) : ''} - åæ°å表`" |
| | | :route-id="routeId" |
| | | :editable="false" |
| | | :process="currentProcess" |
| | | :param-list="paramList" |
| | | @refresh="refreshParamList" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { |
| | | ref, |
| | | computed, |
| | | getCurrentInstance, |
| | | onMounted, |
| | | onUnmounted, |
| | | nextTick, |
| | | } from "vue"; |
| | | import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; |
| | | import ProcessParamListDialog from "@/components/ProcessParamListDialog.vue"; |
| | | import { |
| | | findProcessRouteItemList, |
| | | addOrUpdateProcessRouteItem, |
| | | sortProcessRouteItem, |
| | | batchDeleteProcessRouteItem, |
| | | getProcessParamList, |
| | | } from "@/api/productionManagement/processRouteItem.js"; |
| | | import { |
| | | findProductProcessRouteItemList, |
| | | deleteRouteItem, |
| | | addRouteItem, |
| | | addOrUpdateProductProcessRouteItem, |
| | | sortRouteItem, |
| | | } from "@/api/productionManagement/productProcessRoute.js"; |
| | | import { processList } from "@/api/productionManagement/productionProcess.js"; |
| | | import { useRoute } from "vue-router"; |
| | | import { ElMessageBox, ElMessage } from "element-plus"; |
| | | import Sortable from "sortablejs"; |
| | | |
| | | const route = useRoute(); |
| | | const { proxy } = getCurrentInstance() || {}; |
| | | |
| | | const routeId = computed(() => route.query.id); |
| | | const orderId = computed(() => route.query.orderId); |
| | | const pageType = computed(() => route.query.type); |
| | | |
| | | const tableLoading = ref(false); |
| | | const tableData = ref([]); |
| | | const dialogVisible = ref(false); |
| | | const operationType = ref("add"); // add | edit |
| | | const formRef = ref(null); |
| | | const submitLoading = ref(false); |
| | | const cardsContainer = ref(null); |
| | | const tableRef = ref(null); |
| | | const viewMode = ref("table"); // table | card |
| | | const routeInfo = ref({ |
| | | processRouteCode: "", |
| | | productName: "", |
| | | model: "", |
| | | bomNo: "", |
| | | bomId: null, |
| | | description: "", |
| | | }); |
| | | |
| | | const processOptions = ref([]); |
| | | const showProductSelectDialog = ref(false); |
| | | const showParamListDialog = ref(false); |
| | | const currentProcess = ref(null); |
| | | const paramList = ref([]); |
| | | let tableSortable = null; |
| | | let cardSortable = null; |
| | | |
| | | // 忢è§å¾ |
| | | const toggleView = () => { |
| | | viewMode.value = viewMode.value === "table" ? "card" : "table"; |
| | | // 忢è§å¾åéæ°åå§åææ½æåº |
| | | nextTick(() => { |
| | | initSortable(); |
| | | }); |
| | | }; |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | routeId: routeId.value, |
| | | processId: undefined, |
| | | productModelId: undefined, |
| | | productName: "", |
| | | model: "", |
| | | unit: "", |
| | | isQuality: false, |
| | | }); |
| | | |
| | | const rules = { |
| | | processId: [{ required: true, message: "è¯·éæ©å·¥åº", trigger: "change" }], |
| | | productModelId: [ |
| | | { required: true, message: "è¯·éæ©äº§å", trigger: "change" }, |
| | | ], |
| | | }; |
| | | |
| | | // æ ¹æ®å·¥åºIDè·åå·¥åºåç§° |
| | | const getProcessName = processId => { |
| | | if (!processId) return ""; |
| | | const process = processOptions.value.find(p => p.id === processId); |
| | | return process ? process.name : ""; |
| | | }; |
| | | |
| | | // è·åå表 |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | const listPromise = |
| | | pageType.value === "order" |
| | | ? findProductProcessRouteItemList({ orderId: orderId.value }) |
| | | : findProcessRouteItemList({ routeId: routeId.value }); |
| | | |
| | | listPromise |
| | | .then(res => { |
| | | tableData.value = res.data || []; |
| | | tableLoading.value = false; |
| | | // å表å è½½å®æååå§åææ½æåº |
| | | nextTick(() => { |
| | | initSortable(); |
| | | }); |
| | | }) |
| | | .catch(err => { |
| | | tableLoading.value = false; |
| | | console.error("è·åå表失败ï¼", err); |
| | | proxy?.$modal?.msgError("è·åå表失败"); |
| | | }); |
| | | }; |
| | | |
| | | // è·åå·¥åºå表 |
| | | const getProcessList = () => { |
| | | processList({}) |
| | | .then(res => { |
| | | processOptions.value = res.data || []; |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åå·¥åºå¤±è´¥ï¼", err); |
| | | }); |
| | | }; |
| | | |
| | | // è·åå·¥èºè·¯çº¿è¯¦æ
ï¼ä»è·¯ç±åæ°è·åï¼ |
| | | const getRouteInfo = () => { |
| | | routeInfo.value = { |
| | | processRouteCode: route.query.processRouteCode || "", |
| | | productName: route.query.productName || "", |
| | | model: route.query.model || "", |
| | | bomNo: route.query.bomNo || "", |
| | | bomId: route.query.bomId || null, |
| | | description: route.query.description || "", |
| | | }; |
| | | }; |
| | | |
| | | // æ°å¢ |
| | | const handleAdd = () => { |
| | | operationType.value = "add"; |
| | | resetForm(); |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | | // ç¼è¾ |
| | | const handleEdit = row => { |
| | | operationType.value = "edit"; |
| | | form.value = { |
| | | id: row.id, |
| | | routeId: routeId.value, |
| | | processId: row.processId, |
| | | productModelId: row.productModelId, |
| | | productName: row.productName || "", |
| | | model: row.model || "", |
| | | unit: row.unit || "", |
| | | isQuality: row.isQuality, |
| | | }; |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | | // å é¤ |
| | | const handleDelete = row => { |
| | | ElMessageBox.confirm("确认å é¤è¯¥å·¥èºè·¯çº¿é¡¹ç®ï¼", "æç¤º", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | // ç产订åä¸ä½¿ç¨ productProcessRoute çå 餿¥å£ï¼è·¯ç±åæ¼æ¥ idï¼ï¼å
¶å®æ
åµä½¿ç¨å·¥èºè·¯çº¿é¡¹ç®æ¹éå 餿¥å£ |
| | | const deletePromise = |
| | | pageType.value === "order" |
| | | ? deleteRouteItem(row.id) |
| | | : batchDeleteProcessRouteItem([row.id]); |
| | | |
| | | deletePromise |
| | | .then(() => { |
| | | proxy?.$modal?.msgSuccess("å 餿å"); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | proxy?.$modal?.msgError("å é¤å¤±è´¥"); |
| | | }); |
| | | }) |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | // 产åéæ© |
| | | const handleProductSelect = products => { |
| | | if (products && products.length > 0) { |
| | | const product = products[0]; |
| | | form.value.productModelId = product.id; |
| | | form.value.productName = product.productName; |
| | | form.value.model = product.model; |
| | | form.value.unit = product.unit || ""; |
| | | showProductSelectDialog.value = false; |
| | | // 触å表åéªè¯ |
| | | formRef.value?.validateField("productModelId"); |
| | | } |
| | | }; |
| | | |
| | | // æäº¤ |
| | | const handleSubmit = () => { |
| | | formRef.value.validate(valid => { |
| | | if (valid) { |
| | | submitLoading.value = true; |
| | | |
| | | if (operationType.value === "add") { |
| | | // æ°å¢ï¼ä¼ å个对象ï¼å
å«dragSortåæ®µ |
| | | // dragSort = å½åå表é¿åº¦ + 1ï¼è¡¨ç¤ºæ°å¢è®°å½æå¨æå |
| | | const dragSort = tableData.value.length + 1; |
| | | const isOrderPage = pageType.value === "order"; |
| | | |
| | | const addPromise = isOrderPage |
| | | ? addRouteItem({ |
| | | productOrderId: orderId.value, |
| | | productRouteId: routeId.value, |
| | | processId: form.value.processId, |
| | | productModelId: form.value.productModelId, |
| | | isQuality: form.value.isQuality, |
| | | dragSort, |
| | | }) |
| | | : addOrUpdateProcessRouteItem({ |
| | | routeId: routeId.value, |
| | | processId: form.value.processId, |
| | | productModelId: form.value.productModelId, |
| | | isQuality: form.value.isQuality, |
| | | dragSort, |
| | | }); |
| | | |
| | | addPromise |
| | | .then(() => { |
| | | proxy?.$modal?.msgSuccess("æ°å¢æå"); |
| | | closeDialog(); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | proxy?.$modal?.msgError("æ°å¢å¤±è´¥"); |
| | | }) |
| | | .finally(() => { |
| | | submitLoading.value = false; |
| | | }); |
| | | } else { |
| | | // ç¼è¾ï¼ç产订åä¸ä½¿ç¨ productProcessRoute/updateRouteItemï¼å
¶å®æ
åµä½¿ç¨å·¥èºè·¯çº¿é¡¹ç®æ´æ°æ¥å£ |
| | | const isOrderPage = pageType.value === "order"; |
| | | |
| | | const updatePromise = isOrderPage |
| | | ? addOrUpdateProductProcessRouteItem({ |
| | | id: form.value.id, |
| | | processId: form.value.processId, |
| | | productModelId: form.value.productModelId, |
| | | isQuality: form.value.isQuality, |
| | | }) |
| | | : addOrUpdateProcessRouteItem({ |
| | | routeId: routeId.value, |
| | | processId: form.value.processId, |
| | | productModelId: form.value.productModelId, |
| | | id: form.value.id, |
| | | isQuality: form.value.isQuality, |
| | | }); |
| | | |
| | | updatePromise |
| | | .then(() => { |
| | | proxy?.$modal?.msgSuccess("ä¿®æ¹æå"); |
| | | closeDialog(); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | proxy?.$modal?.msgError("ä¿®æ¹å¤±è´¥"); |
| | | }) |
| | | .finally(() => { |
| | | submitLoading.value = false; |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // é置表å |
| | | const resetForm = () => { |
| | | form.value = { |
| | | id: undefined, |
| | | routeId: routeId.value, |
| | | processId: undefined, |
| | | productModelId: undefined, |
| | | productName: "", |
| | | model: "", |
| | | unit: "", |
| | | }; |
| | | formRef.value?.resetFields(); |
| | | }; |
| | | |
| | | // å
³éå¼¹çª |
| | | const closeDialog = () => { |
| | | dialogVisible.value = false; |
| | | resetForm(); |
| | | }; |
| | | |
| | | // åå§åææ½æåº |
| | | const initSortable = () => { |
| | | destroySortable(); |
| | | |
| | | if (viewMode.value === "table") { |
| | | // è¡¨æ ¼è§å¾çææ½æåº |
| | | if (!tableRef.value) return; |
| | | |
| | | const tbody = |
| | | tableRef.value.$el.querySelector(".el-table__body tbody") || |
| | | tableRef.value.$el.querySelector( |
| | | ".el-table__body-wrapper > table > tbody" |
| | | ); |
| | | |
| | | if (!tbody) return; |
| | | |
| | | tableSortable = new Sortable(tbody, { |
| | | animation: 150, |
| | | ghostClass: "sortable-ghost", |
| | | handle: ".el-table__row", |
| | | filter: ".el-button, .el-select", |
| | | onEnd: evt => { |
| | | if (evt.oldIndex === evt.newIndex || !tableData.value[evt.oldIndex]) |
| | | return; |
| | | |
| | | // éæ°æåºæ°ç» |
| | | const moveItem = tableData.value.splice(evt.oldIndex, 1)[0]; |
| | | tableData.value.splice(evt.newIndex, 0, moveItem); |
| | | |
| | | // è®¡ç®æ°çåºå·ï¼dragSortä»1å¼å§ï¼ |
| | | const newIndex = evt.newIndex; |
| | | const dragSort = newIndex + 1; |
| | | |
| | | // è°ç¨æåºæ¥å£ |
| | | if (moveItem.id) { |
| | | const isOrderPage = pageType.value === "order"; |
| | | const sortPromise = isOrderPage |
| | | ? sortRouteItem({ |
| | | id: moveItem.id, |
| | | dragSort: dragSort, |
| | | }) |
| | | : sortProcessRouteItem({ |
| | | id: moveItem.id, |
| | | dragSort: dragSort, |
| | | }); |
| | | |
| | | sortPromise |
| | | .then(() => { |
| | | // æ´æ°ææè¡çdragSort |
| | | tableData.value.forEach((item, index) => { |
| | | if (item.id) { |
| | | item.dragSort = index + 1; |
| | | } |
| | | }); |
| | | proxy?.$modal?.msgSuccess("æåºæå"); |
| | | }) |
| | | .catch(err => { |
| | | // æåºå¤±è´¥ï¼æ¢å¤åæ°ç» |
| | | tableData.value.splice(newIndex, 1); |
| | | tableData.value.splice(evt.oldIndex, 0, moveItem); |
| | | proxy?.$modal?.msgError("æåºå¤±è´¥"); |
| | | console.error("æåºå¤±è´¥ï¼", err); |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | } else { |
| | | // å¡çè§å¾çææ½æåº |
| | | if (!cardsContainer.value) return; |
| | | |
| | | cardSortable = new Sortable(cardsContainer.value, { |
| | | animation: 150, |
| | | ghostClass: "sortable-ghost", |
| | | handle: ".process-card", |
| | | filter: ".el-button", |
| | | onEnd: evt => { |
| | | if (evt.oldIndex === evt.newIndex || !tableData.value[evt.oldIndex]) |
| | | return; |
| | | |
| | | // éæ°æåºæ°ç» |
| | | const moveItem = tableData.value.splice(evt.oldIndex, 1)[0]; |
| | | tableData.value.splice(evt.newIndex, 0, moveItem); |
| | | |
| | | // è®¡ç®æ°çåºå·ï¼dragSortä»1å¼å§ï¼ |
| | | const newIndex = evt.newIndex; |
| | | const dragSort = newIndex + 1; |
| | | |
| | | // è°ç¨æåºæ¥å£ |
| | | if (moveItem.id) { |
| | | const isOrderPage = pageType.value === "order"; |
| | | const sortPromise = isOrderPage |
| | | ? sortRouteItem({ |
| | | id: moveItem.id, |
| | | dragSort: dragSort, |
| | | }) |
| | | : sortProcessRouteItem({ |
| | | id: moveItem.id, |
| | | dragSort: dragSort, |
| | | }); |
| | | |
| | | sortPromise |
| | | .then(() => { |
| | | // æ´æ°ææè¡çdragSort |
| | | tableData.value.forEach((item, index) => { |
| | | if (item.id) { |
| | | item.dragSort = index + 1; |
| | | } |
| | | }); |
| | | proxy?.$modal?.msgSuccess("æåºæå"); |
| | | }) |
| | | .catch(err => { |
| | | // æåºå¤±è´¥ï¼æ¢å¤åæ°ç» |
| | | tableData.value.splice(newIndex, 1); |
| | | tableData.value.splice(evt.oldIndex, 0, moveItem); |
| | | proxy?.$modal?.msgError("æåºå¤±è´¥"); |
| | | console.error("æåºå¤±è´¥ï¼", err); |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // éæ¯ææ½æåº |
| | | const destroySortable = () => { |
| | | if (tableSortable) { |
| | | tableSortable.destroy(); |
| | | tableSortable = null; |
| | | } |
| | | if (cardSortable) { |
| | | cardSortable.destroy(); |
| | | cardSortable = null; |
| | | } |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getRouteInfo(); |
| | | getList(); |
| | | getProcessList(); |
| | | }); |
| | | |
| | | // æ¥çåæ°å表 |
| | | const handleViewParams = process => { |
| | | currentProcess.value = process; |
| | | // è°ç¨APIè·ååæ°å表 |
| | | getProcessParamList({ |
| | | routeItemId: process.id, |
| | | pageNum: 1, |
| | | pageSize: 1000, |
| | | }) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | paramList.value = res.data?.records || []; |
| | | } else { |
| | | ElMessage.error(res.msg || "è·ååæ°å表失败"); |
| | | paramList.value = []; |
| | | } |
| | | showParamListDialog.value = true; |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·ååæ°å表失败ï¼", err); |
| | | ElMessage.error("è·ååæ°å表失败"); |
| | | paramList.value = []; |
| | | showParamListDialog.value = true; |
| | | }); |
| | | }; |
| | | |
| | | // å·æ°åæ°å表 |
| | | const refreshParamList = () => { |
| | | if (!currentProcess.value) return; |
| | | // éæ°è°ç¨APIè·ååæ°å表 |
| | | getProcessParamList({ |
| | | routeItemId: currentProcess.value.id, |
| | | pageNum: 1, |
| | | pageSize: 1000, |
| | | }) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | paramList.value = res.data?.records || []; |
| | | } else { |
| | | ElMessage.error(res.msg || "è·ååæ°å表失败"); |
| | | paramList.value = []; |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·ååæ°å表失败ï¼", err); |
| | | ElMessage.error("è·ååæ°å表失败"); |
| | | paramList.value = []; |
| | | }); |
| | | }; |
| | | |
| | | onUnmounted(() => { |
| | | destroySortable(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .card-container { |
| | | padding: 20px 0; |
| | | } |
| | | |
| | | .cards-wrapper { |
| | | display: flex; |
| | | gap: 16px; |
| | | overflow-x: auto; |
| | | padding: 10px 0; |
| | | min-height: 200px; |
| | | } |
| | | |
| | | .cards-wrapper::-webkit-scrollbar { |
| | | height: 8px; |
| | | } |
| | | |
| | | .cards-wrapper::-webkit-scrollbar-track { |
| | | background: #f1f1f1; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .cards-wrapper::-webkit-scrollbar-thumb { |
| | | background: #c1c1c1; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .cards-wrapper::-webkit-scrollbar-thumb:hover { |
| | | background: #a8a8a8; |
| | | } |
| | | |
| | | .process-card { |
| | | flex-shrink: 0; |
| | | width: 220px; |
| | | background: #fff; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | padding: 16px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | cursor: move; |
| | | transition: all 0.3s; |
| | | } |
| | | |
| | | .process-card:hover { |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | .card-header { |
| | | text-align: center; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .card-number { |
| | | width: 36px; |
| | | height: 36px; |
| | | line-height: 36px; |
| | | border-radius: 50%; |
| | | background: #409eff; |
| | | color: #fff; |
| | | font-weight: bold; |
| | | font-size: 16px; |
| | | margin: 0 auto 8px; |
| | | } |
| | | |
| | | .card-process-name { |
| | | font-size: 14px; |
| | | color: #333; |
| | | font-weight: 500; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .card-content { |
| | | flex: 1; |
| | | margin-bottom: 12px; |
| | | min-height: 60px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .product-info { |
| | | font-size: 13px; |
| | | color: #666; |
| | | text-align: center; |
| | | width: 100%; |
| | | } |
| | | |
| | | .product-info.empty { |
| | | color: #999; |
| | | text-align: center; |
| | | padding: 20px 0; |
| | | } |
| | | |
| | | .product-name { |
| | | margin-bottom: 6px; |
| | | word-break: break-all; |
| | | line-height: 1.5; |
| | | text-align: center; |
| | | } |
| | | |
| | | .product-model { |
| | | color: #909399; |
| | | font-size: 12px; |
| | | word-break: break-all; |
| | | line-height: 1.5; |
| | | text-align: center; |
| | | } |
| | | |
| | | .product-unit { |
| | | margin-left: 4px; |
| | | color: #409eff; |
| | | } |
| | | |
| | | .product-tag { |
| | | margin: 10px 0; |
| | | } |
| | | |
| | | .card-footer { |
| | | display: flex; |
| | | justify-content: space-around; |
| | | padding-top: 12px; |
| | | border-top: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .card-footer .el-button { |
| | | padding: 0; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | :deep(.sortable-ghost) { |
| | | opacity: 0.5; |
| | | background-color: #f5f7fa !important; |
| | | } |
| | | |
| | | :deep(.sortable-drag) { |
| | | opacity: 0.8; |
| | | } |
| | | |
| | | /* è¡¨æ ¼è§å¾æ ·å¼ */ |
| | | :deep(.el-table__row) { |
| | | transition: background-color 0.2s; |
| | | cursor: move; |
| | | } |
| | | |
| | | :deep(.el-table__row:hover) { |
| | | background-color: #f9fafc !important; |
| | | } |
| | | |
| | | /* åºåæ 颿 ·å¼ */ |
| | | .section-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | padding-left: 12px; |
| | | position: relative; |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .section-title::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 0; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: 3px; |
| | | height: 16px; |
| | | background: #409eff; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .section-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | /* å·¥èºè·¯çº¿ä¿¡æ¯å¡çæ ·å¼ */ |
| | | .route-info-card { |
| | | margin-bottom: 20px; |
| | | border: 1px solid #e4e7ed; |
| | | background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .route-info { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); |
| | | gap: 16px; |
| | | padding: 4px; |
| | | } |
| | | |
| | | .info-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | background: #ffffff; |
| | | border-radius: 6px; |
| | | padding: 14px 16px; |
| | | border: 1px solid #f0f2f5; |
| | | transition: all 0.3s ease; |
| | | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .info-item:hover { |
| | | border-color: #409eff; |
| | | box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15); |
| | | transform: translateY(-1px); |
| | | } |
| | | |
| | | .info-item.full-width { |
| | | grid-column: 1 / -1; |
| | | } |
| | | |
| | | .info-label-wrapper { |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .info-label { |
| | | display: inline-block; |
| | | color: #909399; |
| | | font-size: 12px; |
| | | font-weight: 500; |
| | | text-transform: uppercase; |
| | | letter-spacing: 0.5px; |
| | | padding: 2px 0; |
| | | position: relative; |
| | | } |
| | | |
| | | .info-label::after { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 0; |
| | | bottom: 0; |
| | | width: 20px; |
| | | height: 2px; |
| | | background: linear-gradient(90deg, #409eff, transparent); |
| | | border-radius: 1px; |
| | | } |
| | | |
| | | .info-value-wrapper { |
| | | flex: 1; |
| | | } |
| | | |
| | | .info-value { |
| | | display: block; |
| | | color: #303133; |
| | | font-size: 15px; |
| | | font-weight: 500; |
| | | line-height: 1.5; |
| | | word-break: break-all; |
| | | } |
| | | </style> |
| | |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | <el-form-item label="强度" |
| | | v-if="mergeForm.productName === 'ç å'"> |
| | | <div v-if="strengthError" |
| | | class="strength-error" |
| | | style="color: red; margin-bottom: 8px;">{{ strengthError }}</div> |
| | | <el-select v-model="mergeForm.strength" |
| | | placeholder="è¯·éæ©å¼ºåº¦" |
| | | style="width: 100%" |
| | | required> |
| | | <el-option v-for="item in block_strength" |
| | | :key="item.id" |
| | | :label="item.label" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="çäº§æ¹æ°"> |
| | | <el-input-number v-model="mergeForm.totalAssignedQuantity" |
| | | :min="0" |
| | |
| | | placeholder="è¯·éæ©è®¡åç»ææ¥æ" /> |
| | | </el-form-item> |
| | | <el-form-item label="强度" |
| | | prop="strength"> |
| | | prop="strength" |
| | | v-if="form.productName === 'ç å'"> |
| | | <el-select v-model="form.strength" |
| | | placeholder="è¯·éæ©å¼ºåº¦" |
| | | style="width: 100%"> |
| | | <el-option label="A3.5" |
| | | value="A3.5" /> |
| | | <el-option label="A5.0" |
| | | value="A5.0" /> |
| | | <el-option v-for="item in block_strength" |
| | | :key="item.id" |
| | | :label="item.label" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨ 1" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, ref, reactive, getCurrentInstance } from "vue"; |
| | | import { onMounted, ref, reactive, getCurrentInstance, toRefs } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import dayjs from "dayjs"; |
| | | import ImportDialog from "@/components/Dialog/ImportDialog.vue"; |
| | | import { getToken } from "@/utils/auth"; |
| | | import { useDict } from "@/utils/dict"; |
| | | import { |
| | | productionPlanListPage, |
| | | loadProdData, |
| | |
| | | { |
| | | label: "强度", |
| | | prop: "strength", |
| | | formatData: cell => { |
| | | if (!cell) return ""; |
| | | const strengthItem = block_strength.value.find(item => item.id === cell); |
| | | return strengthItem ? strengthItem.label : cell; |
| | | }, |
| | | }, |
| | | |
| | | { |
| | |
| | | clickFun: row => { |
| | | // åç¬ä¸åæä½ |
| | | // è®¾ç½®è¡¨åæ°æ® |
| | | strengthError.value = ""; |
| | | mergeForm.ids = [row.id]; |
| | | mergeForm.materialCode = row.materialCode; |
| | | mergeForm.productName = row.productName || ""; |
| | |
| | | mergeForm.totalAssignedQuantity = |
| | | (Number(row.volume) - Number(row.assignedQuantity)).toFixed(4) || 0; |
| | | mergeForm.planCompleteTime = row.planCompleteTime || ""; |
| | | mergeForm.strength = row.strength || ""; |
| | | sumAssignedQuantity.value = mergeForm.totalAssignedQuantity; |
| | | // æå¼å¼¹çª |
| | | isShowNewModal.value = true; |
| | |
| | | height: 0, |
| | | totalAssignedQuantity: 0, |
| | | planCompleteTime: "", |
| | | strength: "", |
| | | }); |
| | | |
| | | // 追踪è¿åº¦å¼¹çªæ§å¶ |
| | |
| | | const productOptions = ref([]); |
| | | const specificationOptions = ref([]); |
| | | const formRef = ref(null); |
| | | // è·å强度åå
¸ |
| | | const { block_strength } = useDict("block_strength"); |
| | | const form = reactive({ |
| | | id: undefined, |
| | | applyNo: "", |
| | |
| | | volume: [{ required: true, message: "请è¾å
¥æ¹æ°", trigger: "blur" }], |
| | | productMaterialId: [ |
| | | { required: true, message: "è¯·éæ©äº§å", trigger: "change" }, |
| | | ], |
| | | strength: [ |
| | | { |
| | | validator: (rule, value, callback) => { |
| | | if (form.productName === "ç å" && !value) { |
| | | callback(new Error("ç å产åç强度为å¿
填项")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }, |
| | | trigger: ["blur", "change"], |
| | | required: false, |
| | | }, |
| | | ], |
| | | }); |
| | | |
| | |
| | | |
| | | const handleProductChange = value => { |
| | | form.productMaterialSkuId = undefined; |
| | | // æ¥æ¾éä¸ç产ååç§° |
| | | const findProductName = (options, value) => { |
| | | for (const option of options) { |
| | | if (option.value === value) { |
| | | return option.label; |
| | | } |
| | | if (option.children) { |
| | | const found = findProductName(option.children, value); |
| | | if (found) { |
| | | return found; |
| | | } |
| | | } |
| | | } |
| | | return ""; |
| | | }; |
| | | form.productName = findProductName(productOptions.value, value); |
| | | // 触åå¼ºåº¦åæ®µéªè¯ |
| | | if (formRef.value) { |
| | | formRef.value.validateField("strength"); |
| | | } |
| | | fetchSpecificationOptions(value); |
| | | }; |
| | | |
| | |
| | | .catch(() => {}); |
| | | }; |
| | | const sumAssignedQuantity = ref(0); |
| | | // ååºå¼æ°æ® |
| | | const strengthError = ref(""); |
| | | |
| | | // å¤çåå¹¶ä¸åæé®ç¹å» |
| | | const handleMerge = () => { |
| | | if (selectedRows.value.length === 0) { |
| | |
| | | return; |
| | | } |
| | | console.log(selectedRows.value); |
| | | // æ£æ¥å¼ºåº¦ä¸è´æ§ |
| | | const firstRow = selectedRows.value[0]; |
| | | const productName = firstRow.productName || ""; |
| | | let strengthConsistent = true; |
| | | let firstStrength = firstRow.strength || ""; |
| | | strengthError.value = ""; |
| | | |
| | | if (productName === "ç å") { |
| | | for (const row of selectedRows.value) { |
| | | if (row.strength !== firstStrength) { |
| | | strengthConsistent = false; |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (!strengthConsistent) { |
| | | strengthError.value = "éæ©çç å强度ä¸ä¸è´ï¼è¯·éæ°éæ©"; |
| | | } |
| | | } |
| | | |
| | | // è®¡ç®æ»å¶é æ°é |
| | | const totalAssignedQuantity = selectedRows.value.reduce((sum, row) => { |
| | | return ( |
| | |
| | | sumAssignedQuantity.value = totalAssignedQuantity; |
| | | console.log(totalAssignedQuantity); |
| | | // è®¾ç½®è¡¨åæ°æ® |
| | | const firstRow = selectedRows.value[0]; |
| | | mergeForm.materialCode = selectedserialNo.value; |
| | | mergeForm.productName = firstRow.productName || ""; |
| | | mergeForm.productName = productName; |
| | | mergeForm.model = firstRow.model || ""; |
| | | mergeForm.length = firstRow.length || 0; |
| | | mergeForm.width = firstRow.width || 0; |
| | | mergeForm.height = firstRow.height || 0; |
| | | mergeForm.totalAssignedQuantity = totalAssignedQuantity; |
| | | mergeForm.planCompleteTime = firstRow.planCompleteTime || ""; |
| | | mergeForm.strength = firstStrength; |
| | | mergeForm.ids = selectedRows.value.map(row => row.id); |
| | | |
| | | // æå¼å¼¹çª |
| | |
| | | ElMessage.warning("请è¾å
¥çäº§æ¹æ°"); |
| | | return; |
| | | } |
| | | // éªè¯ç å产åç强度 |
| | | if (mergeForm.productName === "ç å" && !mergeForm.strength) { |
| | | ElMessage.error("ç å产åç强度为å¿
填项"); |
| | | return; |
| | | } |
| | | console.log(sumAssignedQuantity.value, "sumAssignedQuantity"); |
| | | // 计ç®å½åéä¸è¡çæ»æ¹æ° |
| | | const totalVolume = selectedRows.value.reduce((sum, row) => { |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="æ°éï¼" |
| | | prop="quantity"> |
| | | <el-input type="number" |
| | | v-model="form.quantity" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="åä½ï¼" |
| | | prop="unit"> |
| | | <el-input v-model="form.unit" |
| | |
| | | { required: false, message: "è¯·éæ©ææ ", trigger: "change" }, |
| | | ], |
| | | unit: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | | quantity: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | // quantity: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | checkCompany: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | | checkResult: [ |
| | | { required: false, message: "è¯·éæ©æ£æµç»æ", trigger: "change" }, |
| | |
| | | modelOptions.value = res || []; |
| | | // 忥åå¡« model / unitï¼æäºæ¥å£è¿åç row éå¯è½æ²¡å¸¦å
¨ï¼ |
| | | if (form.value.productModelId) { |
| | | handleChangeModel(form.value.productModelId); |
| | | // handleChangeModel(form.value.productModelId); |
| | | } |
| | | } catch (e) { |
| | | console.error("å è½½è§æ ¼åå·å¤±è´¥", e); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="dashboard-container"> |
| | | <div class="data-dashboard"> |
| | | <!-- 顶鍿 颿 --> |
| | | <!-- <div class="dashboard-header"> |
| | | <div class="factory-name">ç产ç»è®¡çæ¿</div> |
| | | </div> --> |
| | | <!-- çéåºå --> |
| | | <div class="filter-area"> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">æ¶é´ç»´åº¦ï¼</span> |
| | | <el-radio-group v-model="dateType" |
| | | @change="handleDateTypeChange" |
| | | class="radio-group"> |
| | | <el-radio-button label="month">æåº¦</el-radio-button> |
| | | <el-radio-button label="year">年度</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">产åç±»åï¼</span> |
| | | <el-radio-group v-model="productType" |
| | | @change="handleProductTypeChange" |
| | | class="radio-group"> |
| | | <el-radio-button label="block">ç å</el-radio-button> |
| | | <el-radio-button label="plate">æ¿æ</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | </div> |
| | | <!-- 主è¦å
容åºå --> |
| | | <div class="dashboard-content"> |
| | | <!-- 第ä¸è¡ --> |
| | | <div class="row row-1"> |
| | | <div class="panel-card card-1"> |
| | | <div class="panel-title">äº§éææ </div> |
| | | <div class="chart-container"> |
| | | <div ref="productionChart" |
| | | style="width: 100%; height: 100%"></div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-2"> |
| | | <div class="panel-title">åºåºå¤çé</div> |
| | | <div class="chart-container"> |
| | | <div ref="solidWasteChart" |
| | | style="width: 100%; height: 100%"></div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-3"> |
| | | <div class="panel-title">综åç»è®¡</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-label">æ»äº§è½</div> |
| | | <div class="stat-value production-color">{{ totalProduction }}</div> |
| | | <div class="stat-unit">ç«æ¹ç±³</div> |
| | | </div> |
| | | <div class="stat-item"> |
| | | <div class="stat-label">æ»åºåºå¤ç</div> |
| | | <div class="stat-value waste-color">{{ totalSolidWaste }}</div> |
| | | <div class="stat-unit">å¨</div> |
| | | </div> |
| | | <div class="stat-item"> |
| | | <div class="stat-label">å¹³ååè</div> |
| | | <div class="stat-value consumption-color">{{ averageUnitConsumption }}</div> |
| | | <div class="stat-unit">å¨/ç«æ¹ç±³</div> |
| | | </div> |
| | | <div class="stat-item"> |
| | | <div class="stat-label">æ»è½è</div> |
| | | <div class="stat-value energy-color">{{ totalEnergy }}</div> |
| | | <div class="stat-unit">kWh</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- 第äºè¡ --> |
| | | <div class="row row-2"> |
| | | <div class="panel-card card-4"> |
| | | <div class="panel-title">çäº§ææ¬åè</div> |
| | | <div class="chart-container"> |
| | | <div ref="costChart" |
| | | style="width: 100%; height: 100%"></div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-5"> |
| | | <div class="panel-title">ç产è½èæ°æ®</div> |
| | | <div class="chart-container"> |
| | | <div ref="energyChart" |
| | | style="width: 100%; height: 100%"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- 第ä¸è¡ --> |
| | | <div class="row row-3"> |
| | | <div class="panel-card card-6"> |
| | | <div class="panel-title">åèæ°æ®æç»</div> |
| | | <div class="table-container"> |
| | | <el-table :data="costTableData" |
| | | style="width: 100%"> |
| | | <el-table-column prop="material" |
| | | label="ç©æç±»å" |
| | | width="120" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getMaterialTypeType(scope.row.material)"> |
| | | {{ scope.row.material }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="unit" |
| | | label="åä½" |
| | | width="100" /> |
| | | <el-table-column prop="monthlyConsumption" |
| | | label="æåº¦ç´¯è®¡ç¨é" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.monthlyConsumption }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="monthlyProduction" |
| | | label="æåº¦ç´¯è®¡äº§é" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.monthlyProduction }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="monthlyUnitConsumption" |
| | | label="æåº¦ç´¯è®¡åè" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.monthlyUnitConsumption }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="yearlyConsumption" |
| | | label="年度累计ç¨é" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.yearlyConsumption }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="yearlyProduction" |
| | | label="年度累计产é" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.yearlyProduction }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="yearlyUnitConsumption" |
| | | label="年度累计åè" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.yearlyUnitConsumption }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { |
| | | ref, |
| | | computed, |
| | | onMounted, |
| | | onBeforeUnmount, |
| | | nextTick, |
| | | watch, |
| | | } from "vue"; |
| | | import * as echarts from "echarts"; |
| | | |
| | | // ç鿡件 |
| | | const dateType = ref("month"); // month æ year |
| | | const productType = ref("block"); // block æ plate |
| | | |
| | | // å¾è¡¨å¼ç¨ |
| | | const productionChart = ref(null); |
| | | const solidWasteChart = ref(null); |
| | | const costChart = ref(null); |
| | | const energyChart = ref(null); |
| | | |
| | | // å¾è¡¨å®ä¾ |
| | | let productionChartInstance = null; |
| | | let solidWasteChartInstance = null; |
| | | let costChartInstance = null; |
| | | let energyChartInstance = null; |
| | | |
| | | // æ¨¡ææ°æ® |
| | | const productionData = ref({ |
| | | month: [ |
| | | { name: "1æ", block: 1200, plate: 800 }, |
| | | { name: "2æ", block: 1300, plate: 850 }, |
| | | { name: "3æ", block: 1100, plate: 750 }, |
| | | { name: "4æ", block: 1400, plate: 900 }, |
| | | { name: "5æ", block: 1500, plate: 950 }, |
| | | { name: "6æ", block: 1350, plate: 880 }, |
| | | { name: "7æ", block: 1450, plate: 920 }, |
| | | { name: "8æ", block: 1600, plate: 1000 }, |
| | | { name: "9æ", block: 1550, plate: 980 }, |
| | | { name: "10æ", block: 1700, plate: 1050 }, |
| | | { name: "11æ", block: 1650, plate: 1020 }, |
| | | { name: "12æ", block: 1800, plate: 1100 }, |
| | | ], |
| | | year: [ |
| | | { name: "2023", block: 15000, plate: 9500 }, |
| | | { name: "2024", block: 16500, plate: 10200 }, |
| | | { name: "2025", block: 18000, plate: 11000 }, |
| | | ], |
| | | }); |
| | | |
| | | const solidWasteData = ref({ |
| | | month: [ |
| | | { name: "1æ", ç²ç
¤ç°: 200, ç³è: 150, ç³ç°: 100 }, |
| | | { name: "2æ", ç²ç
¤ç°: 220, ç³è: 160, ç³ç°: 110 }, |
| | | { name: "3æ", ç²ç
¤ç°: 190, ç³è: 140, ç³ç°: 95 }, |
| | | { name: "4æ", ç²ç
¤ç°: 230, ç³è: 170, ç³ç°: 115 }, |
| | | { name: "5æ", ç²ç
¤ç°: 240, ç³è: 180, ç³ç°: 120 }, |
| | | { name: "6æ", ç²ç
¤ç°: 225, ç³è: 165, ç³ç°: 112 }, |
| | | ], |
| | | year: [ |
| | | { name: "2023", ç²ç
¤ç°: 2500, ç³è: 1800, ç³ç°: 1200 }, |
| | | { name: "2024", ç²ç
¤ç°: 2700, ç³è: 1950, ç³ç°: 1300 }, |
| | | { name: "2025", ç²ç
¤ç°: 2900, ç³è: 2100, ç³ç°: 1400 }, |
| | | ], |
| | | }); |
| | | |
| | | const costData = ref({ |
| | | materials: ["æ°´æ³¥", "éç²è", "è±æ¨¡å", "é²è
å", "æ°¯åå", "å·æä¸"], |
| | | month: { |
| | | consumption: [1200, 50, 80, 30, 40, 60], |
| | | production: [12000, 12000, 12000, 8000, 8000, 8000], |
| | | unitConsumption: [0.1, 0.0042, 0.0067, 0.0038, 0.005, 0.0075], |
| | | }, |
| | | year: { |
| | | consumption: [14000, 600, 950, 350, 480, 720], |
| | | production: [140000, 140000, 140000, 95000, 95000, 95000], |
| | | unitConsumption: [0.1, 0.0043, 0.0068, 0.0037, 0.0051, 0.0076], |
| | | }, |
| | | }); |
| | | |
| | | const energyData = ref({ |
| | | month: [ |
| | | { name: "1æ", çµé: 12000, æ°´é: 8000, æ°é: 5000 }, |
| | | { name: "2æ", çµé: 13000, æ°´é: 8500, æ°é: 5500 }, |
| | | { name: "3æ", çµé: 11000, æ°´é: 7500, æ°é: 4800 }, |
| | | { name: "4æ", çµé: 14000, æ°´é: 9000, æ°é: 6000 }, |
| | | { name: "5æ", çµé: 15000, æ°´é: 9500, æ°é: 6500 }, |
| | | { name: "6æ", çµé: 13500, æ°´é: 8800, æ°é: 5800 }, |
| | | ], |
| | | year: [ |
| | | { name: "2023", çµé: 140000, æ°´é: 95000, æ°é: 65000 }, |
| | | { name: "2024", çµé: 150000, æ°´é: 100000, æ°é: 70000 }, |
| | | { name: "2025", çµé: 160000, æ°´é: 105000, æ°é: 75000 }, |
| | | ], |
| | | }); |
| | | |
| | | // 计ç®å±æ§ |
| | | const productionChartOption = computed(() => { |
| | | const data = productionData.value[dateType.value]; |
| | | return { |
| | | tooltip: { |
| | | trigger: "axis", |
| | | axisPointer: { |
| | | type: "shadow", |
| | | }, |
| | | }, |
| | | legend: { |
| | | data: ["ç å", "æ¿æ"], |
| | | textStyle: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | grid: { |
| | | left: "3%", |
| | | right: "4%", |
| | | bottom: "3%", |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: data.map(item => item.name), |
| | | axisLabel: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "产é (ç«æ¹ç±³)", |
| | | axisLabel: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "ç å", |
| | | type: "line", |
| | | data: data.map(item => item.block), |
| | | smooth: true, |
| | | lineStyle: { |
| | | width: 3, |
| | | }, |
| | | itemStyle: { |
| | | color: "#409EFF", |
| | | }, |
| | | }, |
| | | { |
| | | name: "æ¿æ", |
| | | type: "line", |
| | | data: data.map(item => item.plate), |
| | | smooth: true, |
| | | lineStyle: { |
| | | width: 3, |
| | | }, |
| | | itemStyle: { |
| | | color: "#67C23A", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | const solidWasteChartOption = computed(() => { |
| | | const data = solidWasteData.value[dateType.value]; |
| | | return { |
| | | tooltip: { |
| | | trigger: "axis", |
| | | axisPointer: { |
| | | type: "shadow", |
| | | }, |
| | | }, |
| | | legend: { |
| | | data: ["ç²ç
¤ç°", "ç³è", "ç³ç°"], |
| | | textStyle: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | grid: { |
| | | left: "3%", |
| | | right: "4%", |
| | | bottom: "3%", |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: data.map(item => item.name), |
| | | axisLabel: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "å¤çé (å¨)", |
| | | axisLabel: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "ç²ç
¤ç°", |
| | | type: "bar", |
| | | data: data.map(item => item.ç²ç
¤ç°), |
| | | itemStyle: { |
| | | color: "#909399", |
| | | }, |
| | | }, |
| | | { |
| | | name: "ç³è", |
| | | type: "bar", |
| | | data: data.map(item => item.ç³è), |
| | | itemStyle: { |
| | | color: "#E6A23C", |
| | | }, |
| | | }, |
| | | { |
| | | name: "ç³ç°", |
| | | type: "bar", |
| | | data: data.map(item => item.ç³ç°), |
| | | itemStyle: { |
| | | color: "#F56C6C", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | const costChartOption = computed(() => { |
| | | const data = costData.value; |
| | | return { |
| | | tooltip: { |
| | | trigger: "axis", |
| | | axisPointer: { |
| | | type: "shadow", |
| | | }, |
| | | }, |
| | | legend: { |
| | | data: ["æåº¦åè", "年度åè"], |
| | | textStyle: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | grid: { |
| | | left: "3%", |
| | | right: "4%", |
| | | bottom: "3%", |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: data.materials, |
| | | axisLabel: { |
| | | color: "#333", |
| | | rotate: 45, |
| | | }, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "åè (å¨/ç«æ¹ç±³)", |
| | | axisLabel: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "æåº¦åè", |
| | | type: "bar", |
| | | data: data.month.unitConsumption, |
| | | itemStyle: { |
| | | color: "#409EFF", |
| | | }, |
| | | }, |
| | | { |
| | | name: "年度åè", |
| | | type: "bar", |
| | | data: data.year.unitConsumption, |
| | | itemStyle: { |
| | | color: "#67C23A", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | const energyChartOption = computed(() => { |
| | | const data = energyData.value[dateType.value]; |
| | | return { |
| | | tooltip: { |
| | | trigger: "axis", |
| | | axisPointer: { |
| | | type: "shadow", |
| | | }, |
| | | }, |
| | | legend: { |
| | | data: ["çµé", "æ°´é", "æ°é"], |
| | | textStyle: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | grid: { |
| | | left: "3%", |
| | | right: "4%", |
| | | bottom: "3%", |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: data.map(item => item.name), |
| | | axisLabel: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "è½èé", |
| | | axisLabel: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "çµé", |
| | | type: "line", |
| | | data: data.map(item => item.çµé), |
| | | smooth: true, |
| | | lineStyle: { |
| | | width: 3, |
| | | }, |
| | | itemStyle: { |
| | | color: "#409EFF", |
| | | }, |
| | | }, |
| | | { |
| | | name: "æ°´é", |
| | | type: "line", |
| | | data: data.map(item => item.æ°´é), |
| | | smooth: true, |
| | | lineStyle: { |
| | | width: 3, |
| | | }, |
| | | itemStyle: { |
| | | color: "#67C23A", |
| | | }, |
| | | }, |
| | | { |
| | | name: "æ°é", |
| | | type: "line", |
| | | data: data.map(item => item.æ°é), |
| | | smooth: true, |
| | | lineStyle: { |
| | | width: 3, |
| | | }, |
| | | itemStyle: { |
| | | color: "#E6A23C", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | const costTableData = computed(() => { |
| | | const data = costData.value; |
| | | const materials = data.materials; |
| | | const monthData = data.month; |
| | | const yearData = data.year; |
| | | |
| | | return materials.map((material, index) => ({ |
| | | material, |
| | | unit: "å¨/ç«æ¹ç±³", |
| | | monthlyConsumption: monthData.consumption[index], |
| | | monthlyProduction: monthData.production[index], |
| | | monthlyUnitConsumption: monthData.unitConsumption[index].toFixed(4), |
| | | yearlyConsumption: yearData.consumption[index], |
| | | yearlyProduction: yearData.production[index], |
| | | yearlyUnitConsumption: yearData.unitConsumption[index].toFixed(4), |
| | | })); |
| | | }); |
| | | |
| | | const totalProduction = computed(() => { |
| | | const data = productionData.value[dateType.value]; |
| | | if (dateType.value === "month") { |
| | | return data.reduce( |
| | | (sum, item) => |
| | | sum + item[productType.value === "block" ? "block" : "plate"], |
| | | 0 |
| | | ); |
| | | } else { |
| | | return data[data.length - 1][ |
| | | productType.value === "block" ? "block" : "plate" |
| | | ]; |
| | | } |
| | | }); |
| | | |
| | | const totalSolidWaste = computed(() => { |
| | | const data = solidWasteData.value[dateType.value]; |
| | | if (dateType.value === "month") { |
| | | return data.reduce( |
| | | (sum, item) => sum + item.ç²ç
¤ç° + item.ç³è + item.ç³ç°, |
| | | 0 |
| | | ); |
| | | } else { |
| | | const lastItem = data[data.length - 1]; |
| | | return lastItem.ç²ç
¤ç° + lastItem.ç³è + lastItem.ç³ç°; |
| | | } |
| | | }); |
| | | |
| | | const averageUnitConsumption = computed(() => { |
| | | const data = costData.value; |
| | | const unitConsumption = |
| | | dateType.value === "month" |
| | | ? data.month.unitConsumption |
| | | : data.year.unitConsumption; |
| | | const average = |
| | | unitConsumption.reduce((sum, value) => sum + value, 0) / |
| | | unitConsumption.length; |
| | | return average.toFixed(4); |
| | | }); |
| | | |
| | | const totalEnergy = computed(() => { |
| | | const data = energyData.value[dateType.value]; |
| | | if (dateType.value === "month") { |
| | | return data.reduce( |
| | | (sum, item) => sum + item.çµé + item.æ°´é + item.æ°é, |
| | | 0 |
| | | ); |
| | | } else { |
| | | const lastItem = data[data.length - 1]; |
| | | return lastItem.çµé + lastItem.æ°´é + lastItem.æ°é; |
| | | } |
| | | }); |
| | | |
| | | // äºä»¶å¤ç |
| | | const handleDateTypeChange = () => { |
| | | updateCharts(); |
| | | }; |
| | | |
| | | const handleProductTypeChange = () => { |
| | | updateCharts(); |
| | | }; |
| | | |
| | | // åå§åå¾è¡¨ |
| | | const initCharts = () => { |
| | | if (productionChart.value) { |
| | | productionChartInstance = echarts.init(productionChart.value); |
| | | productionChartInstance.setOption(productionChartOption.value); |
| | | } |
| | | |
| | | if (solidWasteChart.value) { |
| | | solidWasteChartInstance = echarts.init(solidWasteChart.value); |
| | | solidWasteChartInstance.setOption(solidWasteChartOption.value); |
| | | } |
| | | |
| | | if (costChart.value) { |
| | | costChartInstance = echarts.init(costChart.value); |
| | | costChartInstance.setOption(costChartOption.value); |
| | | } |
| | | |
| | | if (energyChart.value) { |
| | | energyChartInstance = echarts.init(energyChart.value); |
| | | energyChartInstance.setOption(energyChartOption.value); |
| | | } |
| | | }; |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | const updateCharts = () => { |
| | | if (productionChartInstance) { |
| | | productionChartInstance.setOption(productionChartOption.value); |
| | | } |
| | | |
| | | if (solidWasteChartInstance) { |
| | | solidWasteChartInstance.setOption(solidWasteChartOption.value); |
| | | } |
| | | |
| | | if (costChartInstance) { |
| | | costChartInstance.setOption(costChartOption.value); |
| | | } |
| | | |
| | | if (energyChartInstance) { |
| | | energyChartInstance.setOption(energyChartOption.value); |
| | | } |
| | | }; |
| | | |
| | | // è°æ´å¾è¡¨å¤§å° |
| | | const resizeCharts = () => { |
| | | productionChartInstance?.resize(); |
| | | solidWasteChartInstance?.resize(); |
| | | costChartInstance?.resize(); |
| | | energyChartInstance?.resize(); |
| | | }; |
| | | |
| | | // çªå£å¤§å°ååå¤ç |
| | | const handleResize = () => { |
| | | // å»¶è¿æ§è¡ï¼ç¡®ä¿DOMæ´æ°å®æ |
| | | setTimeout(() => { |
| | | resizeCharts(); |
| | | }, 100); |
| | | }; |
| | | |
| | | // è·åç©æç±»åæ ç¾ç±»å |
| | | const getMaterialTypeType = material => { |
| | | const typeMap = { |
| | | æ°´æ³¥: "primary", |
| | | éç²è: "success", |
| | | è±æ¨¡å: "warning", |
| | | é²è
å: "danger", |
| | | æ°¯åå: "info", |
| | | å·æä¸: "purple", |
| | | }; |
| | | return typeMap[material] || "info"; |
| | | }; |
| | | |
| | | // çå½å¨æé©å |
| | | onMounted(() => { |
| | | // 使ç¨nextTickç¡®ä¿DOMå®å
¨æ¸²æåååå§å |
| | | nextTick(() => { |
| | | // åå§åå¾è¡¨ |
| | | initCharts(); |
| | | }); |
| | | |
| | | window.addEventListener("resize", handleResize); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | window.removeEventListener("resize", handleResize); |
| | | |
| | | // 鿝å¾è¡¨å®ä¾ |
| | | productionChartInstance?.dispose(); |
| | | solidWasteChartInstance?.dispose(); |
| | | costChartInstance?.dispose(); |
| | | energyChartInstance?.dispose(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | /* å¤é¨å®¹å¨ - å æ®æ´ä¸ªè§å£ */ |
| | | .dashboard-container { |
| | | position: relative; |
| | | width: 100%; |
| | | /* 页é¢å¨å¸¸è§å¸å±ä¸ï¼æé¡¶æ ï¼é»è®¤åå» 84pxï¼é¿å
å
容被è£å */ |
| | | min-height: calc(100vh - 84px); |
| | | background-color: #f5f7fa; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | /* å
é¨å
容åºå - èªéåºå®½åº¦ */ |
| | | .data-dashboard { |
| | | position: relative; |
| | | width: 100%; |
| | | min-height: 100%; |
| | | background-color: #ffffff; |
| | | box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .dashboard-header { |
| | | position: relative; |
| | | z-index: 1; |
| | | height: 86px; |
| | | background-color: #ffffff; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .factory-name { |
| | | font-weight: 600; |
| | | font-size: 32px; |
| | | color: #303133; |
| | | } |
| | | |
| | | .filter-area { |
| | | padding: 20px; |
| | | background-color: #ffffff; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | display: flex; |
| | | gap: 40px; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .filter-section { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .filter-label { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .radio-group { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | /* æé®æ ·å¼ */ |
| | | :deep(.el-radio-button__inner) { |
| | | border-radius: 4px; |
| | | padding: 8px 20px; |
| | | font-size: 14px; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) { |
| | | background-color: #409eff; |
| | | border-color: #409eff; |
| | | color: #ffffff; |
| | | box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3); |
| | | } |
| | | |
| | | :deep(.el-radio-button__inner:hover) { |
| | | color: #409eff; |
| | | border-color: #c6e2ff; |
| | | } |
| | | |
| | | :deep(.el-radio-button:first-child .el-radio-button__inner) { |
| | | border-radius: 4px 0 0 4px; |
| | | } |
| | | |
| | | :deep(.el-radio-button:last-child .el-radio-button__inner) { |
| | | border-radius: 0 4px 4px 0; |
| | | } |
| | | |
| | | .dashboard-content { |
| | | position: relative; |
| | | z-index: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20px; |
| | | padding: 20px; |
| | | min-height: 800px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | /* è¡å¸å± */ |
| | | .row { |
| | | display: flex; |
| | | gap: 20px; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | /* 第ä¸è¡ï¼3个å¡ç */ |
| | | .row-1 { |
| | | height: 300px; |
| | | } |
| | | |
| | | /* 第äºè¡ï¼2个å¡ç */ |
| | | .row-2 { |
| | | height: 300px; |
| | | } |
| | | |
| | | /* 第ä¸è¡ï¼1个å¡ç */ |
| | | .row-3 { |
| | | min-height: 250px; |
| | | } |
| | | |
| | | /* å¡çæ ·å¼ */ |
| | | .panel-card { |
| | | background-color: #ffffff; |
| | | border-radius: 8px; |
| | | border: 1px solid #e4e7ed; |
| | | overflow: hidden; |
| | | display: flex; |
| | | flex-direction: column; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .panel-card:hover { |
| | | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | /* å¡çå¸å± */ |
| | | .card-1 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-2 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-3 { |
| | | flex: 0.8; |
| | | } |
| | | |
| | | .card-4 { |
| | | flex: 1.2; |
| | | } |
| | | |
| | | .card-5 { |
| | | flex: 0.8; |
| | | } |
| | | |
| | | .card-6 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .panel-card { |
| | | background-color: #ffffff; |
| | | border-radius: 8px; |
| | | border: 1px solid #e4e7ed; |
| | | overflow: hidden; |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .panel-title { |
| | | padding: 15px 20px; |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | background-color: #fafafa; |
| | | } |
| | | |
| | | .card-1 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | .card-2 .panel-title { |
| | | border-left: 4px solid #f56c6c; |
| | | } |
| | | |
| | | .card-3 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | .card-4 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | .card-5 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | .card-6 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | .chart-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .table-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | overflow: auto; |
| | | } |
| | | |
| | | .stats-grid { |
| | | flex: 1; |
| | | padding: 15px; |
| | | display: grid; |
| | | grid-template-columns: repeat(2, 1fr); |
| | | grid-template-rows: repeat(2, 1fr); |
| | | gap: 15px; |
| | | } |
| | | |
| | | .stat-item { |
| | | background-color: #fafafa; |
| | | border-radius: 8px; |
| | | padding: 15px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border: 1px solid #e4e7ed; |
| | | min-height: 80px; |
| | | } |
| | | |
| | | .stat-label { |
| | | font-size: 13px; |
| | | color: #606266; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .stat-value { |
| | | font-size: 20px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 3px; |
| | | } |
| | | |
| | | .production-color { |
| | | color: #409eff; |
| | | text-shadow: 0 2px 4px rgba(64, 158, 255, 0.3); |
| | | } |
| | | |
| | | .waste-color { |
| | | color: #f56c6c; |
| | | text-shadow: 0 2px 4px rgba(245, 108, 108, 0.3); |
| | | } |
| | | |
| | | .consumption-color { |
| | | color: #e6a23c; |
| | | text-shadow: 0 2px 4px rgba(230, 162, 60, 0.3); |
| | | } |
| | | |
| | | .energy-color { |
| | | color: #67c23a; |
| | | text-shadow: 0 2px 4px rgba(103, 194, 58, 0.3); |
| | | } |
| | | |
| | | .stat-unit { |
| | | font-size: 11px; |
| | | color: #909399; |
| | | } |
| | | |
| | | /* è¡¨æ ¼æ ·å¼ */ |
| | | :deep(.el-table) { |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | :deep(.el-table th) { |
| | | background-color: #fafafa; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | :deep(.el-table tr:hover > td) { |
| | | background-color: #ecf5ff; |
| | | } |
| | | |
| | | .data-value { |
| | | font-weight: bold; |
| | | color: #409eff; |
| | | } |
| | | |
| | | /* æé®æ ·å¼ */ |
| | | :deep(.el-radio-button__inner) { |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) { |
| | | background-color: #409eff; |
| | | border-color: #409eff; |
| | | color: #ffffff; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="sales-statistics-container"> |
| | | <div class="data-dashboard"> |
| | | <!-- 页颿 é¢ --> |
| | | <!-- <div class="dashboard-header"> |
| | | <div class="factory-name">éå®ç»è®¡çæ¿</div> |
| | | </div> --> |
| | | <!-- ç鿡件 --> |
| | | <div class="filter-area"> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">æ¶é´èå´ï¼</span> |
| | | <el-date-picker v-model="dateRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | @change="handleDateChange" |
| | | style="width: 240px;" /> |
| | | </div> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">产åç±»åï¼</span> |
| | | <el-select v-model="productType" |
| | | placeholder="è¯·éæ©äº§åç±»å" |
| | | @change="handleFilterChange" |
| | | style="width: 160px;"> |
| | | <el-option label="å
¨é¨" |
| | | value="" /> |
| | | <el-option label="ç å" |
| | | value="block" /> |
| | | <el-option label="æ¿æ" |
| | | value="board" /> |
| | | <el-option label="åæ" |
| | | value="profile" /> |
| | | </el-select> |
| | | </div> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">éå®åºåï¼</span> |
| | | <el-select v-model="salesArea" |
| | | placeholder="è¯·éæ©éå®åºå" |
| | | @change="handleFilterChange" |
| | | style="width: 160px;"> |
| | | <el-option label="å
¨é¨" |
| | | value="" /> |
| | | <el-option label="åä¸" |
| | | value="east" /> |
| | | <el-option label="åå" |
| | | value="north" /> |
| | | <el-option label="åå" |
| | | value="south" /> |
| | | <el-option label="西å" |
| | | value="southwest" /> |
| | | <el-option label="西å" |
| | | value="northwest" /> |
| | | </el-select> |
| | | </div> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">ç»è®¡ç»´åº¦ï¼</span> |
| | | <el-select v-model="statDimension" |
| | | placeholder="è¯·éæ©ç»è®¡ç»´åº¦" |
| | | @change="handleFilterChange" |
| | | style="width: 160px;"> |
| | | <el-option label="æåº¦" |
| | | value="month" /> |
| | | <el-option label="年度" |
| | | value="year" /> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | <div class="dashboard-content"> |
| | | <!-- æ ¸å¿ææ å¡ç --> |
| | | <div class="row row-1"> |
| | | <div class="panel-card card-1"> |
| | | <div class="panel-title">å计éé</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value sales-volume-color">{{ totalSalesVolume }}</div> |
| | | <div class="stat-unit">ç«æ¹ç±³</div> |
| | | <div class="stat-change">{{ salesVolumeChange }}%</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-2"> |
| | | <div class="panel-title">éå®éé¢</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value sales-amount-color">{{ totalSalesAmount }}</div> |
| | | <div class="stat-unit">ä¸å
</div> |
| | | <div class="stat-change">{{ salesAmountChange }}%</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-3"> |
| | | <div class="panel-title">æ°å¢å®¢æ·</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value new-customer-color">{{ newCustomerCount }}</div> |
| | | <div class="stat-unit">个</div> |
| | | <div class="stat-change">{{ customerCountChange }}%</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-4"> |
| | | <div class="panel-title">å计客æ·</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value total-customer-color">{{ totalCustomerCount }}</div> |
| | | <div class="stat-unit">个</div> |
| | | <div class="stat-change">{{ totalCustomerChange }}%</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- ééåéå®éé¢è¶å¿ --> |
| | | <div class="row row-2"> |
| | | <div class="panel-card card-5"> |
| | | <div class="panel-title">ééè¶å¿</div> |
| | | <div class="chart-container"> |
| | | <div ref="salesVolumeChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-6"> |
| | | <div class="panel-title">éå®éé¢è¶å¿</div> |
| | | <div class="chart-container"> |
| | | <div ref="salesAmountChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- ç´¯è®¡æ°æ®è¶å¿ --> |
| | | <!-- <div class="row row-3"> |
| | | <div class="panel-card card-10"> |
| | | <div class="panel-title">累计ééè¶å¿</div> |
| | | <div class="chart-container"> |
| | | <div ref="cumulativeSalesVolumeChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-11"> |
| | | <div class="panel-title">累计éå®éé¢è¶å¿</div> |
| | | <div class="chart-container"> |
| | | <div ref="cumulativeSalesAmountChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | </div> --> |
| | | <!-- å¾è¡¨åºååè¡¨æ ¼ --> |
| | | <div class="row row-4"> |
| | | <!-- 左边ï¼è¯¦ç»æ°æ®è¡¨æ ¼ --> |
| | | <div class="panel-card card-9" |
| | | style="flex: 2;"> |
| | | <div class="panel-title">éå®ç»è®¡è¯¦ç»æ°æ®</div> |
| | | <div class="table-container"> |
| | | <el-table :data="tableData" |
| | | style="width: 100%"> |
| | | <el-table-column prop="productType" |
| | | label="产åç±»å" |
| | | width="120" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getProductTypeType(scope.row.productType)"> |
| | | {{ scope.row.productType }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="salesArea" |
| | | label="éå®åºå" |
| | | width="120" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getSalesAreaType(scope.row.salesArea)"> |
| | | {{ scope.row.salesArea }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="period" |
| | | label="ç»è®¡å¨æ" |
| | | width="120" /> |
| | | <el-table-column prop="salesVolume" |
| | | label="éé(ç«æ¹ç±³)" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.salesVolume }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="salesAmount" |
| | | label="éå®éé¢(ä¸å
)" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.salesAmount }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="newCustomers" |
| | | label="æ°å¢å®¢æ·(个)" |
| | | width="150" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.newCustomers }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="totalCustomers" |
| | | label="å计客æ·(个)" |
| | | width="150" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.totalCustomers }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | <!-- å³è¾¹ï¼äº§åç±»ååå¸åéå®åºååå¸ --> |
| | | <div class="chart-column" |
| | | style="flex: 1; display: flex; flex-direction: column; gap: 20px;"> |
| | | <div class="panel-card card-7" |
| | | style="flex: 1;"> |
| | | <div class="panel-title">产åç±»ååå¸</div> |
| | | <div class="chart-container"> |
| | | <div ref="productTypeChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-8" |
| | | style="flex: 1;"> |
| | | <div class="panel-title">éå®åºååå¸</div> |
| | | <div class="chart-container"> |
| | | <div ref="salesAreaChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { |
| | | ref, |
| | | computed, |
| | | onMounted, |
| | | onBeforeUnmount, |
| | | watch, |
| | | nextTick, |
| | | } from "vue"; |
| | | import { useRouter } from "vue-router"; |
| | | import * as echarts from "echarts"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | const router = useRouter(); |
| | | |
| | | // ç鿡件 |
| | | const dateRange = ref([]); |
| | | const productType = ref(""); |
| | | const salesArea = ref(""); |
| | | const statDimension = ref("month"); |
| | | |
| | | // å¾è¡¨å¼ç¨ |
| | | const salesVolumeChart = ref(null); |
| | | const salesAmountChart = ref(null); |
| | | const productTypeChart = ref(null); |
| | | const salesAreaChart = ref(null); |
| | | const cumulativeSalesVolumeChart = ref(null); |
| | | const cumulativeSalesAmountChart = ref(null); |
| | | |
| | | // å¾è¡¨å®ä¾ |
| | | let salesVolumeChartInstance = null; |
| | | let salesAmountChartInstance = null; |
| | | let productTypeChartInstance = null; |
| | | let salesAreaChartInstance = null; |
| | | let cumulativeSalesVolumeChartInstance = null; |
| | | let cumulativeSalesAmountChartInstance = null; |
| | | |
| | | // æ¨¡ææ°æ® |
| | | const mockData = [ |
| | | // 2026å¹´1ææ°æ® |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åä¸", |
| | | period: "2026-01", |
| | | salesVolume: 1200, |
| | | salesAmount: 180, |
| | | newCustomers: 5, |
| | | totalCustomers: 120, |
| | | }, |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åå", |
| | | period: "2026-01", |
| | | salesVolume: 800, |
| | | salesAmount: 120, |
| | | newCustomers: 3, |
| | | totalCustomers: 80, |
| | | }, |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åå", |
| | | period: "2026-01", |
| | | salesVolume: 600, |
| | | salesAmount: 90, |
| | | newCustomers: 2, |
| | | totalCustomers: 60, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "åä¸", |
| | | period: "2026-01", |
| | | salesVolume: 900, |
| | | salesAmount: 270, |
| | | newCustomers: 4, |
| | | totalCustomers: 100, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "åå", |
| | | period: "2026-01", |
| | | salesVolume: 500, |
| | | salesAmount: 150, |
| | | newCustomers: 2, |
| | | totalCustomers: 70, |
| | | }, |
| | | { |
| | | productType: "åæ", |
| | | salesArea: "åä¸", |
| | | period: "2026-01", |
| | | salesVolume: 400, |
| | | salesAmount: 200, |
| | | newCustomers: 3, |
| | | totalCustomers: 50, |
| | | }, |
| | | // 2026å¹´2ææ°æ® |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åä¸", |
| | | period: "2026-02", |
| | | salesVolume: 1300, |
| | | salesAmount: 195, |
| | | newCustomers: 4, |
| | | totalCustomers: 124, |
| | | }, |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åå", |
| | | period: "2026-02", |
| | | salesVolume: 850, |
| | | salesAmount: 127.5, |
| | | newCustomers: 2, |
| | | totalCustomers: 82, |
| | | }, |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åå", |
| | | period: "2026-02", |
| | | salesVolume: 650, |
| | | salesAmount: 97.5, |
| | | newCustomers: 1, |
| | | totalCustomers: 61, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "åä¸", |
| | | period: "2026-02", |
| | | salesVolume: 950, |
| | | salesAmount: 285, |
| | | newCustomers: 3, |
| | | totalCustomers: 103, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "åå", |
| | | period: "2026-02", |
| | | salesVolume: 550, |
| | | salesAmount: 165, |
| | | newCustomers: 1, |
| | | totalCustomers: 71, |
| | | }, |
| | | { |
| | | productType: "åæ", |
| | | salesArea: "åä¸", |
| | | period: "2026-02", |
| | | salesVolume: 450, |
| | | salesAmount: 225, |
| | | newCustomers: 2, |
| | | totalCustomers: 52, |
| | | }, |
| | | // 2026å¹´3ææ°æ® |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åä¸", |
| | | period: "2026-03", |
| | | salesVolume: 1400, |
| | | salesAmount: 210, |
| | | newCustomers: 6, |
| | | totalCustomers: 130, |
| | | }, |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åå", |
| | | period: "2026-03", |
| | | salesVolume: 900, |
| | | salesAmount: 135, |
| | | newCustomers: 3, |
| | | totalCustomers: 85, |
| | | }, |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åå", |
| | | period: "2026-03", |
| | | salesVolume: 700, |
| | | salesAmount: 105, |
| | | newCustomers: 2, |
| | | totalCustomers: 63, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "åä¸", |
| | | period: "2026-03", |
| | | salesVolume: 1000, |
| | | salesAmount: 300, |
| | | newCustomers: 5, |
| | | totalCustomers: 108, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "åå", |
| | | period: "2026-03", |
| | | salesVolume: 600, |
| | | salesAmount: 180, |
| | | newCustomers: 2, |
| | | totalCustomers: 73, |
| | | }, |
| | | { |
| | | productType: "åæ", |
| | | salesArea: "åä¸", |
| | | period: "2026-03", |
| | | salesVolume: 500, |
| | | salesAmount: 250, |
| | | newCustomers: 3, |
| | | totalCustomers: 55, |
| | | }, |
| | | // 西åå西åå°åºæ°æ® |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "西å", |
| | | period: "2026-03", |
| | | salesVolume: 500, |
| | | salesAmount: 75, |
| | | newCustomers: 2, |
| | | totalCustomers: 40, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "西å", |
| | | period: "2026-03", |
| | | salesVolume: 300, |
| | | salesAmount: 90, |
| | | newCustomers: 1, |
| | | totalCustomers: 30, |
| | | }, |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "西å", |
| | | period: "2026-03", |
| | | salesVolume: 400, |
| | | salesAmount: 60, |
| | | newCustomers: 1, |
| | | totalCustomers: 35, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "西å", |
| | | period: "2026-03", |
| | | salesVolume: 200, |
| | | salesAmount: 60, |
| | | newCustomers: 1, |
| | | totalCustomers: 25, |
| | | }, |
| | | ]; |
| | | |
| | | // 计ç®å±æ§ |
| | | const filteredData = computed(() => { |
| | | let result = [...mockData]; |
| | | |
| | | // æäº§åç±»åçé |
| | | if (productType.value) { |
| | | result = result.filter(item => { |
| | | const typeMap = { block: "ç å", board: "æ¿æ", profile: "åæ" }; |
| | | return item.productType === typeMap[productType.value]; |
| | | }); |
| | | } |
| | | |
| | | // æéå®åºåçé |
| | | if (salesArea.value) { |
| | | result = result.filter(item => { |
| | | const areaMap = { |
| | | east: "åä¸", |
| | | north: "åå", |
| | | south: "åå", |
| | | southwest: "西å", |
| | | northwest: "西å", |
| | | }; |
| | | return item.salesArea === areaMap[salesArea.value]; |
| | | }); |
| | | } |
| | | |
| | | // ææ¶é´èå´çé |
| | | if (dateRange.value && dateRange.value.length === 2) { |
| | | const startDate = dayjs(dateRange.value[0]); |
| | | const endDate = dayjs(dateRange.value[1]); |
| | | |
| | | result = result.filter(item => { |
| | | const itemDate = dayjs(item.period); |
| | | return ( |
| | | itemDate.isAfter(startDate.subtract(1, "day")) && |
| | | itemDate.isBefore(endDate.add(1, "day")) |
| | | ); |
| | | }); |
| | | } |
| | | |
| | | return result; |
| | | }); |
| | | |
| | | // æ ¸å¿ææ è®¡ç® |
| | | const totalSalesVolume = computed(() => { |
| | | return filteredData.value.reduce((sum, item) => sum + item.salesVolume, 0); |
| | | }); |
| | | |
| | | const totalSalesAmount = computed(() => { |
| | | return filteredData.value |
| | | .reduce((sum, item) => sum + item.salesAmount, 0) |
| | | .toFixed(2); |
| | | }); |
| | | |
| | | const newCustomerCount = computed(() => { |
| | | return filteredData.value.reduce((sum, item) => sum + item.newCustomers, 0); |
| | | }); |
| | | |
| | | const totalCustomerCount = computed(() => { |
| | | // è®¡ç®æ¯ä¸ªåºåå产åç±»åçæå¤§å®¢æ·æ° |
| | | const customerMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | const key = `${item.productType}-${item.salesArea}`; |
| | | if (!customerMap[key] || item.totalCustomers > customerMap[key]) { |
| | | customerMap[key] = item.totalCustomers; |
| | | } |
| | | }); |
| | | return Object.values(customerMap).reduce((sum, count) => sum + count, 0); |
| | | }); |
| | | |
| | | // ååç计ç®ï¼æ¨¡æï¼ |
| | | const salesVolumeChange = ref("+5.2"); |
| | | const salesAmountChange = ref("+7.8"); |
| | | const customerCountChange = ref("+3.5"); |
| | | const totalCustomerChange = ref("+2.1"); |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | const tableData = computed(() => { |
| | | return filteredData.value.map(item => { |
| | | // 计ç®ç´¯è®¡å¼ï¼æ¨¡æï¼ |
| | | const cumulativeSalesVolume = item.salesVolume * 1.5; |
| | | const cumulativeSalesAmount = item.salesAmount * 1.5; |
| | | const cumulativeNewCustomers = item.newCustomers * 2; |
| | | |
| | | return { |
| | | ...item, |
| | | cumulativeSalesVolume, |
| | | cumulativeSalesAmount, |
| | | cumulativeNewCustomers, |
| | | }; |
| | | }); |
| | | }); |
| | | |
| | | // ééè¶å¿å¾è¡¨é
ç½® |
| | | const salesVolumeChartOption = computed(() => { |
| | | // æå¨æåç» |
| | | const periodMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!periodMap[item.period]) { |
| | | periodMap[item.period] = 0; |
| | | } |
| | | periodMap[item.period] += item.salesVolume; |
| | | }); |
| | | |
| | | const periods = Object.keys(periodMap).sort(); |
| | | const values = periods.map(period => periodMap[period]); |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: "axis", |
| | | formatter: "{b}: {c} ç«æ¹ç±³", |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "ééï¼ç«æ¹ç±³ï¼", |
| | | }, |
| | | series: [ |
| | | { |
| | | data: values, |
| | | type: "line", |
| | | smooth: true, |
| | | lineStyle: { |
| | | width: 3, |
| | | }, |
| | | itemStyle: { |
| | | color: "#409EFF", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // éå®éé¢è¶å¿å¾è¡¨é
ç½® |
| | | const salesAmountChartOption = computed(() => { |
| | | // æå¨æåç» |
| | | const periodMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!periodMap[item.period]) { |
| | | periodMap[item.period] = 0; |
| | | } |
| | | periodMap[item.period] += item.salesAmount; |
| | | }); |
| | | |
| | | const periods = Object.keys(periodMap).sort(); |
| | | const values = periods.map(period => periodMap[period]); |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: "axis", |
| | | formatter: "{b}: {c} ä¸å
", |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "éå®éé¢ï¼ä¸å
ï¼", |
| | | }, |
| | | series: [ |
| | | { |
| | | data: values, |
| | | type: "bar", |
| | | itemStyle: { |
| | | color: "#67C23A", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // 产åç±»ååå¸å¾è¡¨é
ç½® |
| | | const productTypeChartOption = computed(() => { |
| | | // æäº§åç±»ååç» |
| | | const typeMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!typeMap[item.productType]) { |
| | | typeMap[item.productType] = 0; |
| | | } |
| | | typeMap[item.productType] += item.salesVolume; |
| | | }); |
| | | |
| | | const types = Object.keys(typeMap); |
| | | const values = types.map(type => typeMap[type]); |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: "item", |
| | | formatter: "{b}: {c} ç«æ¹ç±³ ({d}%)", |
| | | }, |
| | | series: [ |
| | | { |
| | | type: "pie", |
| | | radius: "60%", |
| | | data: types.map((type, index) => ({ |
| | | name: type, |
| | | value: values[index], |
| | | })), |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: "rgba(0, 0, 0, 0.5)", |
| | | }, |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // éå®åºååå¸å¾è¡¨é
ç½® |
| | | const salesAreaChartOption = computed(() => { |
| | | // æéå®åºååç» |
| | | const areaMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!areaMap[item.salesArea]) { |
| | | areaMap[item.salesArea] = 0; |
| | | } |
| | | areaMap[item.salesArea] += item.salesVolume; |
| | | }); |
| | | |
| | | const areas = Object.keys(areaMap); |
| | | const values = areas.map(area => areaMap[area]); |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: "item", |
| | | formatter: "{b}: {c} ç«æ¹ç±³ ({d}%)", |
| | | }, |
| | | series: [ |
| | | { |
| | | type: "pie", |
| | | radius: "60%", |
| | | data: areas.map((area, index) => ({ |
| | | name: area, |
| | | value: values[index], |
| | | })), |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: "rgba(0, 0, 0, 0.5)", |
| | | }, |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // 累计ééè¶å¿å¾è¡¨é
ç½® |
| | | const cumulativeSalesVolumeChartOption = computed(() => { |
| | | // æå¨æåç» |
| | | const periodMap = {}; |
| | | let cumulativeValue = 0; |
| | | |
| | | // æå¨ææåº |
| | | const sortedData = [...filteredData.value].sort((a, b) => |
| | | a.period.localeCompare(b.period) |
| | | ); |
| | | |
| | | sortedData.forEach(item => { |
| | | cumulativeValue += item.salesVolume; |
| | | periodMap[item.period] = cumulativeValue; |
| | | }); |
| | | |
| | | const periods = Object.keys(periodMap).sort(); |
| | | const values = periods.map(period => periodMap[period]); |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: "axis", |
| | | formatter: "{b}: {c} ç«æ¹ç±³", |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "累计ééï¼ç«æ¹ç±³ï¼", |
| | | }, |
| | | series: [ |
| | | { |
| | | data: values, |
| | | type: "line", |
| | | smooth: true, |
| | | areaStyle: { |
| | | opacity: 0.3, |
| | | }, |
| | | itemStyle: { |
| | | color: "#E6A23C", |
| | | }, |
| | | lineStyle: { |
| | | width: 3, |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // 累计éå®éé¢è¶å¿å¾è¡¨é
ç½® |
| | | const cumulativeSalesAmountChartOption = computed(() => { |
| | | // æå¨æåç» |
| | | const periodMap = {}; |
| | | let cumulativeValue = 0; |
| | | |
| | | // æå¨ææåº |
| | | const sortedData = [...filteredData.value].sort((a, b) => |
| | | a.period.localeCompare(b.period) |
| | | ); |
| | | |
| | | sortedData.forEach(item => { |
| | | cumulativeValue += item.salesAmount; |
| | | periodMap[item.period] = cumulativeValue; |
| | | }); |
| | | |
| | | const periods = Object.keys(periodMap).sort(); |
| | | const values = periods.map(period => periodMap[period]); |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: "axis", |
| | | formatter: "{b}: {c} ä¸å
", |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "累计éå®éé¢ï¼ä¸å
ï¼", |
| | | }, |
| | | series: [ |
| | | { |
| | | data: values, |
| | | type: "bar", |
| | | itemStyle: { |
| | | color: "#F56C6C", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // æ¹æ³ |
| | | const goBack = () => { |
| | | router.back(); |
| | | }; |
| | | |
| | | const handleDateChange = () => { |
| | | // å¤çæ¥æåå |
| | | updateCharts(); |
| | | }; |
| | | |
| | | const handleFilterChange = () => { |
| | | // å¤çç鿡件åå |
| | | updateCharts(); |
| | | }; |
| | | |
| | | // åå§åå¾è¡¨ |
| | | const initCharts = () => { |
| | | // åå§åééè¶å¿å¾è¡¨ |
| | | if (salesVolumeChart.value && !salesVolumeChartInstance) { |
| | | salesVolumeChartInstance = echarts.init(salesVolumeChart.value); |
| | | } |
| | | |
| | | // åå§åéå®éé¢è¶å¿å¾è¡¨ |
| | | if (salesAmountChart.value && !salesAmountChartInstance) { |
| | | salesAmountChartInstance = echarts.init(salesAmountChart.value); |
| | | } |
| | | |
| | | // åå§å产åç±»ååå¸å¾è¡¨ |
| | | if (productTypeChart.value && !productTypeChartInstance) { |
| | | productTypeChartInstance = echarts.init(productTypeChart.value); |
| | | } |
| | | |
| | | // åå§åéå®åºååå¸å¾è¡¨ |
| | | if (salesAreaChart.value && !salesAreaChartInstance) { |
| | | salesAreaChartInstance = echarts.init(salesAreaChart.value); |
| | | } |
| | | |
| | | // åå§å累计ééè¶å¿å¾è¡¨ |
| | | if (cumulativeSalesVolumeChart.value && !cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance = echarts.init( |
| | | cumulativeSalesVolumeChart.value |
| | | ); |
| | | } |
| | | |
| | | // åå§å累计éå®éé¢è¶å¿å¾è¡¨ |
| | | if (cumulativeSalesAmountChart.value && !cumulativeSalesAmountChartInstance) { |
| | | cumulativeSalesAmountChartInstance = echarts.init( |
| | | cumulativeSalesAmountChart.value |
| | | ); |
| | | } |
| | | |
| | | updateCharts(); |
| | | }; |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | const updateCharts = () => { |
| | | // æ´æ°ééè¶å¿å¾è¡¨ |
| | | if (salesVolumeChartInstance) { |
| | | salesVolumeChartInstance.setOption(salesVolumeChartOption.value); |
| | | } |
| | | |
| | | // æ´æ°éå®éé¢è¶å¿å¾è¡¨ |
| | | if (salesAmountChartInstance) { |
| | | salesAmountChartInstance.setOption(salesAmountChartOption.value); |
| | | } |
| | | |
| | | // æ´æ°äº§åç±»ååå¸å¾è¡¨ |
| | | if (productTypeChartInstance) { |
| | | productTypeChartInstance.setOption(productTypeChartOption.value); |
| | | } |
| | | |
| | | // æ´æ°éå®åºååå¸å¾è¡¨ |
| | | if (salesAreaChartInstance) { |
| | | salesAreaChartInstance.setOption(salesAreaChartOption.value); |
| | | } |
| | | |
| | | // æ´æ°ç´¯è®¡ééè¶å¿å¾è¡¨ |
| | | if (cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance.setOption( |
| | | cumulativeSalesVolumeChartOption.value |
| | | ); |
| | | } |
| | | |
| | | // æ´æ°ç´¯è®¡éå®éé¢è¶å¿å¾è¡¨ |
| | | if (cumulativeSalesAmountChartInstance) { |
| | | cumulativeSalesAmountChartInstance.setOption( |
| | | cumulativeSalesAmountChartOption.value |
| | | ); |
| | | } |
| | | }; |
| | | |
| | | // çå¬çªå£å¤§å°åå |
| | | const handleResize = () => { |
| | | if (salesVolumeChartInstance) { |
| | | salesVolumeChartInstance.resize(); |
| | | } |
| | | if (salesAmountChartInstance) { |
| | | salesAmountChartInstance.resize(); |
| | | } |
| | | if (productTypeChartInstance) { |
| | | productTypeChartInstance.resize(); |
| | | } |
| | | if (salesAreaChartInstance) { |
| | | salesAreaChartInstance.resize(); |
| | | } |
| | | if (cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance.resize(); |
| | | } |
| | | if (cumulativeSalesAmountChartInstance) { |
| | | cumulativeSalesAmountChartInstance.resize(); |
| | | } |
| | | }; |
| | | |
| | | // çå½å¨æ |
| | | onMounted(() => { |
| | | // 设置é»è®¤æ¥æèå´ä¸ºæè¿3个æ |
| | | const endDate = dayjs(); |
| | | const startDate = endDate.subtract(3, "month"); |
| | | dateRange.value = [ |
| | | startDate.format("YYYY-MM-DD"), |
| | | endDate.format("YYYY-MM-DD"), |
| | | ]; |
| | | |
| | | // çå¾
DOMæ´æ°ååå§åå¾è¡¨ |
| | | nextTick(() => { |
| | | initCharts(); |
| | | }); |
| | | |
| | | // æ·»å çªå£å¤§å°ååçå¬ |
| | | window.addEventListener("resize", handleResize); |
| | | }); |
| | | |
| | | // è·å产åç±»åæ ç¾ç±»å |
| | | const getProductTypeType = type => { |
| | | const typeMap = { |
| | | ç å: "primary", |
| | | æ¿æ: "success", |
| | | åæ: "warning", |
| | | }; |
| | | return typeMap[type] || "info"; |
| | | }; |
| | | |
| | | // è·åéå®åºåæ ç¾ç±»å |
| | | const getSalesAreaType = area => { |
| | | const typeMap = { |
| | | åä¸: "primary", |
| | | åå: "success", |
| | | åå: "warning", |
| | | 西å: "danger", |
| | | 西å: "info", |
| | | }; |
| | | return typeMap[area] || "info"; |
| | | }; |
| | | |
| | | // ç»ä»¶å¸è½½æ¶éæ¯å¾è¡¨å®ä¾ |
| | | onBeforeUnmount(() => { |
| | | if (salesVolumeChartInstance) { |
| | | salesVolumeChartInstance.dispose(); |
| | | } |
| | | if (salesAmountChartInstance) { |
| | | salesAmountChartInstance.dispose(); |
| | | } |
| | | if (productTypeChartInstance) { |
| | | productTypeChartInstance.dispose(); |
| | | } |
| | | if (salesAreaChartInstance) { |
| | | salesAreaChartInstance.dispose(); |
| | | } |
| | | if (cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance.dispose(); |
| | | } |
| | | if (cumulativeSalesAmountChartInstance) { |
| | | cumulativeSalesAmountChartInstance.dispose(); |
| | | } |
| | | |
| | | // ç§»é¤çªå£å¤§å°ååçå¬ |
| | | window.removeEventListener("resize", handleResize); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | /* å¤é¨å®¹å¨ - å æ®æ´ä¸ªè§å£ */ |
| | | .sales-statistics-container { |
| | | position: relative; |
| | | width: 100%; |
| | | /* 页é¢å¨å¸¸è§å¸å±ä¸ï¼æé¡¶æ ï¼é»è®¤åå» 84pxï¼é¿å
å
容被è£å */ |
| | | min-height: calc(100vh - 84px); |
| | | background-color: #f5f7fa; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | /* å
é¨å
容åºå - èªéåºå®½åº¦ */ |
| | | .data-dashboard { |
| | | position: relative; |
| | | width: 100%; |
| | | min-height: 100%; |
| | | background-color: #ffffff; |
| | | box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .filter-area { |
| | | padding: 20px; |
| | | background-color: #ffffff; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | display: flex; |
| | | gap: 40px; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .filter-section { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .filter-label { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .dashboard-content { |
| | | position: relative; |
| | | z-index: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20px; |
| | | padding: 20px; |
| | | min-height: 800px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | /* è¡å¸å± */ |
| | | .row { |
| | | display: flex; |
| | | gap: 20px; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | /* 第ä¸è¡ï¼4ä¸ªææ å¡ç */ |
| | | .row-1 { |
| | | height: 180px; |
| | | } |
| | | |
| | | /* 第äºè¡ï¼2个è¶å¿å¾è¡¨ */ |
| | | .row-2 { |
| | | height: 350px; |
| | | } |
| | | |
| | | /* 第ä¸è¡ï¼ç´¯è®¡æ°æ®è¶å¿ */ |
| | | .row-3 { |
| | | height: 350px; |
| | | } |
| | | |
| | | /* 第åè¡ï¼è¡¨æ ¼åå¾è¡¨ */ |
| | | .row-4 { |
| | | height: 600px; |
| | | } |
| | | |
| | | /* å¡çæ ·å¼ */ |
| | | .panel-card { |
| | | background-color: #ffffff; |
| | | border-radius: 8px; |
| | | border: 1px solid #e4e7ed; |
| | | overflow: hidden; |
| | | display: flex; |
| | | flex-direction: column; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .panel-card:hover { |
| | | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | /* å¡çå¸å± */ |
| | | .card-1 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-2 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-3 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-4 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-5 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-6 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-7 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-8 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-9 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-10 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-11 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .panel-title { |
| | | padding: 15px 20px; |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | background-color: #fafafa; |
| | | } |
| | | |
| | | .card-1 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | .card-2 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | .card-3 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | .card-4 .panel-title { |
| | | border-left: 4px solid #f56c6c; |
| | | } |
| | | |
| | | .card-5 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | .card-6 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | .card-7 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | .card-8 .panel-title { |
| | | border-left: 4px solid #f56c6c; |
| | | } |
| | | |
| | | .card-9 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | .card-10 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | .card-11 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | .chart-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .table-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | overflow: auto; |
| | | } |
| | | |
| | | .stats-grid { |
| | | flex: 1; |
| | | padding: 15px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .stat-item { |
| | | background-color: #fafafa; |
| | | border-radius: 8px; |
| | | padding: 15px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border: 1px solid #e4e7ed; |
| | | min-height: 80px; |
| | | width: 100%; |
| | | } |
| | | |
| | | .stat-value { |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 5px; |
| | | } |
| | | |
| | | .sales-volume-color { |
| | | color: #409eff; |
| | | text-shadow: 0 2px 4px rgba(64, 158, 255, 0.3); |
| | | } |
| | | |
| | | .sales-amount-color { |
| | | color: #67c23a; |
| | | text-shadow: 0 2px 4px rgba(103, 194, 58, 0.3); |
| | | } |
| | | |
| | | .new-customer-color { |
| | | color: #e6a23c; |
| | | text-shadow: 0 2px 4px rgba(230, 162, 60, 0.3); |
| | | } |
| | | |
| | | .total-customer-color { |
| | | color: #f56c6c; |
| | | text-shadow: 0 2px 4px rgba(245, 108, 108, 0.3); |
| | | } |
| | | |
| | | .stat-unit { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-bottom: 3px; |
| | | } |
| | | |
| | | .stat-change { |
| | | font-size: 12px; |
| | | color: #67c23a; |
| | | } |
| | | |
| | | /* è¡¨æ ¼æ ·å¼ */ |
| | | :deep(.el-table) { |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | :deep(.el-table th) { |
| | | background-color: #fafafa; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | :deep(.el-table tr:hover > td) { |
| | | background-color: #ecf5ff; |
| | | } |
| | | |
| | | .data-value { |
| | | font-weight: bold; |
| | | color: #409eff; |
| | | } |
| | | |
| | | /* ä¸æéæ©æ¡æ ·å¼ */ |
| | | :deep(.el-select) { |
| | | width: 100%; |
| | | } |
| | | |
| | | :deep(.el-date-picker) { |
| | | width: 100%; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="dashboard-container"> |
| | | <div class="data-dashboard"> |
| | | <!-- çéåºå --> |
| | | <div class="filter-area"> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">æ¶é´ç»´åº¦ï¼</span> |
| | | <el-radio-group v-model="dateType" |
| | | @change="handleDateTypeChange" |
| | | class="radio-group"> |
| | | <el-radio-button label="month">æåº¦</el-radio-button> |
| | | <el-radio-button label="year">年度</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | </div> |
| | | <!-- 主è¦å
容åºå --> |
| | | <div class="dashboard-content"> |
| | | <!-- 第ä¸è¡ï¼æ ¸å¿ææ --> |
| | | <div class="row row-1"> |
| | | <div class="panel-card card-1"> |
| | | <div class="panel-title">æ ¸å¿ææ </div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-label">å计é</div> |
| | | <div class="stat-value">{{ totalSolidWaste }}</div> |
| | | <div class="stat-unit">å¨</div> |
| | | </div> |
| | | <div class="stat-item"> |
| | | <div class="stat-label">2022å¹´è³ä»ç´¯è®¡æ¶çº³é</div> |
| | | <div class="stat-value">{{ totalSolidWasteSince2022 }}</div> |
| | | <div class="stat-unit">å¨</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- 第äºè¡ï¼åºåºæ¶çº³è¶å¿ --> |
| | | <div class="row row-2"> |
| | | <div class="panel-card card-2"> |
| | | <div class="panel-title">åºåºæ¶çº³è¶å¿</div> |
| | | <div class="chart-container"> |
| | | <div ref="trendChart" |
| | | style="width: 100%; height: 100%"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- 第ä¸è¡ï¼åºåºç±»ååå¸ --> |
| | | <div class="row row-3"> |
| | | <div class="panel-card card-3"> |
| | | <div class="panel-title">åºåºç±»ååå¸</div> |
| | | <div class="chart-container"> |
| | | <div ref="distributionChart" |
| | | style="width: 100%; height: 100%"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- 第åè¡ï¼æ¶çº³éæç» --> |
| | | <div class="row row-4"> |
| | | <div class="panel-card card-4"> |
| | | <div class="panel-title">æ¶çº³éæç»</div> |
| | | <div class="table-container"> |
| | | <el-table :data="wasteTableData" |
| | | style="width: 100%"> |
| | | <el-table-column prop="time" |
| | | label="æ¶é´" |
| | | width="120" /> |
| | | <el-table-column prop="type" |
| | | label="åºåºç±»å" |
| | | width="120" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getWasteTypeType(scope.row.type)"> |
| | | {{ scope.row.type }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="quantity" |
| | | label="æ¶çº³é" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.quantity }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="unit" |
| | | label="åä½" |
| | | width="80" /> |
| | | <el-table-column prop="source" |
| | | label="æ¥æº" /> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted, onBeforeUnmount, nextTick } from "vue"; |
| | | import * as echarts from "echarts"; |
| | | |
| | | // ç鿡件 |
| | | const dateType = ref("month"); // month æ year |
| | | |
| | | // å¾è¡¨å¼ç¨ |
| | | const trendChart = ref(null); |
| | | const distributionChart = ref(null); |
| | | |
| | | // å¾è¡¨å®ä¾ |
| | | let trendChartInstance = null; |
| | | let distributionChartInstance = null; |
| | | |
| | | // æ¨¡ææ°æ® |
| | | const solidWasteData = ref({ |
| | | month: [ |
| | | { name: "1æ", ç²ç
¤ç°: 200, ç³è: 150, ç³ç°: 100 }, |
| | | { name: "2æ", ç²ç
¤ç°: 220, ç³è: 160, ç³ç°: 110 }, |
| | | { name: "3æ", ç²ç
¤ç°: 190, ç³è: 140, ç³ç°: 95 }, |
| | | { name: "4æ", ç²ç
¤ç°: 230, ç³è: 170, ç³ç°: 115 }, |
| | | { name: "5æ", ç²ç
¤ç°: 240, ç³è: 180, ç³ç°: 120 }, |
| | | { name: "6æ", ç²ç
¤ç°: 225, ç³è: 165, ç³ç°: 112 }, |
| | | ], |
| | | year: [ |
| | | { name: "2022", ç²ç
¤ç°: 2300, ç³è: 1700, ç³ç°: 1100 }, |
| | | { name: "2023", ç²ç
¤ç°: 2500, ç³è: 1800, ç³ç°: 1200 }, |
| | | { name: "2024", ç²ç
¤ç°: 2700, ç³è: 1950, ç³ç°: 1300 }, |
| | | { name: "2025", ç²ç
¤ç°: 2900, ç³è: 2100, ç³ç°: 1400 }, |
| | | ], |
| | | }); |
| | | |
| | | // 计ç®å±æ§ |
| | | const totalSolidWaste = computed(() => { |
| | | const data = solidWasteData.value[dateType.value]; |
| | | if (dateType.value === "month") { |
| | | return data.reduce( |
| | | (sum, item) => sum + item.ç²ç
¤ç° + item.ç³è + item.ç³ç°, |
| | | 0 |
| | | ); |
| | | } else { |
| | | const lastItem = data[data.length - 1]; |
| | | return lastItem.ç²ç
¤ç° + lastItem.ç³è + lastItem.ç³ç°; |
| | | } |
| | | }); |
| | | |
| | | const totalSolidWasteSince2022 = computed(() => { |
| | | const data = solidWasteData.value.year; |
| | | return data.reduce( |
| | | (sum, item) => sum + item.ç²ç
¤ç° + item.ç³è + item.ç³ç°, |
| | | 0 |
| | | ); |
| | | }); |
| | | |
| | | const wasteTableData = computed(() => { |
| | | const data = solidWasteData.value[dateType.value]; |
| | | const result = []; |
| | | |
| | | data.forEach(item => { |
| | | result.push({ |
| | | time: item.name, |
| | | type: "ç²ç
¤ç°", |
| | | quantity: item.ç²ç
¤ç°, |
| | | unit: "å¨", |
| | | source: "ç产è¿ç¨", |
| | | }); |
| | | result.push({ |
| | | time: item.name, |
| | | type: "ç³è", |
| | | quantity: item.ç³è, |
| | | unit: "å¨", |
| | | source: "ç产è¿ç¨", |
| | | }); |
| | | result.push({ |
| | | time: item.name, |
| | | type: "ç³ç°", |
| | | quantity: item.ç³ç°, |
| | | unit: "å¨", |
| | | source: "ç产è¿ç¨", |
| | | }); |
| | | }); |
| | | |
| | | return result; |
| | | }); |
| | | |
| | | // å¾è¡¨é
ç½® |
| | | const trendChartOption = computed(() => { |
| | | const data = solidWasteData.value[dateType.value]; |
| | | return { |
| | | tooltip: { |
| | | trigger: "axis", |
| | | axisPointer: { |
| | | type: "shadow", |
| | | }, |
| | | }, |
| | | legend: { |
| | | data: ["ç²ç
¤ç°", "ç³è", "ç³ç°"], |
| | | textStyle: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | grid: { |
| | | left: "3%", |
| | | right: "4%", |
| | | bottom: "3%", |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: data.map(item => item.name), |
| | | axisLabel: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "æ¶çº³é (å¨)", |
| | | axisLabel: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "ç²ç
¤ç°", |
| | | type: "bar", |
| | | data: data.map(item => item.ç²ç
¤ç°), |
| | | itemStyle: { |
| | | color: "#909399", |
| | | }, |
| | | }, |
| | | { |
| | | name: "ç³è", |
| | | type: "bar", |
| | | data: data.map(item => item.ç³è), |
| | | itemStyle: { |
| | | color: "#E6A23C", |
| | | }, |
| | | }, |
| | | { |
| | | name: "ç³ç°", |
| | | type: "bar", |
| | | data: data.map(item => item.ç³ç°), |
| | | itemStyle: { |
| | | color: "#F56C6C", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | const distributionChartOption = computed(() => { |
| | | const data = solidWasteData.value[dateType.value]; |
| | | const lastItem = data[data.length - 1]; |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: "item", |
| | | formatter: "{a} <br/>{b}: {c} ({d}%)", |
| | | }, |
| | | legend: { |
| | | orient: "vertical", |
| | | left: "left", |
| | | textStyle: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "åºåºç±»å", |
| | | type: "pie", |
| | | radius: "60%", |
| | | center: ["50%", "50%"], |
| | | data: [ |
| | | { value: lastItem.ç²ç
¤ç°, name: "ç²ç
¤ç°" }, |
| | | { value: lastItem.ç³è, name: "ç³è" }, |
| | | { value: lastItem.ç³ç°, name: "ç³ç°" }, |
| | | ], |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: "rgba(0, 0, 0, 0.5)", |
| | | }, |
| | | }, |
| | | itemStyle: { |
| | | color: function (params) { |
| | | const colors = ["#909399", "#E6A23C", "#F56C6C"]; |
| | | return colors[params.dataIndex]; |
| | | }, |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // äºä»¶å¤ç |
| | | const handleDateTypeChange = () => { |
| | | updateCharts(); |
| | | }; |
| | | |
| | | // åå§åå¾è¡¨ |
| | | const initCharts = () => { |
| | | if (trendChart.value) { |
| | | trendChartInstance = echarts.init(trendChart.value); |
| | | trendChartInstance.setOption(trendChartOption.value); |
| | | } |
| | | |
| | | if (distributionChart.value) { |
| | | distributionChartInstance = echarts.init(distributionChart.value); |
| | | distributionChartInstance.setOption(distributionChartOption.value); |
| | | } |
| | | }; |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | const updateCharts = () => { |
| | | if (trendChartInstance) { |
| | | trendChartInstance.setOption(trendChartOption.value); |
| | | } |
| | | |
| | | if (distributionChartInstance) { |
| | | distributionChartInstance.setOption(distributionChartOption.value); |
| | | } |
| | | }; |
| | | |
| | | // è°æ´å¾è¡¨å¤§å° |
| | | const resizeCharts = () => { |
| | | trendChartInstance?.resize(); |
| | | distributionChartInstance?.resize(); |
| | | }; |
| | | |
| | | // çªå£å¤§å°ååå¤ç |
| | | const handleResize = () => { |
| | | // å»¶è¿æ§è¡ï¼ç¡®ä¿DOMæ´æ°å®æ |
| | | setTimeout(() => { |
| | | resizeCharts(); |
| | | }, 100); |
| | | }; |
| | | |
| | | // è·ååºåºç±»åæ ç¾ç±»å |
| | | const getWasteTypeType = type => { |
| | | const typeMap = { |
| | | ç²ç
¤ç°: "info", |
| | | ç³è: "warning", |
| | | ç³ç°: "danger", |
| | | }; |
| | | return typeMap[type] || "info"; |
| | | }; |
| | | |
| | | // çå½å¨æé©å |
| | | onMounted(() => { |
| | | // 使ç¨nextTickç¡®ä¿DOMå®å
¨æ¸²æåååå§å |
| | | nextTick(() => { |
| | | // åå§åå¾è¡¨ |
| | | initCharts(); |
| | | }); |
| | | |
| | | window.addEventListener("resize", handleResize); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | window.removeEventListener("resize", handleResize); |
| | | |
| | | // 鿝å¾è¡¨å®ä¾ |
| | | trendChartInstance?.dispose(); |
| | | distributionChartInstance?.dispose(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | /* å¤é¨å®¹å¨ - å æ®æ´ä¸ªè§å£ */ |
| | | .dashboard-container { |
| | | position: relative; |
| | | width: 100%; |
| | | /* 页é¢å¨å¸¸è§å¸å±ä¸ï¼æé¡¶æ ï¼é»è®¤åå» 84pxï¼é¿å
å
容被è£å */ |
| | | min-height: calc(100vh - 84px); |
| | | background-color: #f5f7fa; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | /* å
é¨å
容åºå - èªéåºå®½åº¦ */ |
| | | .data-dashboard { |
| | | position: relative; |
| | | width: 100%; |
| | | min-height: 100%; |
| | | background-color: #ffffff; |
| | | box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .filter-area { |
| | | padding: 20px; |
| | | background-color: #ffffff; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | display: flex; |
| | | gap: 40px; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .filter-section { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .filter-label { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .radio-group { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | /* æé®æ ·å¼ */ |
| | | :deep(.el-radio-button__inner) { |
| | | border-radius: 4px; |
| | | padding: 8px 20px; |
| | | font-size: 14px; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) { |
| | | background-color: #409eff; |
| | | border-color: #409eff; |
| | | color: #ffffff; |
| | | box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3); |
| | | } |
| | | |
| | | :deep(.el-radio-button__inner:hover) { |
| | | color: #409eff; |
| | | border-color: #c6e2ff; |
| | | } |
| | | |
| | | :deep(.el-radio-button:first-child .el-radio-button__inner) { |
| | | border-radius: 4px 0 0 4px; |
| | | } |
| | | |
| | | :deep(.el-radio-button:last-child .el-radio-button__inner) { |
| | | border-radius: 0 4px 4px 0; |
| | | } |
| | | |
| | | .dashboard-content { |
| | | position: relative; |
| | | z-index: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20px; |
| | | padding: 20px; |
| | | min-height: 800px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | /* è¡å¸å± */ |
| | | .row { |
| | | display: flex; |
| | | gap: 20px; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | /* 第ä¸è¡ï¼æ ¸å¿ææ */ |
| | | .row-1 { |
| | | height: 250px; |
| | | } |
| | | |
| | | /* 第äºè¡ï¼åºåºæ¶çº³è¶å¿ */ |
| | | .row-2 { |
| | | height: 300px; |
| | | } |
| | | |
| | | /* 第ä¸è¡ï¼åºåºç±»ååå¸ */ |
| | | .row-3 { |
| | | height: 300px; |
| | | } |
| | | |
| | | /* 第åè¡ï¼æ¶çº³éæç» */ |
| | | .row-4 { |
| | | min-height: 250px; |
| | | } |
| | | |
| | | /* å¡çæ ·å¼ */ |
| | | .panel-card { |
| | | background-color: #ffffff; |
| | | border-radius: 8px; |
| | | border: 1px solid #e4e7ed; |
| | | overflow: hidden; |
| | | display: flex; |
| | | flex-direction: column; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .panel-card:hover { |
| | | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | /* å¡çå¸å± */ |
| | | .card-1 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-2 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-3 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-4 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .panel-title { |
| | | padding: 15px 20px; |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | background-color: #fafafa; |
| | | } |
| | | |
| | | .card-1 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | .card-2 .panel-title { |
| | | border-left: 4px solid #f56c6c; |
| | | } |
| | | |
| | | .card-3 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | .card-4 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | .chart-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .table-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | overflow: auto; |
| | | } |
| | | |
| | | .stats-grid { |
| | | flex: 1; |
| | | padding: 15px; |
| | | display: grid; |
| | | grid-template-columns: repeat(2, 1fr); |
| | | gap: 15px; |
| | | } |
| | | |
| | | .stat-item { |
| | | background-color: #ffffff; |
| | | border-radius: 12px; |
| | | padding: 25px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border: 2px solid #e4e7ed; |
| | | min-height: 120px; |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .stat-item:hover { |
| | | box-shadow: 0 8px 24px rgba(64, 158, 255, 0.2); |
| | | border-color: #409eff; |
| | | transform: translateY(-4px); |
| | | } |
| | | |
| | | .stat-label { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .stat-value { |
| | | font-size: 32px; |
| | | font-weight: 700; |
| | | color: #409eff; |
| | | margin-bottom: 5px; |
| | | text-shadow: 0 2px 4px rgba(64, 158, 255, 0.3); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .stat-value:hover { |
| | | transform: scale(1.05); |
| | | } |
| | | |
| | | .stat-unit { |
| | | font-size: 12px; |
| | | font-weight: 500; |
| | | color: #606266; |
| | | } |
| | | |
| | | /* è¡¨æ ¼æ ·å¼ */ |
| | | :deep(.el-table) { |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | :deep(.el-table th) { |
| | | background-color: #fafafa; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | :deep(.el-table tr:hover > td) { |
| | | background-color: #ecf5ff; |
| | | } |
| | | |
| | | .data-value { |
| | | font-weight: bold; |
| | | color: #409eff; |
| | | } |
| | | </style> |