Merge branch 'dev_New' into dev_天津军泰伟业
| | |
| | | // 首页接口 |
| | | import request from "@/utils/request"; |
| | | |
| | | // 原材料检测 |
| | | export const rawMaterialDetection = (query) => { |
| | | return request({ |
| | | url: "/home/rawMaterialDetection", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | }; |
| | | |
| | | // 过程检测 |
| | | export const processDetection = (query) => { |
| | | return request({ |
| | | url: "/home/processDetection", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | }; |
| | | |
| | | // 成品出厂检测 |
| | | export const factoryDetection = (query) => { |
| | | return request({ |
| | | url: "/home/factoryDetection", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | }; |
| | | |
| | | // 检验数量 |
| | | export const qualityInspectionCount = () => { |
| | | return request({ |
| | | url: "/home/qualityInspectionCount", |
| | | method: "get", |
| | | }); |
| | | }; |
| | | |
| | | // 不合格预警 |
| | | export const nonComplianceWarning = () => { |
| | | return request({ |
| | | url: "/home/nonComplianceWarning", |
| | | method: "get", |
| | | }); |
| | | }; |
| | | |
| | | // 完成检验数 |
| | | export const completedInspectionCount = () => { |
| | | return request({ |
| | | url: "/home/completedInspectionCount", |
| | | method: "get", |
| | | }); |
| | | }; |
| | | |
| | | // 不合格产品排名 |
| | | export const unqualifiedProductRanking = () => { |
| | | return request({ |
| | | url: "/home/unqualifiedProductRanking", |
| | | method: "get", |
| | | }); |
| | | }; |
| | | |
| | | // 不合格检品处理分析 |
| | | export const unqualifiedProductProcessingAnalysis = () => { |
| | | return request({ |
| | | url: "/home/unqualifiedProductProcessingAnalysis", |
| | | method: "get", |
| | | }); |
| | | }; |
| | | |
| | | // 销售-采购-库存数据 |
| | | export const getBusiness = () => { |
| | | return request({ |
| | |
| | | </el-button> |
| | | </el-col> |
| | | </el-row> |
| | | <el-table :data="regulations" |
| | | border |
| | | v-loading="tableLoading" |
| | | style="width: 100%"> |
| | | <el-table-column prop="regulationNum" |
| | | label="制度编号" |
| | | width="120" /> |
| | | <el-table-column prop="title" |
| | | label="制度标题" |
| | | min-width="150" /> |
| | | <el-table-column prop="category" |
| | | label="分类" |
| | | width="120"> |
| | | <template #default="scope"> |
| | | <el-tag>{{ getCategoryText(scope.row.category) }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="version" |
| | | label="版本" |
| | | width="120" /> |
| | | <el-table-column prop="createUserName" |
| | | label="发布人" |
| | | width="120" /> |
| | | <el-table-column prop="createTime" |
| | | label="发布时间" |
| | | width="180" /> |
| | | <el-table-column prop="status" |
| | | label="状态" |
| | | width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'"> |
| | | {{ scope.row.status === 'active' ? '生效中' : '已废止' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="readCount" |
| | | label="已读人数" |
| | | width="100" /> |
| | | <el-table-column label="操作" |
| | | width="320" |
| | | fixed="right"> |
| | | <template #default="scope"> |
| | | <el-button link |
| | | @click="viewRegulation(scope.row)">查看</el-button> |
| | | <el-button link |
| | | type="primary" |
| | | @click="handleEdit(scope.row)">编辑</el-button> |
| | | <el-button link |
| | | type="danger" |
| | | @click="repealEdit(scope.row)">废弃</el-button> |
| | | <el-button link |
| | | type="success" |
| | | @click="viewVersionHistory(scope.row)">版本历史</el-button> |
| | | <!-- <el-button link type="warning" @click="viewReadStatus(scope.row)">阅读状态</el-button> --> |
| | | <el-button link |
| | | type="primary" |
| | | @click="openFileDialog(scope.row)">附件</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <pagination v-show="page.total > 0" :total="page.total" layout="total, sizes, prev, pager, next, jumper" |
| | | :page="page.current" :limit="page.size" @pagination="paginationChange" /> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="regulationTableColumn" |
| | | :tableData="regulations" |
| | | :tableLoading="tableLoading" |
| | | :page="page" |
| | | :isShowPagination="true" |
| | | @pagination="paginationChange" |
| | | /> |
| | | </div> |
| | | </el-card> |
| | | <!-- 用印申请对话框(已移除) --> |
| | |
| | | delRuleFile, |
| | | addRuleFile, |
| | | } from "@/api/collaborativeApproval/rulesRegulationsManagementFile.js"; |
| | | import PIMTable from "@/components/PIMTable/PIMTable.vue"; |
| | | |
| | | // 响应式数据 |
| | | const operationType = ref("add"); |
| | |
| | | |
| | | const regulations = ref([]); |
| | | |
| | | // 表格列配置 |
| | | const regulationTableColumn = ref([ |
| | | { label: "制度编号", prop: "regulationNum"}, |
| | | { label: "制度标题", prop: "title" }, |
| | | { |
| | | label: "分类", |
| | | prop: "category", |
| | | dataType: "tag", |
| | | formatData: (v) => getCategoryText(v), |
| | | formatType: () => "info", |
| | | }, |
| | | { label: "版本", prop: "version", width: 120 }, |
| | | { label: "发布人", prop: "createUserName", width: 120 }, |
| | | { label: "发布时间", prop: "createTime", width: 180 }, |
| | | { |
| | | label: "状态", |
| | | prop: "status", |
| | | width: 100, |
| | | dataType: "tag", |
| | | formatData: (v) => (v === "active" ? "生效中" : "已废止"), |
| | | formatType: (v) => (v === "active" ? "success" : "info"), |
| | | }, |
| | | { label: "已读人数", prop: "readCount", width: 100 }, |
| | | { |
| | | dataType: "action", |
| | | label: "操作", |
| | | width: 320, |
| | | fixed: "right", |
| | | align: "center", |
| | | operation: [ |
| | | { name: "查看", clickFun: (row) => viewRegulation(row) }, |
| | | { name: "编辑", clickFun: (row) => handleEdit(row) }, |
| | | { name: "废弃", clickFun: (row) => repealEdit(row) }, |
| | | { name: "版本历史", clickFun: (row) => viewVersionHistory(row) }, |
| | | { name: "附件", clickFun: (row) => openFileDialog(row) }, |
| | | ], |
| | | }, |
| | | ]); |
| | | |
| | | const versionHistory = ref([]); |
| | | |
| | | const readStatusList = ref([]); |
| | |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-table :data="sealApplications" border v-loading="tableLoading" style="width: 100%"> |
| | | <el-table-column prop="applicationNum" label="申请编号" width="120" /> |
| | | <el-table-column prop="title" label="申请标题" min-width="200" /> |
| | | <el-table-column prop="createUserName" label="申请人" width="120" /> |
| | | <el-table-column prop="department" label="所属部门" width="150" /> |
| | | <el-table-column prop="sealType" label="用印类型" width="120"> |
| | | <template #default="scope"> |
| | | {{ getSealTypeText(scope.row.sealType) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="createTime" label="申请时间" width="180" /> |
| | | <el-table-column prop="status" label="状态" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getStatusType(scope.row.status)"> |
| | | {{ getStatusText(scope.row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="200" fixed="right"> |
| | | <template #default="scope"> |
| | | <el-button link @click="viewSealDetail(scope.row)">查看</el-button> |
| | | <el-button |
| | | v-if="scope.row.status === 'pending'" |
| | | link |
| | | type="primary" |
| | | @click="approveSeal(scope.row)" |
| | | > |
| | | 审批 |
| | | </el-button> |
| | | <el-button |
| | | v-if="scope.row.status === 'pending'" |
| | | link |
| | | type="danger" |
| | | @click="rejectSeal(scope.row)" |
| | | > |
| | | 拒绝 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <pagination v-show="page.total > 0" :total="page.total" layout="total, sizes, prev, pager, next, jumper" |
| | | :page="page.current" :limit="page.size" @pagination="paginationChange" /> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="sealTableColumn" |
| | | :tableData="sealApplications" |
| | | :tableLoading="tableLoading" |
| | | :page="page" |
| | | :isShowPagination="true" |
| | | @pagination="paginationChange" |
| | | /> |
| | | </div> |
| | | </el-card> |
| | | |
| | |
| | | import { userLoginFacotryList } from "@/api/system/user.js" |
| | | import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js" |
| | | import FormDialog from '@/components/Dialog/FormDialog.vue' |
| | | import PIMTable from '@/components/PIMTable/PIMTable.vue' |
| | | |
| | | // 响应式数据 |
| | | const currentUser = ref(null) |
| | |
| | | // 分页参数 |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | size: 10, |
| | | total: 0 |
| | | }) |
| | | // 规章制度相关 |
| | |
| | | official: '公章', |
| | | contract: '合同专用章', |
| | | finance: '财务专用章', |
| | | legal: '法人章', |
| | | tegal: '技术专用章' |
| | | } |
| | | return sealTypeMap[sealType] || '未知' |
| | | } |
| | | |
| | | // 用印申请表格列配置(需在 getStatusText/getSealTypeText 等之后定义) |
| | | const sealTableColumn = ref([ |
| | | { label: '申请编号', prop: 'applicationNum',}, |
| | | { label: '申请标题', prop: 'title', showOverflowTooltip: true }, |
| | | { label: '申请人', prop: 'createUserName', }, |
| | | { label: '所属部门', prop: 'department', width: 150 }, |
| | | { |
| | | label: '用印类型', |
| | | prop: 'sealType', |
| | | dataType: 'tag', |
| | | formatData: (v) => getSealTypeText(v), |
| | | formatType: () => 'info' |
| | | }, |
| | | { label: '申请时间', prop: 'createTime', width: 180 }, |
| | | { |
| | | label: '状态', |
| | | prop: 'status', |
| | | width: 100, |
| | | dataType: 'tag', |
| | | formatData: (v) => getStatusText(v), |
| | | formatType: (v) => getStatusType(v) |
| | | }, |
| | | { |
| | | dataType: 'action', |
| | | label: '操作', |
| | | width: 200, |
| | | fixed: 'right', |
| | | align: 'center', |
| | | operation: [ |
| | | { name: '查看', clickFun: (row) => viewSealDetail(row) }, |
| | | { |
| | | name: '审批', |
| | | clickFun: (row) => approveSeal(row), |
| | | showHide: (row) => row.status === 'pending' |
| | | }, |
| | | { |
| | | name: '拒绝', |
| | | clickFun: (row) => rejectSeal(row), |
| | | showHide: (row) => row.status === 'pending' |
| | | } |
| | | ] |
| | | } |
| | | ]) |
| | | |
| | | // 制度分类 |
| | | const getCategoryText = (category) => { |
| | | const categoryMap = { |
| | |
| | | <el-table-column label="产品名称" prop="productName" min-width="160" /> |
| | | <el-table-column label="规格名称" prop="model" min-width="140" /> |
| | | <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> |
| | |
| | | {{ 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> |
| | |
| | | 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> |
| | | |
| | |
| | | productName: "", |
| | | model: "", |
| | | unit: "", |
| | | isQuality: false, |
| | | }); |
| | | |
| | | const rules = { |
| | |
| | | productName: row.productName || "", |
| | | model: row.model || "", |
| | | unit: row.unit || "", |
| | | isQuality: row.isQuality, |
| | | }; |
| | | dialogVisible.value = true; |
| | | }; |
| | |
| | | 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, |
| | | }); |
| | | |
| | |
| | | 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 |
| | |
| | | color: #409eff; |
| | | } |
| | | |
| | | .product-tag { |
| | | margin: 10px 0; |
| | | } |
| | | |
| | | .card-footer { |
| | | display: flex; |
| | | justify-content: space-around; |
| | |
| | | |
| | | // 添加表行类名方法 |
| | | const tableRowClassName = ({ row }) => { |
| | | const diff = row.deliveryDaysDiff; |
| | | if (row.isFh) return ''; |
| | | |
| | | const diff = row.deliveryDaysDiff; |
| | | if (diff === 15) { |
| | | return 'yellow'; |
| | | } else if (diff === 10) { |
| | |
| | | <el-form-item label="工资定额" prop="salaryQuota"> |
| | | <el-input v-model="formState.salaryQuota" type="number" :step="0.001" /> |
| | | </el-form-item> |
| | | <el-form-item label="是否质检" prop="isQuality"> |
| | | <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/> |
| | | </el-form-item> |
| | | <el-form-item label="备注" prop="remark"> |
| | | <el-input v-model="formState.remark" type="textarea" /> |
| | | </el-form-item> |
| | |
| | | no: props.record.no, |
| | | remark: props.record.remark, |
| | | salaryQuota: props.record.salaryQuota, |
| | | isQuality: props.record.isQuality, |
| | | }); |
| | | |
| | | const isShow = computed({ |
| | |
| | | no: newRecord.no || '', |
| | | remark: newRecord.remark || '', |
| | | salaryQuota: newRecord.salaryQuota || '', |
| | | isQuality: props.record.isQuality, |
| | | }; |
| | | } |
| | | }, { immediate: true, deep: true }); |
| | |
| | | no: props.record.no || '', |
| | | remark: props.record.remark || '', |
| | | salaryQuota: props.record.salaryQuota || '', |
| | | isQuality: props.record.isQuality, |
| | | }; |
| | | } |
| | | }); |
| | |
| | | <el-form-item label="工资定额" prop="salaryQuota"> |
| | | <el-input v-model="formState.salaryQuota" type="number" :step="0.001" /> |
| | | </el-form-item> |
| | | <el-form-item label="是否质检" prop="isQuality"> |
| | | <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/> |
| | | </el-form-item> |
| | | <el-form-item label="备注" prop="remark"> |
| | | <el-input v-model="formState.remark" type="textarea" /> |
| | | </el-form-item> |
| | |
| | | name: '', |
| | | remark: '', |
| | | salaryQuota: '', |
| | | isQuality: false, |
| | | }); |
| | | |
| | | const isShow = computed({ |
| | |
| | | label: "工序名称", |
| | | prop: "name", |
| | | }, |
| | | |
| | | { |
| | | label: "工资定额", |
| | | prop: "salaryQuota", |
| | | }, |
| | | { |
| | | label: "是否质检", |
| | | prop: "isQuality", |
| | | formatData: (params) => { |
| | | return params ? "是" : "否"; |
| | | }, |
| | | }, |
| | | { |
| | | label: "备注", |
| | | prop: "remark", |
| | | }, |
| | |
| | | const openDialog = async (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | getOptions().then((res) => { |
| | | supplierList.value = res.data; |
| | | }); |
| | | let userLists = await userListNoPage(); |
| | | userList.value = userLists.data; |
| | | form.value = {} |
| | | // 先清空表单验证状态,避免闪烁 |
| | | await nextTick(); |
| | | proxy.$refs.formRef?.clearValidate(); |
| | | |
| | | // 并行加载基础数据 |
| | | const [userListsRes] = await Promise.all([ |
| | | userListNoPage(), |
| | | getProductOptions(), |
| | | getOptions().then((res) => { |
| | | supplierList.value = res.data; |
| | | }) |
| | | ]); |
| | | userList.value = userListsRes.data; |
| | | |
| | | form.value = {} |
| | | testStandardOptions.value = []; |
| | | tableData.value = []; |
| | | getProductOptions(); |
| | | |
| | | if (operationType.value === 'edit') { |
| | | // 先保存 testStandardId,避免被清空 |
| | | const savedTestStandardId = row.testStandardId; |
| | | // 先设置表单数据,但暂时清空 testStandardId,等选项加载完成后再设置 |
| | | form.value = {...row, testStandardId: ''} |
| | | currentProductId.value = row.productId || 0 |
| | | // 编辑模式下,先加载指标选项,然后加载参数列表 |
| | | if (currentProductId.value) { |
| | | // 先加载指标选项 |
| | | let params = { |
| | | productId: currentProductId.value, |
| | | inspectType: 2 |
| | | } |
| | | qualityInspectDetailByProductId(params).then(res => { |
| | | testStandardOptions.value = res.data || []; |
| | | // 使用 nextTick 和 setTimeout 确保选项已经渲染到 DOM |
| | | nextTick(() => { |
| | | setTimeout(() => { |
| | | // 如果编辑数据中有 testStandardId,则设置并加载对应的参数 |
| | | if (savedTestStandardId) { |
| | | // 确保类型匹配(item.id 可能是数字或字符串) |
| | | const matchedOption = testStandardOptions.value.find(item => |
| | | item.id == savedTestStandardId || String(item.id) === String(savedTestStandardId) |
| | | ); |
| | | if (matchedOption) { |
| | | // 确保使用匹配项的 id(保持类型一致) |
| | | form.value.testStandardId = matchedOption.id; |
| | | // 编辑场景保留已有检验值,直接拉取原参数数据 |
| | | getQualityInspectParamList(row.id); |
| | | } else { |
| | | // 如果找不到匹配项,尝试直接使用原值 |
| | | console.warn('未找到匹配的指标选项,testStandardId:', savedTestStandardId, '可用选项:', testStandardOptions.value); |
| | | form.value.testStandardId = savedTestStandardId; |
| | | getQualityInspectParamList(row.id); |
| | | } |
| | | } else { |
| | | // 否则使用旧的逻辑 |
| | | getQualityInspectParamList(row.id); |
| | | } |
| | | }, 100); |
| | | }); |
| | | }); |
| | | } else { |
| | | getQualityInspectParamList(row.id); |
| | | } |
| | | currentProductId.value = row.productId || 0 |
| | | // 清空验证状态,避免数据加载过程中的校验闪烁 |
| | | nextTick(() => { |
| | | proxy.$refs.formRef?.clearValidate(); |
| | | }); |
| | | |
| | | // 编辑模式下,并行加载规格型号和指标选项 |
| | | if (currentProductId.value) { |
| | | // 设置产品名称 |
| | | form.value.productName = findNodeById(productOptions.value, currentProductId.value); |
| | | |
| | | // 并行加载规格型号和指标选项 |
| | | const params = { |
| | | productId: currentProductId.value, |
| | | inspectType: 2 |
| | | }; |
| | | |
| | | Promise.all([ |
| | | modelList({ id: currentProductId.value }), |
| | | qualityInspectDetailByProductId(params) |
| | | ]).then(([modelRes, testStandardRes]) => { |
| | | // 设置规格型号选项 |
| | | modelOptions.value = modelRes || []; |
| | | // 如果表单中已有 productModelId,设置对应的 model 和 unit |
| | | if (form.value.productModelId && modelOptions.value.length > 0) { |
| | | const selectedModel = modelOptions.value.find(item => item.id == form.value.productModelId); |
| | | if (selectedModel) { |
| | | form.value.model = selectedModel.model || ''; |
| | | form.value.unit = selectedModel.unit || ''; |
| | | } |
| | | } |
| | | |
| | | // 设置指标选项 |
| | | testStandardOptions.value = testStandardRes.data || []; |
| | | |
| | | // 设置 testStandardId 并加载参数列表 |
| | | nextTick(() => { |
| | | if (savedTestStandardId) { |
| | | // 确保类型匹配(item.id 可能是数字或字符串) |
| | | const matchedOption = testStandardOptions.value.find(item => |
| | | item.id == savedTestStandardId || String(item.id) === String(savedTestStandardId) |
| | | ); |
| | | if (matchedOption) { |
| | | // 确保使用匹配项的 id(保持类型一致) |
| | | form.value.testStandardId = matchedOption.id; |
| | | } else { |
| | | // 如果找不到匹配项,尝试直接使用原值 |
| | | console.warn('未找到匹配的指标选项,testStandardId:', savedTestStandardId, '可用选项:', testStandardOptions.value); |
| | | form.value.testStandardId = savedTestStandardId; |
| | | } |
| | | } |
| | | // 编辑场景保留已有检验值,直接拉取原参数数据 |
| | | getQualityInspectParamList(row.id); |
| | | }); |
| | | }); |
| | | } else { |
| | | getQualityInspectParamList(row.id); |
| | | } |
| | | } |
| | | } |
| | | const getProductOptions = () => { |
| | | productTreeList().then((res) => { |
| | | return productTreeList().then((res) => { |
| | | productOptions.value = convertIdToValue(res); |
| | | }); |
| | | }; |
| | |
| | | <template> |
| | | <div> |
| | | <PanelHeader title="工单执行效率分析" /> |
| | | <div class="chart-header"> |
| | | <PanelHeader title="完成检验数" /> |
| | | <div class="warn-range" @click="handleRangeClick">近7天</div> |
| | | </div> |
| | | <div class="main-panel panel-item-customers"> |
| | | <Echarts |
| | | ref="chart" |
| | |
| | | |
| | | <script setup> |
| | | import { ref, onMounted } from 'vue' |
| | | import { qualityStatistics } from '@/api/viewIndex.js' |
| | | import { completedInspectionCount } from '@/api/viewIndex.js' |
| | | import PanelHeader from './PanelHeader.vue' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | |
| | |
| | | height: '135%', |
| | | } |
| | | |
| | | const grid = { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true } |
| | | const grid = { left: '8%', right: '8%', bottom: '8%', top: '15%', containLabel: true } |
| | | |
| | | const barLegend = { |
| | | show: true, |
| | | textStyle: { color: '#B8C8E0' }, |
| | | data: ['开工', '完成'], |
| | | top: '5%', |
| | | left: 'center', |
| | | textStyle: { color: '#B8C8E0', fontSize: 14 }, |
| | | itemGap: 30, |
| | | data: ['合格', '不合格', '合格率'], |
| | | } |
| | | |
| | | // 柱状图:开工、完成;折线图:良品率(颜色 rgba(90, 216, 166, 1)) |
| | | // 柱状图:合格(黄色)、不合格(紫色);折线图:合格率(蓝色) |
| | | const chartSeries = ref([ |
| | | { |
| | | name: '开工', |
| | | name: '合格', |
| | | type: 'bar', |
| | | barWidth: 20, |
| | | barGap: '40%', |
| | | barGap: '20%', |
| | | yAxisIndex: 0, |
| | | emphasis: { focus: 'series' }, |
| | | itemStyle: { |
| | | color: { |
| | |
| | | x2: 0, |
| | | y2: 1, |
| | | colorStops: [ |
| | | { offset: 1, color: 'rgba(0, 164, 237, 0)' }, |
| | | { offset: 0, color: 'rgba(78, 228, 255, 1)' }, |
| | | { offset: 0, color: 'rgba(255, 215, 0, 1)' }, // 金黄色顶部 |
| | | { offset: 1, color: 'rgba(255, 215, 0, 0.5)' }, // 半透明底部 |
| | | ], |
| | | }, |
| | | }, |
| | | data: [], |
| | | }, |
| | | { |
| | | name: '完成', |
| | | name: '不合格', |
| | | type: 'bar', |
| | | barGap: '40%', |
| | | barGap: '20%', |
| | | barWidth: 20, |
| | | yAxisIndex: 0, |
| | | emphasis: { focus: 'series' }, |
| | | itemStyle: { |
| | | color: { |
| | |
| | | x2: 0, |
| | | y2: 1, |
| | | colorStops: [ |
| | | { offset: 1, color: 'rgba(83, 126, 245, 0.19)' }, |
| | | { offset: 0, color: 'rgba(144, 97, 248, 1)' }, |
| | | { offset: 0, color: 'rgba(144, 97, 248, 1)' }, // 紫色顶部 |
| | | { offset: 1, color: 'rgba(144, 97, 248, 0.6)' }, // 半透明底部 |
| | | ], |
| | | }, |
| | | }, |
| | | data: [], |
| | | }, |
| | | { |
| | | name: '合格率', |
| | | type: 'line', |
| | | yAxisIndex: 1, |
| | | smooth: true, |
| | | symbol: 'circle', |
| | | symbolSize: 8, |
| | | lineStyle: { |
| | | color: 'rgba(78, 228, 255, 1)', // 青色 |
| | | width: 2, |
| | | }, |
| | | itemStyle: { |
| | | color: 'rgba(78, 228, 255, 1)', |
| | | borderWidth: 2, |
| | | borderColor: '#fff', |
| | | }, |
| | | emphasis: { |
| | | focus: 'series', |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowColor: 'rgba(78, 228, 255, 0.8)', |
| | | }, |
| | | }, |
| | | data: [], |
| | |
| | | const tooltip = { |
| | | trigger: 'axis', |
| | | axisPointer: { type: 'cross' }, |
| | | backgroundColor: 'rgba(0, 0, 0, 0.8)', |
| | | borderColor: 'rgba(78, 228, 255, 0.5)', |
| | | borderWidth: 1, |
| | | textStyle: { color: '#B8C8E0' }, |
| | | formatter(params) { |
| | | let result = params[0].axisValueLabel + '<br/>' |
| | | params.forEach((item) => { |
| | | const unit = item.seriesName === '近7天' |
| | | result += `<div>${item.marker} ${item.seriesName}: ${item.value}${unit}</div>` |
| | | let unit = '' |
| | | if (item.seriesName === '合格率') { |
| | | unit = '%' |
| | | } else { |
| | | unit = '件' |
| | | } |
| | | result += `<div style="margin: 4px 0;">${item.marker} ${item.seriesName}: ${item.value}${unit}</div>` |
| | | }) |
| | | return result |
| | | }, |
| | | } |
| | | |
| | | const xAxis1 = ref([ |
| | | { type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] }, |
| | | { |
| | | type: 'category', |
| | | axisTick: { show: false }, |
| | | axisLabel: { color: '#B8C8E0', fontSize: 12 }, |
| | | axisLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.3)' } }, |
| | | data: [], |
| | | }, |
| | | ]) |
| | | |
| | | const yAxis1 = [ |
| | | { type: 'value', name: '件', axisLabel: { color: '#B8C8E0' }, nameTextStyle: { color: '#B8C8E0' } }, |
| | | { |
| | | type: 'value', |
| | | name: '近7天', |
| | | name: '单位: 件', |
| | | nameLocation: 'start', |
| | | nameTextStyle: { color: '#B8C8E0', fontSize: 12, padding: [0, 0, 0, 10] }, |
| | | axisLabel: { color: '#B8C8E0', fontSize: 12 }, |
| | | axisLine: { show: false }, |
| | | splitLine: { |
| | | show: true, |
| | | lineStyle: { color: 'rgba(184, 200, 224, 0.2)', type: 'dashed' }, |
| | | }, |
| | | }, |
| | | { |
| | | type: 'value', |
| | | name: '单位: %', |
| | | nameLocation: 'end', |
| | | nameTextStyle: { color: '#B8C8E0', fontSize: 12, padding: [0, 0, 0, 10] }, |
| | | min: 0, |
| | | max: 100, |
| | | axisLabel: { color: '#B8C8E0', formatter: '{value}%' }, |
| | | nameTextStyle: { color: '#B8C8E0' }, |
| | | splitLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.2)' } }, |
| | | axisLabel: { color: '#B8C8E0', fontSize: 12, formatter: '{value}' }, |
| | | axisLine: { show: false }, |
| | | splitLine: { |
| | | show: true, |
| | | lineStyle: { color: 'rgba(184, 200, 224, 0.2)', type: 'dashed' }, |
| | | }, |
| | | }, |
| | | ] |
| | | |
| | | const fetchData = () => { |
| | | qualityStatistics() |
| | | .then((res) => { |
| | | if (!res?.data?.item || !Array.isArray(res.data.item)) return |
| | | const items = res.data.item |
| | | xAxis1.value[0].data = items.map((d) => d.date) |
| | | // 开工:过程检验数 |
| | | chartSeries.value[0].data = items.map((d) => Number(d.processNum) || 0) |
| | | // 完成:出厂数 |
| | | chartSeries.value[1].data = items.map((d) => Number(d.factoryNum) || 0) |
| | | // 良品率:出厂数/过程数*100(无单独接口时用此占位) |
| | | chartSeries.value[2].data = items.map((d) => { |
| | | const processNum = Number(d.processNum) || 0 |
| | | const factoryNum = Number(d.factoryNum) || 0 |
| | | if (processNum <= 0) return 0 |
| | | return Math.min(100, Math.round((factoryNum / processNum) * 100)) |
| | | }) |
| | | }) |
| | | .catch((err) => { |
| | | console.error('获取开工与良品率数据失败:', err) |
| | | }) |
| | | completedInspectionCount() |
| | | .then((res) => { |
| | | if (res?.code === 200 && Array.isArray(res?.data)) { |
| | | const items = res.data |
| | | // 更新X轴日期数据 |
| | | xAxis1.value[0].data = items.map((d) => d.dateStr || '') |
| | | // 更新合格数(黄色柱状图) |
| | | chartSeries.value[0].data = items.map((d) => Number(d.qualifiedCount) || 0) |
| | | // 更新不合格数(紫色柱状图) |
| | | chartSeries.value[1].data = items.map((d) => Number(d.unqualifiedCount) || 0) |
| | | // 更新合格率(蓝色折线图) |
| | | chartSeries.value[2].data = items.map((d) => Number(d.passRate) || 0) |
| | | } |
| | | }) |
| | | .catch((err) => { |
| | | console.error('获取完成检验数数据失败:', err) |
| | | }) |
| | | } |
| | | |
| | | const handleRangeClick = () => { |
| | | // 先按截图做静态"近7天",后续有真实筛选需求再接入 |
| | | fetchData() |
| | | } |
| | | |
| | | onMounted(() => { |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .chart-header { |
| | | position: relative; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .warn-range { |
| | | position: absolute; |
| | | right: 0; |
| | | top: 0; |
| | | height: 32px; |
| | | padding: 0 14px; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 4px; |
| | | color: #ffffff; |
| | | font-weight: 600; |
| | | font-size: 14px; |
| | | background: linear-gradient(180deg, rgba(51, 120, 255, 1) 0%, rgba(0, 164, 237, 1) 100%); |
| | | border: 1px solid rgba(78, 228, 255, 0.25); |
| | | cursor: pointer; |
| | | z-index: 10; |
| | | } |
| | | |
| | | .warn-range:hover { |
| | | background: linear-gradient(180deg, rgba(51, 140, 255, 1) 0%, rgba(0, 184, 237, 1) 100%); |
| | | } |
| | | |
| | | .main-panel { |
| | | display: flex; |
| | | flex-direction: column; |
| | |
| | | padding: 18px; |
| | | width: 100%; |
| | | height: 449px; |
| | | position: relative; |
| | | background: radial-gradient(circle at 50% 50%, rgba(78, 228, 255, 0.05) 0%, rgba(0, 0, 0, 0) 70%); |
| | | } |
| | | </style> |
| | |
| | | <div class="warn-body"> |
| | | <div class="warn-list" role="list"> |
| | | <div v-for="item in warnings" :key="item.id" class="warn-item" role="listitem" @click="openWarning(item)"> |
| | | <div class="warn-tag" :class="tagClass(item.type)">{{ item.typeText }}</div> |
| | | <div class="warn-tag" :class="tagClass(item.type)">{{ item.parentProductTitle }}-{{ item.productTitle }} |
| | | </div> |
| | | <div class="warn-text" :title="item.title">{{ item.title }}</div> |
| | | <div class="warn-action" @click.stop="openWarning(item)">查看</div> |
| | | <div class="warn-date">{{ item.date }}</div> |
| | |
| | | <script setup> |
| | | import { computed, getCurrentInstance, ref, onMounted } from 'vue' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import { qualityUnqualifiedListPage } from '@/api/qualityManagement/nonconformingManagement.js' |
| | | import { nonComplianceWarning } from '@/api/viewIndex.js' |
| | | |
| | | const { proxy } = getCurrentInstance() || {} |
| | | |
| | | const warnings = ref([ |
| | | { id: '1', type: 'raw', typeText: '原材料', title: '关于企业原材料调整通知', date: '2024.08.24' }, |
| | | { id: '2', type: 'raw', typeText: '原材料', title: '关于原材料消耗方案建设的通知', date: '2024.08.24' }, |
| | | { id: '3', type: 'final', typeText: '成品', title: '成品工作台系统维护计划安排', date: '2024.08.24' }, |
| | | { id: '4', type: 'final', typeText: '成品', title: '成品工作台系统维护计划安排', date: '2024.08.24' }, |
| | | { id: '5', type: 'semi', typeText: '半成品', title: 'HRM系统安全升级公告:加强访问控制…', date: '2024.08.24' }, |
| | | { id: '6', type: 'semi', typeText: '半成品', title: 'HRM系统安全升级公告:加强访问控制…', date: '2024.08.24' }, |
| | | ]) |
| | | const warnings = ref([]) |
| | | |
| | | // 占比数据 |
| | | const ratios = ref({ |
| | | rawMaterialRatio: 0, |
| | | semiFinishedProductRatio: 0, |
| | | finishedProductRatio: 0, |
| | | }) |
| | | |
| | | const TAG_COLORS = { |
| | | raw: '#7C4DFF', |
| | |
| | | return 'tag-semi' |
| | | } |
| | | |
| | | // 根据productTitle映射类型 |
| | | const mapProductTitleToType = (productTitle) => { |
| | | if (productTitle === '原材料') return 'raw' |
| | | if (productTitle === '半成品') return 'semi' |
| | | if (productTitle === '成品') return 'final' |
| | | return 'raw' // 默认值 |
| | | } |
| | | |
| | | const pieChartStyle = { width: '100%', height: '100%' } |
| | | |
| | | const pieOptions = { |
| | |
| | | |
| | | const pieTooltip = { |
| | | trigger: 'item', |
| | | formatter: (p) => `${p.name}:${p.value}`, |
| | | formatter: (p) => `${p.name}:${p.value}%`, |
| | | } |
| | | |
| | | const pieData = computed(() => { |
| | | const counts = { raw: 0, final: 0, semi: 0 } |
| | | warnings.value.forEach((w) => { |
| | | const key = w.type in counts ? w.type : 'raw' |
| | | counts[key] += 1 |
| | | }) |
| | | return [ |
| | | { name: '原材料', value: counts.raw, itemStyle: { color: TAG_COLORS.raw } }, |
| | | { name: '半成品', value: counts.semi, itemStyle: { color: TAG_COLORS.semi } }, |
| | | { name: '成品', value: counts.final, itemStyle: { color: TAG_COLORS.final } }, |
| | | { name: '原材料', value: ratios.value.rawMaterialRatio, itemStyle: { color: TAG_COLORS.raw } }, |
| | | { name: '半成品', value: ratios.value.semiFinishedProductRatio, itemStyle: { color: TAG_COLORS.semi } }, |
| | | { name: '成品', value: ratios.value.finishedProductRatio, itemStyle: { color: TAG_COLORS.final } }, |
| | | ] |
| | | }) |
| | | |
| | |
| | | |
| | | const fetchWarnings = async () => { |
| | | try { |
| | | const res = await qualityUnqualifiedListPage({ pageNum: 1, pageSize: 6 }) |
| | | const rows = res?.rows || res?.data?.rows || res?.data || [] |
| | | if (!Array.isArray(rows) || rows.length === 0) return |
| | | const res = await nonComplianceWarning() |
| | | if (res?.code === 200 && res?.data) { |
| | | const data = res.data |
| | | |
| | | warnings.value = rows.slice(0, 6).map((r, idx) => { |
| | | const typeCode = r.inspectType ?? r.modelType ?? r.type |
| | | const mappedType = typeCode === 0 || typeCode === '0' ? 'raw' : typeCode === 1 || typeCode === '1' ? 'semi' : 'final' |
| | | const title = r.title || r.unqualifiedTitle || r.remark || r.unqualifiedReason || '不合格预警' |
| | | const date = (r.warningTime || r.createTime || r.updateTime || '').slice(0, 10).replace(/-/g, '.') || '2024.08.24' |
| | | return { |
| | | id: r.id ?? r.unqualifiedId ?? `${idx}`, |
| | | type: mappedType, |
| | | typeText: mappedType === 'raw' ? '原材料' : mappedType === 'semi' ? '半成品' : '成品', |
| | | title, |
| | | date, |
| | | // 更新占比数据 |
| | | ratios.value = { |
| | | rawMaterialRatio: data.rawMaterialRatio ?? 0, |
| | | semiFinishedProductRatio: data.semiFinishedProductRatio ?? 0, |
| | | finishedProductRatio: data.finishedProductRatio ?? 0, |
| | | } |
| | | }) |
| | | |
| | | // 更新警告列表 |
| | | const children = data.children || [] |
| | | warnings.value = children.map((item, idx) => { |
| | | const type = mapProductTitleToType(item.parentProductTitle) |
| | | const date = item.date ? item.date.replace(/-/g, '.') : '' |
| | | return { |
| | | id: item.id ?? `warning-${idx}`, |
| | | type, |
| | | parentProductTitle: item.parentProductTitle || '原材料', |
| | | productTitle: item.productTitle || '原材料', |
| | | title: item.description || '不合格预警', |
| | | date, |
| | | } |
| | | }) |
| | | } |
| | | } catch (e) { |
| | | // 接口失败则保持 mock |
| | | // 接口失败则保持空数据 |
| | | console.error('获取不合格预警失败:', e) |
| | | } |
| | | } |
| | | |
| | | const openWarning = (item) => { |
| | | const title = `【${item.typeText}】${item.title}` |
| | | const content = `${title}时间:${item.date}` |
| | | const title = `【${item.parentProductTitle}-${item.productTitle}】${item.title}` |
| | | if (proxy?.$modal?.alert) { |
| | | proxy.$modal.alert(content) |
| | | proxy.$modal.alert(title) |
| | | return |
| | | } |
| | | // 兜底:没有全局 modal 时用 console |
| | |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | border-bottom: 1px solid; |
| | | border-image: linear-gradient( |
| | | 270deg, |
| | | border-image: linear-gradient(270deg, |
| | | rgba(0, 126, 255, 0) 0%, |
| | | rgba(0, 126, 255, 0.4549) 35%, |
| | | #007eff 78%, |
| | | #007eff 100% |
| | | ) |
| | | 1; |
| | | #007eff 100%) 1; |
| | | padding: 10px 0 6px; |
| | | } |
| | | |
| | |
| | | |
| | | .warn-item { |
| | | display: grid; |
| | | grid-template-columns: 88px 1fr auto 110px; |
| | | grid-template-columns: 130px 1fr auto 110px; |
| | | align-items: center; |
| | | gap: 12px; |
| | | color: #b8c8e0; |
| | | font-size: 14px; |
| | | line-height: 1; |
| | | padding: 6px 0; |
| | | line-height: 1.2; |
| | | padding: 8px 0; |
| | | border-radius: 4px; |
| | | transition: background-color 0.2s, color 0.2s; |
| | | } |
| | |
| | | <div> |
| | | <!-- 顶部统计卡片 --> |
| | | <div class="stats-cards"> |
| | | <div |
| | | v-for="item in statItems" |
| | | :key="item.name" |
| | | class="stat-card" |
| | | > |
| | | <div v-for="item in statItems" :key="item.name" class="stat-card"> |
| | | <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" /> |
| | | <div class="card-content"> |
| | | <span class="card-label">{{ item.name }}</span> |
| | |
| | | |
| | | <script setup> |
| | | import { ref, onMounted } from 'vue' |
| | | import { salesPurchaseStorageProductCount } from '@/api/viewIndex.js' |
| | | import { qualityInspectionCount } from '@/api/viewIndex.js' |
| | | |
| | | const statItems = ref([]) |
| | | |
| | |
| | | const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down') |
| | | |
| | | const fetchData = () => { |
| | | salesPurchaseStorageProductCount() |
| | | qualityInspectionCount() |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | statItems.value = res.data.map((item) => ({ |
| | | name: item.name, |
| | | value: item.value, |
| | | rate: item.rate, |
| | | })) |
| | | if (res.code === 200 && res.data) { |
| | | const data = res.data |
| | | |
| | | statItems.value = [ |
| | | { |
| | | name: '总检验数', |
| | | value: data.totalCount ?? 0, |
| | | rate: data.totalCountGrowthRate ?? 0, |
| | | }, |
| | | { |
| | | name: '今日待完成数', |
| | | value: data.todayPendingCount ?? 0, |
| | | rate: data.todayPendingCountGrowthRate ?? 0, |
| | | }, |
| | | { |
| | | name: '今日已完成数', |
| | | value: data.todayCompletedCount ?? 0, |
| | | rate: data.todayCompletedCountGrowthRate ?? 0, |
| | | }, |
| | | ] |
| | | } |
| | | }) |
| | | .catch((err) => { |
| | | console.error('获取销售/采购/储存产品数失败:', err) |
| | | console.error('获取质量检验统计失败:', err) |
| | | }) |
| | | } |
| | | |
| | |
| | | |
| | | .card-label { |
| | | font-weight: 400; |
| | | font-size: 19px; |
| | | font-size: 16px; |
| | | color: rgba(208, 231, 255, 0.7); |
| | | } |
| | | |
| | |
| | | color: #d0e7ff; |
| | | } |
| | | |
| | | .card-compare > span:first-child { |
| | | .card-compare>span:first-child { |
| | | font-size: 13px; |
| | | opacity: 0.8; |
| | | } |
| | |
| | | .compare-icon { |
| | | font-size: 14px; |
| | | position: relative; |
| | | top: -1px; /* 轻微上移,让箭头与文字垂直居中对齐 */ |
| | | top: -1px; |
| | | /* 轻微上移,让箭头与文字垂直居中对齐 */ |
| | | } |
| | | |
| | | .compare-up .compare-value, |
| | |
| | | .compare-down .compare-icon { |
| | | color: #ff5252; |
| | | } |
| | | |
| | | </style> |
| | |
| | | |
| | | <div class="inspect-body"> |
| | | <div class="ring"> |
| | | <Echarts |
| | | :chartStyle="ringChartStyle" |
| | | :series="buildRingSeries(section)" |
| | | :tooltip="ringTooltip" |
| | | :legend="{ show: false }" |
| | | :options="ringOptions" |
| | | /> |
| | | <Echarts :chartStyle="ringChartStyle" :series="buildRingSeries(section)" :tooltip="ringTooltip" |
| | | :legend="{ show: false }" :options="ringOptions" /> |
| | | </div> |
| | | |
| | | <div class="stats"> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive } from 'vue' |
| | | import { reactive, onMounted } from 'vue' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import PanelHeader from './PanelHeader.vue' |
| | | import DateTypeSwitch from './DateTypeSwitch.vue' |
| | | import { rawMaterialDetection, processDetection, factoryDetection } from '@/api/viewIndex.js' |
| | | |
| | | const QUALIFIED_COLOR = '#4EE4FF' |
| | | const UNQUALIFIED_COLOR = '#3378FF' |
| | | const TRACK_COLOR = 'rgba(78, 228, 255, 0.12)' |
| | | |
| | | const apiMap = { |
| | | raw: rawMaterialDetection, |
| | | process: processDetection, |
| | | final: factoryDetection, |
| | | } |
| | | |
| | | |
| | | const fetchSectionData = async (section) => { |
| | | const api = apiMap[section.key] |
| | | if (!api) return |
| | | |
| | | try { |
| | | const res = await api({ |
| | | type: section.dateType, |
| | | }) |
| | | |
| | | if (res?.code === 200 && res?.data) { |
| | | const data = res.data |
| | | section.qualifiedCount = Number(data.qualifiedCount || 0) |
| | | section.unqualifiedCount = Number(data.unqualifiedCount || 0) |
| | | section.qualifiedRate = Number(data.qualifiedRate || 0) |
| | | section.unqualifiedRate = Number(data.unqualifiedRate || 0) |
| | | } |
| | | } catch (err) { |
| | | console.error(`${section.key} 接口请求失败`, err) |
| | | } |
| | | } |
| | | |
| | | |
| | | const sections = reactive([ |
| | | { |
| | | key: 'raw', |
| | | title: '原材料检测', |
| | | dateType: 1, |
| | | qualifiedCount: 199, |
| | | unqualifiedCount: 99, |
| | | qualifiedRate: 90, |
| | | unqualifiedRate: 10, |
| | | qualifiedCount: 0, |
| | | unqualifiedCount: 0, |
| | | qualifiedRate: 0, |
| | | unqualifiedRate: 0, |
| | | }, |
| | | { |
| | | key: 'process', |
| | | title: '过程检测', |
| | | dateType: 1, |
| | | qualifiedCount: 199, |
| | | unqualifiedCount: 99, |
| | | qualifiedRate: 90, |
| | | unqualifiedRate: 10, |
| | | qualifiedCount: 0, |
| | | unqualifiedCount: 0, |
| | | qualifiedRate: 0, |
| | | unqualifiedRate: 0, |
| | | }, |
| | | { |
| | | key: 'final', |
| | | title: '成品出厂检测', |
| | | dateType: 1, |
| | | qualifiedCount: 199, |
| | | unqualifiedCount: 99, |
| | | qualifiedRate: 90, |
| | | unqualifiedRate: 10, |
| | | qualifiedCount: 0, |
| | | unqualifiedCount: 0, |
| | | qualifiedRate: 0, |
| | | unqualifiedRate: 0, |
| | | }, |
| | | ]) |
| | | |
| | |
| | | const section = sections.find((s) => s.key === key) |
| | | if (!section) return |
| | | section.dateType = dateType |
| | | const rates = calcRates(section.qualifiedCount, section.unqualifiedCount) |
| | | section.qualifiedRate = rates.qualifiedRate |
| | | section.unqualifiedRate = rates.unqualifiedRate |
| | | // 切换日期类型时重新获取数据 |
| | | fetchSectionData(section) |
| | | } |
| | | |
| | | sections.forEach((s) => { |
| | | const rates = calcRates(s.qualifiedCount, s.unqualifiedCount) |
| | | s.qualifiedRate = rates.qualifiedRate |
| | | s.unqualifiedRate = rates.unqualifiedRate |
| | | // 组件挂载时获取所有section的数据 |
| | | onMounted(() => { |
| | | sections.forEach((section) => { |
| | | fetchSectionData(section) |
| | | }) |
| | | }) |
| | | </script> |
| | | |
| | |
| | | width: 18px; |
| | | height: 7px; |
| | | border-radius: 8px; |
| | | background: linear-gradient(360deg, rgba(33,133,255,0.4) 0%, rgba(33,221,255,0) 100%); |
| | | background: linear-gradient(360deg, rgba(33, 133, 255, 0.4) 0%, rgba(33, 221, 255, 0) 100%); |
| | | position: absolute; |
| | | top: 50%; |
| | | left: -1px; |
| | |
| | | flex: 1 1 auto; |
| | | min-height: 0; |
| | | display: flex; |
| | | justify-content:space-around; |
| | | justify-content: space-around; |
| | | align-items: center; |
| | | gap: 18px; |
| | | } |
| | |
| | | position: absolute; |
| | | inset: -8px; |
| | | border-radius: 50%; |
| | | background: repeating-conic-gradient( |
| | | from 0deg, |
| | | rgba(78, 228, 255, 0.75) 0 1deg, |
| | | rgba(78, 228, 255, 0) 1deg 9deg |
| | | ); |
| | | background: repeating-conic-gradient(from 0deg, |
| | | rgba(78, 228, 255, 0.75) 0 1deg, |
| | | rgba(78, 228, 255, 0) 1deg 9deg); |
| | | -webkit-mask: radial-gradient(circle, transparent 62%, #000 63%); |
| | | mask: radial-gradient(circle, transparent 62%, #000 63%); |
| | | opacity: 0.35; |
| | |
| | | <template> |
| | | <div> |
| | | <PanelHeader title="产品大类" /> |
| | | <PanelHeader title="不合格检品处理分析" /> |
| | | <div class="panel-item-customers"> |
| | | <div class="pie-chart-wrapper" ref="pieWrapperRef"> |
| | | <div class="pie-background" ref="pieBackgroundRef"></div> |
| | | <Echarts |
| | | ref="chart" |
| | | :chartStyle="chartStyle" |
| | | :legend="landLegend" |
| | | :series="landSeries" |
| | | :tooltip="landTooltip" |
| | | :color="landColors" |
| | | :options="pieOptions" |
| | | style="height: 100%" |
| | | class="land-chart" |
| | | /> |
| | | <Echarts ref="chart" :chartStyle="chartStyle" :legend="landLegend" :series="computedSeries" |
| | | :tooltip="landTooltip" :color="landColors" :options="pieOptions" style="height: 100%" class="land-chart" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, onBeforeUnmount } from 'vue' |
| | | import { ref, computed, onMounted, onBeforeUnmount } from 'vue' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import PanelHeader from './PanelHeader.vue' |
| | | import { productCategoryDistribution } from '@/api/viewIndex.js' |
| | | import { unqualifiedProductProcessingAnalysis } from '@/api/viewIndex.js' |
| | | import { useChartBackground } from '@/hooks/useChartBackground.js' |
| | | |
| | | const pieWrapperRef = ref(null) |
| | | const pieBackgroundRef = ref(null) |
| | | const chart = ref(null) |
| | | |
| | | // 数据列表(来自接口) |
| | | // 数据列表 |
| | | const dataList = ref([]) |
| | | |
| | | // 颜色列表 |
| | | // 颜色列表 |
| | | const landColors = ['#26FFCB', '#24CBFF', '#35FBF4', '#2651FF', '#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF'] |
| | | |
| | | // label 富文本:为每个颜色生成一个小圆点样式(确保在 label 中可见) |
| | | // label 富文本样式 |
| | | const dotRich = landColors.reduce((acc, color, idx) => { |
| | | acc[`dot${idx}`] = { |
| | | width: 8, |
| | |
| | | return acc |
| | | }, {}) |
| | | |
| | | // 图例配置(右侧竖排) |
| | | const landLegend = { |
| | | // 图例配置 |
| | | const landLegend = ref({ |
| | | show: false, |
| | | icon: 'circle', |
| | | data: [], |
| | | right: '8%', |
| | | top: '40%', |
| | | orient: 'vertical', |
| | | itemGap: 14, |
| | | itemWidth: 6, |
| | | itemHeight: 6, |
| | | textStyle: { |
| | | fontSize: 12, |
| | | color: '#fff', |
| | | rich: { |
| | | unit: { |
| | | color: '#fff', |
| | | fontSize: 12, |
| | | padding: [0, 10, 0, 0], |
| | | }, |
| | | text: { |
| | | width: 60, |
| | | color: '#fff', |
| | | fontSize: 12, |
| | | }, |
| | | }, |
| | | }, |
| | | formatter: function (name) { |
| | | const list = dataList.value || [] |
| | | const item = list.find((d) => d.name === name) |
| | | if (!item) return name |
| | | const val = Number(item.value || 0) |
| | | const totalValue = list.reduce((sum, it) => sum + Number(it.value || 0), 0) |
| | | const percent = totalValue ? ((val / totalValue) * 100).toFixed(2) : '0.00' |
| | | return `{text|${name}}${val}{unit| 公顷}${percent}{unit|%}` |
| | | }, |
| | | } |
| | | unit: { color: '#fff', fontSize: 12, padding: [0, 10, 0, 0] }, |
| | | text: { width: 60, color: '#fff', fontSize: 12 }, |
| | | } |
| | | } |
| | | }) |
| | | |
| | | // 提示框 |
| | | // 提示框配置 |
| | | const landTooltip = { |
| | | // triggerOn: 'hover', |
| | | alwaysShowContent: true, |
| | | trigger: 'item', |
| | | alwaysShowContent: false, |
| | | position: function (pt) { |
| | | return [pt[0], 130] |
| | | }, |
| | | formatter: function (params) { |
| | | return `${params.name} (${params.value}类)` |
| | | // 确保 params.data 存在 |
| | | if (!params.data) return '' |
| | | const { name, value, rate } = params.data |
| | | return `${name}<br/>数量:${value}个<br/>占比:${rate}%` |
| | | }, |
| | | } |
| | | |
| | | // 双层环形饼图 |
| | | const landSeries = ref([ |
| | | { |
| | | name: '产品大类', |
| | | type: 'pie', |
| | | radius: ['35%', '55%'], |
| | | center: ['50%', '50%'], |
| | | label: { |
| | | show: true, |
| | | position: 'outside', |
| | | color: '#fff', |
| | | fontSize: 12, |
| | | lineHeight: 18, |
| | | rich: { |
| | | ...dotRich, |
| | | parent: { fontSize: 14, fontWeight: 600, color: '#fff', lineHeight: 20, overflow: 'break' }, |
| | | child: { fontSize: 12, color: '#fff', lineHeight: 18 }, |
| | | // 使用计算属性处理 Series |
| | | const computedSeries = computed(() => { |
| | | return [ |
| | | { |
| | | name: '不合格检品处理分析', |
| | | type: 'pie', |
| | | radius: ['35%', '55%'], |
| | | center: ['50%', '50%'], |
| | | label: { |
| | | show: true, |
| | | position: 'outside', |
| | | color: '#fff', |
| | | rich: { |
| | | ...dotRich, |
| | | parent: { fontSize: 14, fontWeight: 600, color: '#fff', lineHeight: 20 }, |
| | | child: { fontSize: 12, color: '#fff', lineHeight: 18 }, |
| | | }, |
| | | formatter: function (params) { |
| | | if (!params.data) return '' |
| | | const dotKey = `dot${params.dataIndex % landColors.length}` |
| | | return `{${dotKey}|} {parent|${params.data.name} (${params.data.value}个)}` |
| | | }, |
| | | }, |
| | | formatter: function (params) { |
| | | const children = params?.data?.children || [] |
| | | const parentName = params?.data?.name || '' |
| | | const rawVal = params?.data?.value |
| | | const parentValue = typeof rawVal === 'number' && !Number.isNaN(rawVal) ? rawVal : (Number(rawVal) || 0) |
| | | const dotKey = `dot${(params?.dataIndex || 0) % landColors.length}` |
| | | const dot = `{${dotKey}|} ` |
| | | const parentLine = `${dot}{parent|${parentName} (${parentValue}类)}` |
| | | if (!children.length) return parentLine |
| | | // 父级全部显示;子级最多 5 个,超出显示省略号 |
| | | const displayed = children.slice(0, 5).map((c) => `{child|${c.name}}`) |
| | | if (children.length > 5) displayed.push('{child|…}') |
| | | return [parentLine, ...displayed].join('\n') |
| | | labelLine: { |
| | | show: true, |
| | | length: 20, |
| | | lineStyle: { color: '#B8C8E0' }, |
| | | }, |
| | | data: dataList.value, |
| | | }, |
| | | labelLine: { |
| | | show: true, |
| | | length: 20, |
| | | length2: 20, |
| | | lineStyle: { |
| | | color: '#B8C8E0', |
| | | }, |
| | | { |
| | | // 内圈装饰 |
| | | type: 'pie', |
| | | radius: ['35%', '40%'], |
| | | center: ['50%', '50%'], |
| | | silent: true, |
| | | label: { show: false }, |
| | | itemStyle: { color: 'rgba(0, 127, 255, 0.25)' }, |
| | | data: [1], |
| | | }, |
| | | itemStyle: { |
| | | color: function (params) { |
| | | return landColors[params.dataIndex % landColors.length] |
| | | }, |
| | | }, |
| | | data: dataList.value, |
| | | }, |
| | | { |
| | | // 内圈 |
| | | type: 'pie', |
| | | radius: ['35%', '40%'], |
| | | center: ['50%', '50%'], |
| | | silent: true, |
| | | label: { |
| | | show: false, |
| | | }, |
| | | labelLine: { |
| | | show: false, |
| | | }, |
| | | itemStyle: { |
| | | color: 'rgba(0, 127, 255, 0.25)', |
| | | }, |
| | | data: [1], |
| | | }, |
| | | ]) |
| | | ] |
| | | }) |
| | | |
| | | const chartStyle = { |
| | | width: '100%', |
| | | height: '126%', |
| | | } |
| | | const chartStyle = { width: '100%', height: '126%' } |
| | | const pieOptions = { backgroundColor: 'transparent' } |
| | | |
| | | const pieOptions = { |
| | | backgroundColor: 'transparent', |
| | | textStyle: { color: '#B8C8E0' }, |
| | | } |
| | | |
| | | // 使用封装的背景位置调整方法,可自定义偏移值 |
| | | // 背景处理钩子 |
| | | const { adjustBackgroundPosition, init: initBackground, cleanup: cleanupBackground } = useChartBackground({ |
| | | wrapperRef: pieWrapperRef, |
| | | backgroundRef: pieBackgroundRef, |
| | | offsetX: '-51.5%', // X 轴偏移,可动态调整 |
| | | offsetY: '-39%', // Y 轴偏移,可动态调整 |
| | | watchData: dataList // 监听数据变化,自动调整位置 |
| | | offsetX: '-51.5%', |
| | | offsetY: '-39%', |
| | | watchData: dataList |
| | | }) |
| | | |
| | | const loadData = async () => { |
| | | try { |
| | | const res = await productCategoryDistribution() |
| | | const items = res?.data?.items || [] |
| | | dataList.value = items.map((it) => ({ |
| | | name: it.name, |
| | | value: Number(it.value || 0), |
| | | rate: it.rate, |
| | | children: Array.isArray(it.children) ? it.children : [], |
| | | })) |
| | | landLegend.data = dataList.value.map((d) => d.name) |
| | | landSeries.value[0].data = dataList.value |
| | | // 数据加载完成后调整背景位置 |
| | | adjustBackgroundPosition() |
| | | const res = await unqualifiedProductProcessingAnalysis() |
| | | if (res && res.code === 200) { |
| | | dataList.value = (res.data || []).map((it) => ({ |
| | | name: it.name, |
| | | value: Number(it.value || 0), |
| | | rate: it.rate, |
| | | })) |
| | | landLegend.value.data = dataList.value.map((d) => d.name) |
| | | |
| | | // 数据更新后微调背景 |
| | | setTimeout(() => { |
| | | adjustBackgroundPosition() |
| | | }, 100) |
| | | } |
| | | } catch (e) { |
| | | console.error('获取产品大类分布失败:', e) |
| | | dataList.value = [] |
| | | landLegend.data = [] |
| | | landSeries.value[0].data = [] |
| | | console.error('获取数据失败:', e) |
| | | } |
| | | } |
| | | |
| | | |
| | | onMounted(() => { |
| | | loadData() |
| | |
| | | position: relative; |
| | | width: 100%; |
| | | height: 320px; |
| | | background: transparent; |
| | | } |
| | | |
| | | .pie-background { |
| | |
| | | background-repeat: no-repeat; |
| | | z-index: 1; |
| | | pointer-events: none; |
| | | /* 默认居中,会在 JS 中动态调整 */ |
| | | left: 50%; |
| | | top: 50%; |
| | | transform: translate(-51.5%, -39%); |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | <template> |
| | | <div> |
| | | <PanelHeader title="工单执行效率分析" /> |
| | | <PanelHeader title="不合格产品排名" /> |
| | | <div class="main-panel panel-item-customers"> |
| | | <div class="main-panel-container"> |
| | | <div |
| | | style="color: white" |
| | | class="main-panel-box" |
| | | v-for="(item, index) in panelList" |
| | | :key="index" |
| | | > |
| | | <div style="flex: 1" class="main-panel-box-left">Top{{ index + 1 }}</div> |
| | | <div style="flex: 3" class="main-panel-box-right"> |
| | | <div class="main-panel-box-right-text"> |
| | | <span>总数量:{{ item.total }}</span> |
| | | <span>已完成:{{ item.finished }}</span> |
| | | <span>合格率:{{ item.qualifiedRate }}</span> |
| | | </div> |
| | | <div class="main-panel-box-right-progress"> |
| | | <el-progress :percentage="item.percentage" :format="format" /> |
| | | <div style="color: white" class="main-panel-box" v-for="(item, index) in panelList" :key="index"> |
| | | <!-- <div style="flex: 1" class="main-panel-box-left">{{ item.rank }}</div> --> |
| | | <div style="flex: 1" class="main-panel-box-left">{{ item.productName }}</div> |
| | | <div style="flex: 3" class="main-panel-box-right"> |
| | | <!-- <div class="main-panel-box-right-title">{{ item.productName }}</div> --> |
| | | <div class="main-panel-box-right-text"> |
| | | <span>总数量:{{ item.total }}</span> |
| | | <span>已完成:{{ item.finished }}</span> |
| | | <span>合格率:{{ item.qualifiedRate }}%</span> |
| | | </div> |
| | | <div class="main-panel-box-right-progress"> |
| | | <el-progress :percentage="item.percentage" :format="format" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted } from 'vue' |
| | | import { unqualifiedProductRanking } from '@/api/viewIndex.js' |
| | | import PanelHeader from './PanelHeader.vue' |
| | | const panelList = [ |
| | | { total: 100, finished: 100, qualifiedRate: 100, percentage: 100 }, // Top1 |
| | | { total: 200, finished: 180, qualifiedRate: 90, percentage: 90 }, // Top2 |
| | | { total: 200, finished: 180, qualifiedRate: 90, percentage: 90 }, // Top2 |
| | | { total: 200, finished: 180, qualifiedRate: 90, percentage: 90 }, // Top2 |
| | | { total: 200, finished: 180, qualifiedRate: 90, percentage: 90 }, // Top2 |
| | | { total: 200, finished: 180, qualifiedRate: 90, percentage: 90 }, // Top2 |
| | | { total: 200, finished: 180, qualifiedRate: 90, percentage: 90 }, // Top2 |
| | | { total: 200, finished: 180, qualifiedRate: 90, percentage: 90 }, // Top2 |
| | | { total: 200, finished: 180, qualifiedRate: 90, percentage: 90 }, // Top2 |
| | | { total: 150, finished: 120, qualifiedRate: 80, percentage: 80 } // Top3 |
| | | ] |
| | | const format = (percentage) => { |
| | | return `${percentage}%`; |
| | | } |
| | | |
| | | const panelList = ref([]) |
| | | |
| | | const format = (percentage) => { |
| | | return `${percentage}%` |
| | | } |
| | | |
| | | const fetchData = () => { |
| | | unqualifiedProductRanking() |
| | | .then((res) => { |
| | | if (res?.code === 200 && Array.isArray(res?.data)) { |
| | | const data = res.data |
| | | panelList.value = data.map((item, index) => { |
| | | const total = Number(item.totalCount) || 0 |
| | | const finished = Number(item.completedCount) || 0 |
| | | const passRate = Number(item.passRate) || 0 |
| | | |
| | | return { |
| | | rank: `Top${index + 1}`, |
| | | productName: item.productName || `产品${index + 1}`, |
| | | total: total.toFixed(2), |
| | | finished: finished.toFixed(2), |
| | | qualifiedRate: passRate.toFixed(2), |
| | | percentage: Math.min(100, Math.max(0, passRate)), // 确保百分比在0-100之间 |
| | | } |
| | | }) |
| | | } |
| | | }) |
| | | .catch((err) => { |
| | | console.error('获取工单执行效率分析数据失败:', err) |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | // fetchData() |
| | | fetchData() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .main-panel-box{ |
| | | .main-panel-box { |
| | | display: flex; |
| | | flex-direction: row; |
| | | align-items: center; |
| | | height: 40px; |
| | | .main-panel-box-left{ |
| | | |
| | | .main-panel-box-left { |
| | | background: red; |
| | | border-radius: 20px; |
| | | text-align: center; |
| | | line-height: 32px; |
| | | margin: 0 20px; |
| | | margin: 0 20px; |
| | | } |
| | | .main-panel-box-right{ |
| | | |
| | | .main-panel-box-right { |
| | | display: flex; |
| | | flex-direction: column; |
| | | .main-panel-box-right-text{ |
| | | flex: 1; |
| | | |
| | | .main-panel-box-right-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #ffffff; |
| | | margin-bottom: 6px; |
| | | } |
| | | |
| | | .main-panel-box-right-text { |
| | | font-size: 12px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | padding-right: 60px; |
| | | margin-bottom: 4px; |
| | | } |
| | | .main-panel-box-right-progress{ |
| | | :deep(.el-progress__text){ |
| | | |
| | | .main-panel-box-right-progress { |
| | | :deep(.el-progress__text) { |
| | | color: white !important; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .main-panel-container { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | height: 100%; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .main-panel { |
| | | display: flex; |
| | | flex-direction: column; |
| | |
| | | padding: 18px; |
| | | width: 100%; |
| | | height: 449px; |
| | | overflow: hidden; |
| | | } |
| | | </style> |
| | |
| | | |
| | | <!-- 顶部标题栏 --> |
| | | <div class="dashboard-header"> |
| | | <div class="factory-name">生产数据分析</div> |
| | | <div class="factory-name">进销质量类分析</div> |
| | | </div> |
| | | |
| | | <!-- 主要内容区域 --> |
| | |
| | | |
| | | <!-- 右侧区域 --> |
| | | <div class="right-panel"> |
| | | |
| | | <RightTop /> |
| | | <RightBottom /> |
| | | </div> |
| | |
| | | |
| | | // 添加表行类名方法 |
| | | const tableRowClassName = ({ row }) => { |
| | | const diff = row.deliveryDaysDiff; |
| | | if (row.isFh) return ''; |
| | | |
| | | const diff = row.deliveryDaysDiff; |
| | | if (diff === 15) { |
| | | return 'yellow'; |
| | | } else if (diff === 10) { |