| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="app-container trace-page"> |
| | | <PageHeader v-if="showHeader" |
| | | content="生产订单" /> |
| | | <el-card style="height:82vh;overflow:auto;"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | content="生产追溯" /> |
| | | |
| | | <el-card class="filter-card" |
| | | shadow="never"> |
| | | <el-form :inline="true" |
| | | :model="searchForm" |
| | | class="search-form"> |
| | | class="search-form" |
| | | @submit.prevent> |
| | | <el-form-item label="生产订单号"> |
| | | <el-select v-model="selectedNpsNo" |
| | | filterable |
| | | remote |
| | | reserve-keyword |
| | | <el-input v-model="searchForm.npsNo" |
| | | placeholder="请输入生产订单号" |
| | | :loading="npsNoLoading" |
| | | :remote-method="handleNpsNoSearch" |
| | | @change="handleSearch" |
| | | style="width: 400px;"> |
| | | <el-option v-for="option in npsNoOptions" |
| | | :key="option.id" |
| | | :label="option.npsNo + '-' + option.productName + '-' + option.model" |
| | | :value="option.id" /> |
| | | clearable |
| | | prefix-icon="Search" |
| | | style="width: 200px;" |
| | | @keyup.enter="handleQuery" |
| | | @clear="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="产品名称"> |
| | | <el-input v-model="searchForm.productName" |
| | | placeholder="请输入产品名称" |
| | | clearable |
| | | prefix-icon="Search" |
| | | style="width: 180px;" |
| | | @keyup.enter="handleQuery" |
| | | @clear="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="规格"> |
| | | <el-input v-model="searchForm.model" |
| | | placeholder="请输入规格" |
| | | clearable |
| | | prefix-icon="Search" |
| | | style="width: 180px;" |
| | | @keyup.enter="handleQuery" |
| | | @clear="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="状态"> |
| | | <el-select v-model="searchForm.status" |
| | | placeholder="全部" |
| | | clearable |
| | | style="width: 140px;" |
| | | @change="handleQuery"> |
| | | <el-option v-for="item in statusOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" |
| | | icon="Search" |
| | | @click="handleQuery">搜索</el-button> |
| | | <el-button icon="Refresh" |
| | | @click="handleReset">重置</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | |
| | | <div class="trace-layout"> |
| | | <el-card class="order-panel" |
| | | shadow="never" |
| | | :body-style="{ padding: '12px 14px 8px' }"> |
| | | <template #header> |
| | | <div class="panel-header"> |
| | | <span class="panel-title"> |
| | | <el-icon><Tickets /></el-icon> |
| | | 生产订单 |
| | | </span> |
| | | <el-tag type="info" |
| | | effect="plain" |
| | | round>共 {{ page.total }} 条</el-tag> |
| | | </div> |
| | | </template> |
| | | <!-- 基础信息 --> |
| | | <div v-if="rowData.productionOrderDto" |
| | | class="detail-section"> |
| | | <h3 class="section-title">基础信息</h3> |
| | | <el-table ref="orderTableRef" |
| | | v-loading="listLoading" |
| | | :data="orderList" |
| | | row-key="id" |
| | | border |
| | | highlight-current-row |
| | | height="calc(100vh - 28em)" |
| | | @row-click="handleSelectOrder"> |
| | | <el-table-column label="生产订单号" |
| | | min-width="210"> |
| | | <template #default="{ row }"> |
| | | <div class="order-cell"> |
| | | <span class="order-no">{{ row.npsNo || '-' }}</span> |
| | | <span class="order-sub">{{ [row.productName, row.model].filter(Boolean).join(' / ') || '-' }}</span> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="状态" |
| | | width="82" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | <el-tag size="small" |
| | | effect="light" |
| | | :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="完成进度" |
| | | width="116" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | <div class="mini-progress"> |
| | | <el-progress :percentage="toProgress(row.completionStatus)" |
| | | :color="progressColor(row.completionStatus)" |
| | | :stroke-width="6" |
| | | :show-text="false" /> |
| | | <span class="mini-progress-text">{{ percentText(row.completionStatus) }}</span> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" |
| | | width="70" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button type="primary" |
| | | link |
| | | @click.stop="handleSelectOrder(row)">追溯</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | <template #empty> |
| | | <el-empty description="暂无生产订单" |
| | | :image-size="80" /> |
| | | </template> |
| | | </el-table> |
| | | <div class="panel-pagination"> |
| | | <el-pagination small |
| | | background |
| | | layout="sizes, prev, pager, next" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | :pager-count="5" |
| | | :total="page.total" |
| | | :current-page="page.current" |
| | | :page-size="page.size" |
| | | @current-change="handlePageChange" |
| | | @size-change="handleSizeChange" /> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <div class="detail-panel"> |
| | | <el-card v-if="!currentOrder" |
| | | class="placeholder-card" |
| | | shadow="never"> |
| | | <div class="placeholder"> |
| | | <div class="placeholder-icon"> |
| | | <el-icon><Aim /></el-icon> |
| | | </div> |
| | | <p class="placeholder-title">生产全流程追溯</p> |
| | | <p class="placeholder-tip"> |
| | | 从左侧订单列表中选择一个生产订单,即可查看该订单的基础信息、工序进度、报工记录与质检信息 |
| | | </p> |
| | | </div> |
| | | <div v-if="orderList.length" |
| | | class="quick-orders"> |
| | | <div class="quick-title">快速查看当前页订单</div> |
| | | <div class="quick-list"> |
| | | <div v-for="item in orderList.slice(0, 6)" |
| | | :key="item.id" |
| | | class="quick-item" |
| | | @click="handleSelectOrder(item)"> |
| | | <span class="quick-no">{{ item.npsNo }}</span> |
| | | <span class="quick-name">{{ item.productName || '-' }}</span> |
| | | <el-tag size="small" |
| | | effect="light" |
| | | :type="getStatusType(item.status)">{{ getStatusText(item.status) }}</el-tag> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <template v-else> |
| | | <el-card class="detail-card" |
| | | shadow="never" |
| | | v-loading="detailLoading"> |
| | | <div class="detail-head"> |
| | | <div class="detail-head-left"> |
| | | <el-image v-if="productImage" |
| | | :src="productImage" |
| | | :preview-src-list="productImageList" |
| | | preview-teleported |
| | | fit="cover" |
| | | class="product-thumb" /> |
| | | <div v-else |
| | | class="product-thumb product-thumb-empty"> |
| | | <el-icon><Box /></el-icon> |
| | | </div> |
| | | <div class="detail-head-info"> |
| | | <div class="detail-title-row"> |
| | | <span class="detail-no">{{ currentOrder.npsNo || '-' }}</span> |
| | | <el-tag :type="getStatusType(currentOrder.status)" |
| | | effect="light">{{ getStatusText(currentOrder.status) }}</el-tag> |
| | | <el-tag v-if="currentOrder.endOrder" |
| | | type="danger" |
| | | effect="plain">已结束</el-tag> |
| | | </div> |
| | | <div class="detail-sub"> |
| | | <span>{{ orderInfo.productName || '-' }}</span> |
| | | <el-divider direction="vertical" /> |
| | | <span>规格:{{ orderInfo.model || '-' }}</span> |
| | | <el-divider direction="vertical" /> |
| | | <span>客户:{{ orderInfo.customerName || '-' }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="detail-head-actions"> |
| | | <el-button link |
| | | type="primary" |
| | | icon="Refresh" |
| | | @click="loadDetail(currentOrder)">刷新</el-button> |
| | | <el-button link |
| | | icon="Close" |
| | | @click="handleCloseDetail">收起</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="stat-grid"> |
| | | <div v-for="item in statTiles" |
| | | :key="item.label" |
| | | class="stat-tile" |
| | | :class="`tone-${item.tone}`"> |
| | | <div class="stat-tile-value">{{ item.value }}</div> |
| | | <div class="stat-tile-label">{{ item.label }}</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <h4 class="sub-title">基础信息</h4> |
| | | <el-descriptions :column="3" |
| | | border> |
| | | <el-descriptions-item label="生产订单号">{{ rowData.productionOrderDto?.npsNo || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="产品名称">{{ rowData.productionOrderDto?.productName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="产品规格">{{ rowData.productionOrderDto?.model || '-' }}</el-descriptions-item> |
| | | <!-- <el-descriptions-item label="物料编码">{{ rowData.productionOrderDto?.materialCode || '-' }}</el-descriptions-item> --> |
| | | <el-descriptions-item label="计划数量">{{ rowData.productionOrderDto?.quantity || 0 }} <span class="unit">{{ rowData.productionOrderDto?.unit || '-' }}</span></el-descriptions-item> |
| | | <el-descriptions-item label="当前状态"> |
| | | <el-tag :type="getStatusType(rowData.productionOrderDto?.status)"> |
| | | {{ getStatusText(rowData.productionOrderDto?.status) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="客户名称">{{ rowData.productionOrderDto?.customerName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="开始日期">{{ parseTime(rowData.productionOrderDto?.startTime,'{y}-{m}-{d}') || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="完成进度"> |
| | | <el-progress :percentage="rowData.productionOrderDto?.completionStatus>=100?100:rowData.productionOrderDto?.completionStatus" |
| | | :color="customColors(rowData.productionOrderDto?.completionStatus)" |
| | | :status="rowData.productionOrderDto?.completionStatus >= 100 ? 'success' : ''" |
| | | style="width: 80%;" /> |
| | | <el-descriptions-item label="生产订单号">{{ orderInfo.npsNo || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="产品名称">{{ orderInfo.productName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="产品规格">{{ orderInfo.model || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="销售单号">{{ orderInfo.salesContractNo || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="客户名称">{{ orderInfo.customerName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="工艺路线编号">{{ orderInfo.processRouteCode || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="BOM编号">{{ orderInfo.bomNo || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="开始日期">{{ orderStartDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="计划完成时间">{{ parseTime(orderInfo.planCompleteTime, '{y}-{m}-{d}') || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="结束日期">{{ orderEndDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="创建时间">{{ parseTime(orderInfo.createTime, '{y}-{m}-{d}') || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="订单完成进度"> |
| | | <el-progress :percentage="toProgress(orderInfo.completionStatus)" |
| | | :color="progressColor(orderInfo.completionStatus)" |
| | | :status="toPercent(orderInfo.completionStatus) >= 100 ? 'success' : ''"> |
| | | <span>{{ percentText(orderInfo.completionStatus) }}</span> |
| | | </el-progress> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-card> |
| | | |
| | | <el-card v-if="workOrderList.length" |
| | | class="detail-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="panel-header"> |
| | | <span class="panel-title"> |
| | | <el-icon><Operation /></el-icon> |
| | | 工序进度 |
| | | </span> |
| | | <span class="panel-hint">点击工序卡片可查看该工序的报工记录</span> |
| | | </div> |
| | | <el-empty v-else |
| | | description="请搜索生产订单号" /> |
| | | <!-- 生产报工记录 --> |
| | | <div v-if="rowData.productionRecords && rowData.productionRecords.length > 0" |
| | | class="progress-container"> |
| | | <div class="progress-section"> |
| | | <h3 class="section-title">工单信息</h3> |
| | | <div class="order-item"> |
| | | <el-table :data="rowData.productionRecords" |
| | | </template> |
| | | <div class="flow-steps"> |
| | | <div v-for="(item, index) in workOrderList" |
| | | :key="item.workOrder?.id ?? index" |
| | | class="flow-step" |
| | | :class="stepClass(item)" |
| | | @click="handleClickStep(item)"> |
| | | <div class="flow-step-main"> |
| | | <div class="flow-step-circle">{{ percentLabel(item.workOrder?.completionStatus) }}</div> |
| | | <div class="flow-step-name" |
| | | :title="item.workOrder?.operationName">{{ item.workOrder?.operationName || '未命名工序' }}</div> |
| | | <div class="flow-step-qty">{{ formatNumber(item.workOrder?.completeQuantity) }}/{{ formatNumber(item.workOrder?.planQuantity) }}</div> |
| | | <div class="flow-step-badges"> |
| | | <span class="badge badge-report">报工 {{ (item.reportList || []).length }}</span> |
| | | <span class="badge badge-inspect">质检 {{ (item.inspectList || []).length }}</span> |
| | | </div> |
| | | </div> |
| | | <div v-if="index < workOrderList.length - 1" |
| | | class="flow-step-line"></div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <el-card class="detail-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="panel-header"> |
| | | <span class="panel-title"> |
| | | <el-icon><List /></el-icon> |
| | | 工单信息 |
| | | </span> |
| | | <span class="panel-hint">共 {{ workOrderList.length }} 道工序</span> |
| | | </div> |
| | | </template> |
| | | <el-table :data="workOrderList" |
| | | border |
| | | style="width: 100%"> |
| | | <el-table-column prop="workOrder.workOrderNo" |
| | | label="工单编号" |
| | | align="center"> |
| | | </el-table-column> |
| | | <el-table-column label="产品名称" |
| | | <el-table-column label="工单编号" |
| | | min-width="150" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | {{ row.workOrder.productName || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="规格" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | {{ row.workOrder.model || '-' }} |
| | | {{ row.workOrder?.workOrderNo || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="工序" |
| | | prop="workOrder.operationName" |
| | | align="center" /> |
| | | <el-table-column prop="workOrder.planQuantity" |
| | | label="需求数量" |
| | | align="center" /> |
| | | <el-table-column prop="workOrder.completeQuantity" |
| | | label="完成数量" |
| | | align="center" /> |
| | | <el-table-column prop="workOrder.scrapQty" |
| | | label="报废数量" |
| | | align="center" /> |
| | | <el-table-column prop="workOrder.completionStatus" |
| | | label="完成进度" |
| | | min-width="110" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | <span :style="{ color: customColors(row.workOrder.completionStatus) }">{{ row.workOrder.completionStatus || 0 }}%</span> |
| | | {{ row.workOrder?.operationName || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="详情" |
| | | align="center" |
| | | width="200"> |
| | | <el-table-column label="产品名称" |
| | | min-width="120" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | <el-link @click="handleClickStep(row)" |
| | | type="primary">报工记录</el-link> |
| | | <el-link @click="handleClickQuality(row)" |
| | | style="margin-left:20px" |
| | | type="primary">质检信息</el-link> |
| | | {{ row.workOrder?.productName || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="规格" |
| | | min-width="110" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | {{ row.workOrder?.model || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="需求数量" |
| | | width="90" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | {{ formatNumber(row.workOrder?.planQuantity) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="完成数量" |
| | | width="90" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | {{ formatNumber(row.workOrder?.completeQuantity) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="报废数量" |
| | | width="90" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | <span :class="{ 'danger-text': Number(row.workOrder?.scrapQty) > 0 }">{{ formatNumber(row.workOrder?.scrapQty) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="完成进度" |
| | | width="150" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | <el-progress :percentage="toProgress(row.workOrder?.completionStatus)" |
| | | :color="progressColor(row.workOrder?.completionStatus)" |
| | | :status="toPercent(row.workOrder?.completionStatus) >= 100 ? 'success' : ''"> |
| | | <span>{{ percentText(row.workOrder?.completionStatus) }}</span> |
| | | </el-progress> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="报工/质检" |
| | | width="110" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | <span class="badge badge-report">{{ (row.reportList || []).length }}</span> |
| | | <span class="badge badge-inspect">{{ (row.inspectList || []).length }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="实际时间" |
| | | min-width="190" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | {{ parseTime(row.workOrder?.actualStartTime, '{y}-{m}-{d}') || '-' }} |
| | | ~ |
| | | {{ parseTime(row.workOrder?.actualEndTime, '{y}-{m}-{d}') || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" |
| | | width="170" |
| | | align="center" |
| | | fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button type="primary" |
| | | link |
| | | @click="handleClickStep(row)">报工记录</el-button> |
| | | <el-button type="success" |
| | | link |
| | | @click="handleClickQuality(row)">质检信息</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | <template #empty> |
| | | <el-empty description="暂无工单数据" |
| | | :image-size="80" /> |
| | | </template> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <el-empty v-else-if="rowData.productionOrderDto" |
| | | description="暂无报工记录" /> |
| | | </el-card> |
| | | <!-- 生产报工详情弹窗 --> |
| | | </template> |
| | | </div> |
| | | </div> |
| | | |
| | | <el-dialog v-model="detailDialogVisible" |
| | | title="生产报工详情" |
| | | :title="reportDialogTitle" |
| | | width="1000px" |
| | | :close-on-click-modal="false" |
| | | custom-class="custom-dialog"> |
| | | class="trace-dialog"> |
| | | <div class="detail-container"> |
| | | <!-- 基础信息 --> |
| | | <div class="detail-section"> |
| | | <h3 class="section-title">工单基础信息</h3> |
| | | <el-descriptions :column="3" |
| | | border> |
| | | <el-descriptions-item label="生产工单号">{{ detailData.workOrder.workOrderNo || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="计划数量">{{ detailData.workOrder.planQuantity || 0 }}</el-descriptions-item> |
| | | <el-descriptions-item label="完成数量">{{ detailData.workOrder.completeQuantity || 0 }}</el-descriptions-item> |
| | | <el-descriptions-item label="工序名称">{{ detailData.workOrder.operationName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="计划数量">{{ formatNumber(detailData.workOrder.planQuantity) }}</el-descriptions-item> |
| | | <el-descriptions-item label="完成数量">{{ formatNumber(detailData.workOrder.completeQuantity) }}</el-descriptions-item> |
| | | <el-descriptions-item label="报废数量">{{ formatNumber(detailData.workOrder.scrapQty) }}</el-descriptions-item> |
| | | <el-descriptions-item label="完成进度"> |
| | | <span :style="{ color: progressColor(detailData.workOrder.completionStatus) }">{{ percentText(detailData.workOrder.completionStatus) }}</span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="计划开始时间">{{ parseTime(detailData.workOrder.planStartTime, '{y}-{m}-{d}') || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="计划完成时间">{{ parseTime(detailData.workOrder.planEndTime, '{y}-{m}-{d}') || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="实际开始时间">{{ parseTime(detailData.workOrder.actualStartTime, '{y}-{m}-{d}') || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="实际结束时间">{{ parseTime(detailData.workOrder.actualEndTime, '{y}-{m}-{d}') || '-' }}</el-descriptions-item> |
| | | <!-- <el-descriptions-item label="完成进度"> |
| | | <el-progress :percentage="detailData.workOrder.completionStatus >= 100 ? 100 : (detailData.workOrder.completionStatus || 0)" |
| | | :color="customColors(detailData.workOrder.completionStatus)" |
| | | :status="detailData.workOrder.completionStatus >= 100 ? 'success' : ''" |
| | | style="width:500px;" /> |
| | | </el-descriptions-item> --> |
| | | <el-descriptions-item label="完成进度"><span :style="{ color: customColors(detailData.workOrder.completionStatus) }">{{ detailData.workOrder.completionStatus || 0 }}%</span></el-descriptions-item> |
| | | <el-descriptions-item label="报工人员">{{ detailData.workOrder.userNames || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="报工次数">{{ detailData.reports.length }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </div> |
| | | <div class="detail-section"> |
| | |
| | | <el-table :data="detailData.reports" |
| | | border |
| | | style="width: 100%"> |
| | | <el-table-column label="序号" |
| | | type="index" |
| | | width="60" |
| | | align="center" /> |
| | | <el-table-column label="报工单号" |
| | | prop="productNo" |
| | | align="center" /> |
| | |
| | | <el-table-column label="创建时间" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | {{ parseTime(row.createTime, '{y}-{m}-{d}') }} |
| | | {{ parseTime(row.createTime, '{y}-{m}-{d}') || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="工时(h)" |
| | | prop="workHour" |
| | | align="center" /> |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | {{ formatNumber(row.workHour) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="产出数量" |
| | | prop="quantity" |
| | | align="center" /> |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | {{ formatNumber(row.quantity) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="报废数量" |
| | | prop="scrapQty" |
| | | align="center" /> |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | {{ formatNumber(row.scrapQty) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" |
| | | align="center" |
| | | width="200"> |
| | |
| | | @click="showParamDetail(row.productionOperationParamList)">参数详情</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | <template #empty> |
| | | <el-empty description="暂无报工记录" |
| | | :image-size="80" /> |
| | | </template> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 投入模态框 --> |
| | | |
| | | <input-modal v-if="isShowInput" |
| | | v-model:visible="isShowInput" |
| | | :production-product-main-id="isShowingId" /> |
| | | <!-- 参数详情弹窗 --> |
| | | |
| | | <el-dialog v-model="paramDetailVisible" |
| | | title="参数详情" |
| | | width="600px"> |
| | | width="600px" |
| | | class="trace-dialog"> |
| | | <div v-if="currentParams && currentParams.length > 0" |
| | | class="param-detail-list"> |
| | | <el-descriptions :column="1" |
| | |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 质检信息弹窗 --> |
| | | |
| | | <el-dialog v-model="qualityDialogVisible" |
| | | title="质检详情" |
| | | width="1000px" |
| | | :close-on-click-modal="false" |
| | | custom-class="custom-dialog"> |
| | | class="trace-dialog"> |
| | | <div class="detail-container"> |
| | | <div v-if="qualityRecords.length"> |
| | | <div v-for="(record, index) in qualityRecords" |
| | | :key="record.id" |
| | | :key="record.id ?? index" |
| | | class="quality-record-block"> |
| | | <div class="detail-section"> |
| | | <h3 class="section-title">检测记录 {{ index + 1 }} - {{ parseTime(record.createTime, '{y}-{m}-{d}') }}</h3> |
| | |
| | | <el-descriptions-item label="报工人员">{{ record.userName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="产品名称">{{ record.productName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="规格型号">{{ record.model || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="数量">{{ record.quantity || 0 }} {{ record.unit || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="数量">{{ formatNumber(record.quantity) }} {{ record.unit || '' }}</el-descriptions-item> |
| | | <el-descriptions-item label="检测单位">{{ record.checkCompany || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="检验员">{{ record.checkName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="合格率"> |
| | |
| | | {{ getPassRateDisplay(record) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="合格数量">{{ formatNumber(record.qualifiedQuantity) }}</el-descriptions-item> |
| | | <el-descriptions-item label="不合格数量">{{ formatNumber(record.unqualifiedQuantity) }}</el-descriptions-item> |
| | | <el-descriptions-item label="检验结论">{{ record.checkResult || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="不合格现象" |
| | | :span="3">{{ record.defectivePhenomena || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | <h4 class="sub-section-title">检验指标列表</h4> |
| | | <el-table :data="record.inspectParamList" |
| | |
| | | <el-table-column label="实际值" |
| | | prop="testValue" |
| | | align="center" /> |
| | | <template #empty> |
| | | <el-empty description="暂无检验指标" |
| | | :image-size="60" /> |
| | | </template> |
| | | </el-table> |
| | | <div v-if="record.inspectFileList && record.inspectFileList.length" |
| | | class="inspect-files"> |
| | | <h4 class="sub-section-title">检验附件</h4> |
| | | <div class="inspect-file-list"> |
| | | <a v-for="file in record.inspectFileList" |
| | | :key="file.id" |
| | | :href="resolveFileUrl(file.url) || undefined" |
| | | :target="resolveFileUrl(file.url) ? '_blank' : undefined" |
| | | class="inspect-file-item" |
| | | :class="{ 'is-disabled': !resolveFileUrl(file.url) }"> |
| | | <el-icon><Paperclip /></el-icon> |
| | | {{ file.name || '附件' }} |
| | | </a> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <el-divider v-if="index < qualityRecords.length - 1" /> |
| | | </div> |
| | | </div> |
| | | <el-empty v-else |
| | | description="暂无质检记录" /> |
| | | </div> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, computed } from "vue"; |
| | | import { useRoute, useRouter } from "vue-router"; |
| | | import { ref, reactive, computed, onMounted } from "vue"; |
| | | import { useRoute } from "vue-router"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { parseTime } from "@/utils/ruoyi"; |
| | | import InputModal from "@/views/productionManagement/productionReporting/Input.vue"; |
| | |
| | | } from "@/api/productionManagement/productionOrder"; |
| | | |
| | | const route = useRoute(); |
| | | const router = useRouter(); |
| | | |
| | | // 判断是否显示页头 |
| | | const statusOptions = [ |
| | | { label: "待开始", value: 1 }, |
| | | { label: "进行中", value: 2 }, |
| | | { label: "已完成", value: 3 }, |
| | | { label: "已取消", value: 4 }, |
| | | { label: "已结束", value: 5 }, |
| | | ]; |
| | | |
| | | const showHeader = computed(() => { |
| | | return ( |
| | | Object.keys(route.query).length > 0 || Object.keys(route.params).length > 0 |
| | | ); |
| | | }); |
| | | |
| | | // 搜索相关 |
| | | const searchForm = reactive({ |
| | | npsNo: "", |
| | | }); |
| | | const selectedNpsNo = ref(null); |
| | | const npsNoLoading = ref(false); |
| | | const npsNoOptions = ref([]); |
| | | |
| | | // 详情数据 |
| | | const rowData = reactive({ |
| | | productionOrderDto: null, |
| | | productionRecords: [], |
| | | productName: "", |
| | | model: "", |
| | | status: "", |
| | | }); |
| | | |
| | | // 报工详情弹窗 |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 20, |
| | | total: 0, |
| | | }); |
| | | |
| | | const orderList = ref([]); |
| | | const listLoading = ref(false); |
| | | const orderTableRef = ref(null); |
| | | |
| | | const currentOrder = ref(null); |
| | | const detailLoading = ref(false); |
| | | const detail = ref({ |
| | | productionOrder: null, |
| | | workOrderList: [], |
| | | }); |
| | | |
| | | const workOrderList = computed(() => detail.value.workOrderList || []); |
| | | const orderInfo = computed( |
| | | () => detail.value.productionOrder || currentOrder.value || {} |
| | | ); |
| | | const productImageList = computed(() => |
| | | (orderInfo.value.productImages || []) |
| | | .map(item => item.previewURL || item.url) |
| | | .filter(Boolean) |
| | | ); |
| | | const productImage = computed(() => productImageList.value[0] || ""); |
| | | const unitText = computed(() => workOrderList.value[0]?.workOrder?.unit || ""); |
| | | |
| | | const firstDate = values => { |
| | | const list = values.filter(Boolean).map(String).sort(); |
| | | return list.length ? parseTime(list[0], "{y}-{m}-{d}") : null; |
| | | }; |
| | | |
| | | const lastDate = values => { |
| | | const list = values.filter(Boolean).map(String).sort(); |
| | | return list.length ? parseTime(list[list.length - 1], "{y}-{m}-{d}") : null; |
| | | }; |
| | | |
| | | const reportCreateTimes = computed(() => |
| | | workOrderList.value.flatMap(item => |
| | | (item.reportList || []).map(report => report.reportMain?.createTime) |
| | | ) |
| | | ); |
| | | |
| | | const orderStartDate = computed(() => { |
| | | const info = orderInfo.value; |
| | | if (info.startTime) return parseTime(info.startTime, "{y}-{m}-{d}"); |
| | | const fromWorkOrders = firstDate( |
| | | workOrderList.value.map(item => item.workOrder?.actualStartTime) |
| | | ); |
| | | if (fromWorkOrders) return fromWorkOrders; |
| | | return firstDate(reportCreateTimes.value) || "-"; |
| | | }); |
| | | |
| | | const orderEndDate = computed(() => { |
| | | const info = orderInfo.value; |
| | | if (info.endTime) return parseTime(info.endTime, "{y}-{m}-{d}"); |
| | | const fromWorkOrders = lastDate( |
| | | workOrderList.value.map(item => item.workOrder?.actualEndTime) |
| | | ); |
| | | if (fromWorkOrders) return fromWorkOrders; |
| | | return lastDate(reportCreateTimes.value) || "-"; |
| | | }); |
| | | |
| | | const getStatusType = status => { |
| | | const typeMap = { 1: "primary", 2: "warning", 3: "success", 5: "danger" }; |
| | | return typeMap[status] || "info"; |
| | | }; |
| | | const getStatusText = status => { |
| | | if (status === null || status === undefined || status === "") return "-"; |
| | | const statusMap = { 1: "待开始", 2: "进行中", 3: "已完成", 5: "已结束", 4: "已取消" }; |
| | | return statusMap[status] || "-"; |
| | | }; |
| | | |
| | | const resolveFileUrl = url => { |
| | | if (!url) return ""; |
| | | if (/^(https?:)?\/\//i.test(url) || url.startsWith("data:")) return url; |
| | | return ""; |
| | | }; |
| | | |
| | | const toPercent = value => { |
| | | const num = Number(value); |
| | | if (!Number.isFinite(num) || num <= 0) return 0; |
| | | return Math.round(num * 100) / 100; |
| | | }; |
| | | |
| | | const toProgress = value => Math.min(100, Math.round(toPercent(value))); |
| | | |
| | | const percentText = value => `${toPercent(value)}%`; |
| | | |
| | | const percentLabel = value => `${Math.round(toPercent(value))}%`; |
| | | |
| | | const progressColor = value => { |
| | | const percentage = toPercent(value); |
| | | if (percentage < 30) return "#f56c6c"; |
| | | if (percentage < 70) return "#e6a23c"; |
| | | return "#67c23a"; |
| | | }; |
| | | |
| | | const formatNumber = value => { |
| | | if (value === null || value === undefined || value === "") return "-"; |
| | | const num = Number(value); |
| | | if (!Number.isFinite(num)) return value; |
| | | return Number.isInteger(num) ? num : Number(num.toFixed(2)); |
| | | }; |
| | | |
| | | const summary = computed(() => { |
| | | let scrapQty = 0; |
| | | let reportCount = 0; |
| | | let inspectCount = 0; |
| | | let qualified = 0; |
| | | let unqualified = 0; |
| | | workOrderList.value.forEach(item => { |
| | | const workOrder = item.workOrder || {}; |
| | | scrapQty += Number(workOrder.scrapQty) || 0; |
| | | reportCount += (item.reportList || []).length; |
| | | const inspectList = item.inspectList || []; |
| | | inspectCount += inspectList.length; |
| | | inspectList.forEach(record => { |
| | | qualified += Number(record.inspect?.qualifiedQuantity) || 0; |
| | | unqualified += Number(record.inspect?.unqualifiedQuantity) || 0; |
| | | }); |
| | | }); |
| | | const totalInspect = qualified + unqualified; |
| | | return { |
| | | scrapQty, |
| | | reportCount, |
| | | inspectCount, |
| | | workOrderCount: workOrderList.value.length, |
| | | passRate: totalInspect |
| | | ? Number(((qualified / totalInspect) * 100).toFixed(2)) |
| | | : null, |
| | | }; |
| | | }); |
| | | |
| | | const statTiles = computed(() => { |
| | | const info = orderInfo.value; |
| | | const data = summary.value; |
| | | return [ |
| | | { label: "计划数量", value: withUnit(info.quantity), tone: "blue" }, |
| | | { label: "完成数量", value: withUnit(info.completeQuantity), tone: "green" }, |
| | | { label: "报废数量", value: withUnit(data.scrapQty), tone: "red" }, |
| | | { label: "完成进度", value: percentText(info.completionStatus), tone: "orange" }, |
| | | { label: "工单数", value: data.workOrderCount, tone: "indigo" }, |
| | | { label: "报工记录", value: data.reportCount, tone: "cyan" }, |
| | | { label: "质检记录", value: data.inspectCount, tone: "purple" }, |
| | | { |
| | | label: "良品率", |
| | | value: data.passRate === null ? "—" : `${data.passRate}%`, |
| | | tone: "teal", |
| | | }, |
| | | ]; |
| | | }); |
| | | |
| | | const withUnit = value => { |
| | | const text = formatNumber(value); |
| | | if (text === "-") return text; |
| | | return unitText.value ? `${text} ${unitText.value}` : text; |
| | | }; |
| | | |
| | | const stepClass = item => { |
| | | const percentage = toPercent(item.workOrder?.completionStatus); |
| | | if (percentage >= 100) return "is-done"; |
| | | if (percentage > 0) return "is-active"; |
| | | return ""; |
| | | }; |
| | | |
| | | const getList = async () => { |
| | | listLoading.value = true; |
| | | try { |
| | | const res = await productOrderListPage({ |
| | | npsNo: searchForm.npsNo || undefined, |
| | | productName: searchForm.productName || undefined, |
| | | model: searchForm.model || undefined, |
| | | status: searchForm.status || undefined, |
| | | current: page.current, |
| | | size: page.size, |
| | | }); |
| | | const data = res.data || {}; |
| | | orderList.value = data.records || []; |
| | | page.total = Number(data.total) || 0; |
| | | } catch (error) { |
| | | console.error(error); |
| | | ElMessage.error("获取生产订单列表失败"); |
| | | orderList.value = []; |
| | | page.total = 0; |
| | | } finally { |
| | | listLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const handleReset = () => { |
| | | searchForm.npsNo = ""; |
| | | searchForm.productName = ""; |
| | | searchForm.model = ""; |
| | | searchForm.status = ""; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | const handlePageChange = current => { |
| | | page.current = current; |
| | | getList(); |
| | | }; |
| | | |
| | | const handleSizeChange = size => { |
| | | page.size = size; |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const loadDetail = async row => { |
| | | if (!row?.npsNo) return; |
| | | detailLoading.value = true; |
| | | detail.value = { productionOrder: row, workOrderList: [] }; |
| | | try { |
| | | const res = await getOrderDetail(row.npsNo); |
| | | if (res.code === 200 && res.data) { |
| | | const loadedOrder = res.data.productionOrder || row; |
| | | detail.value = { |
| | | productionOrder: loadedOrder, |
| | | workOrderList: res.data.workOrderList || [], |
| | | }; |
| | | currentOrder.value = loadedOrder; |
| | | } else { |
| | | ElMessage.warning(res.msg || "获取生产订单追溯信息失败"); |
| | | } |
| | | } catch (error) { |
| | | console.error(error); |
| | | ElMessage.error("获取生产订单追溯信息失败"); |
| | | } finally { |
| | | detailLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const handleSelectOrder = row => { |
| | | if (!row) return; |
| | | currentOrder.value = row; |
| | | orderTableRef.value?.setCurrentRow(row); |
| | | loadDetail(row); |
| | | }; |
| | | |
| | | const handleCloseDetail = () => { |
| | | currentOrder.value = null; |
| | | detail.value = { productionOrder: null, workOrderList: [] }; |
| | | orderTableRef.value?.setCurrentRow(); |
| | | }; |
| | | |
| | | const detailDialogVisible = ref(false); |
| | | const detailData = ref({ |
| | | workOrder: {}, |
| | | reports: [], |
| | | }); |
| | | |
| | | // 投入模态框 |
| | | const reportDialogTitle = computed(() => { |
| | | const operationName = detailData.value.workOrder?.operationName; |
| | | return operationName ? `${operationName} - 报工记录` : "生产报工详情"; |
| | | }); |
| | | |
| | | const handleClickStep = row => { |
| | | detailData.value = { |
| | | workOrder: row.workOrder || {}, |
| | | reports: (row.reportList || []).map(item => ({ |
| | | ...(item.reportMain || {}), |
| | | ...((item.reportOutputList || [])[0] || {}), |
| | | workHour: item.workHour ?? item.reportMain?.workHour, |
| | | productionOperationParamList: item.reportParamList || [], |
| | | })), |
| | | }; |
| | | detailDialogVisible.value = true; |
| | | }; |
| | | |
| | | const isShowInput = ref(false); |
| | | const isShowingId = ref(0); |
| | | const showInput = id => { |
| | |
| | | isShowingId.value = id; |
| | | }; |
| | | |
| | | // 参数详情弹窗 |
| | | const paramDetailVisible = ref(false); |
| | | const currentParams = ref([]); |
| | | const showParamDetail = params => { |
| | |
| | | paramDetailVisible.value = true; |
| | | }; |
| | | |
| | | // 质检信息弹窗 |
| | | const qualityDialogVisible = ref(false); |
| | | const qualityRecords = ref([]); |
| | | |
| | | // 状态处理 |
| | | const getStatusType = status => { |
| | | const typeMap = { 1: "primary", 2: "warning", 3: "success", 5: "danger" }; |
| | | return typeMap[status] || "info"; |
| | | }; |
| | | const getStatusText = status => { |
| | | const statusMap = { 1: "待开始", 2: "进行中", 3: "已完成", 5: "已结束" }; |
| | | return statusMap[status] || "已取消"; |
| | | }; |
| | | const customColors = percentage => { |
| | | if (percentage < 30) return "#f56c6c"; |
| | | if (percentage < 70) return "#e6a23c"; |
| | | return "#67c23a"; |
| | | const handleClickQuality = row => { |
| | | qualityRecords.value = (row.inspectList || []).map(item => ({ |
| | | ...(item.inspect || {}), |
| | | reportNo: item.reportNo, |
| | | userName: item.reportMain?.userName || "-", |
| | | inspectParamList: item.inspectParamList || [], |
| | | inspectFileList: item.inspectFileList || [], |
| | | })); |
| | | qualityDialogVisible.value = true; |
| | | }; |
| | | |
| | | const getPassRate = (record) => { |
| | | const getPassRate = record => { |
| | | const qualified = Number(record.qualifiedQuantity) || 0; |
| | | const unqualified = Number(record.unqualifiedQuantity) || 0; |
| | | const total = qualified + unqualified; |
| | |
| | | return Number(((qualified / total) * 100).toFixed(2)); |
| | | }; |
| | | |
| | | const getPassRateDisplay = (record) => { |
| | | const getPassRateDisplay = record => { |
| | | const rate = getPassRate(record); |
| | | if (rate === null) return '—'; |
| | | if (rate === null) return "—"; |
| | | return `${rate}%`; |
| | | }; |
| | | |
| | | const getPassRateTagType = (record) => { |
| | | const getPassRateTagType = record => { |
| | | const rate = getPassRate(record); |
| | | if (rate === null) return 'info'; |
| | | if (rate >= 100) return 'success'; |
| | | if (rate >= 75) return 'primary'; |
| | | return 'danger'; |
| | | }; |
| | | |
| | | // 模拟搜索方法 |
| | | const handleNpsNoSearch = async query => { |
| | | npsNoLoading.value = true; |
| | | try { |
| | | const res = await productOrderListPage({ |
| | | npsNo: query || "", |
| | | pageNum: 1, |
| | | pageSize: 50, |
| | | }); |
| | | // 参照 productionOrder/index.vue 的数据结构 res.data.records |
| | | npsNoOptions.value = res.data?.records || res.rows || []; |
| | | } catch (error) { |
| | | console.error(error); |
| | | } finally { |
| | | npsNoLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const handleSearch = async id => { |
| | | const selected = npsNoOptions.value.find(item => item.id === id); |
| | | if (selected) { |
| | | try { |
| | | const res = await getOrderDetail(selected.npsNo); |
| | | if (res.code === 200) { |
| | | const { productionOrder, workOrderList } = res.data; |
| | | rowData.productionOrderDto = productionOrder || selected; |
| | | rowData.productionRecords = workOrderList || []; |
| | | } else { |
| | | rowData.productionOrderDto = selected; |
| | | rowData.productionRecords = []; |
| | | } |
| | | } catch (error) { |
| | | console.error(error); |
| | | ElMessage.error("获取订单详情失败"); |
| | | rowData.productionOrderDto = selected; |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const handleBack = () => { |
| | | router.back(); |
| | | }; |
| | | |
| | | const handleClickStep = row => { |
| | | // row 是 workOrderList 中的一项,包含 workOrder, reportList, inspectList |
| | | detailData.value = { |
| | | workOrder: row.workOrder || {}, |
| | | reports: (row.reportList || []).map(r => ({ |
| | | ...r.reportMain, |
| | | ...(r.reportOutputList[0] || {}), |
| | | productionOperationParamList: r.reportParamList || [], |
| | | })), |
| | | }; |
| | | detailDialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleClickQuality = row => { |
| | | // row 是 workOrderList 中的一项 |
| | | const inspects = row.inspectList || []; |
| | | qualityRecords.value = inspects.map(i => ({ |
| | | ...i.inspect, |
| | | reportNo: i.reportNo, |
| | | userName: i.reportMain?.userName || "-", |
| | | inspectParamList: i.inspectParamList || [], |
| | | inspectFileList: i.inspectFileList || [], |
| | | })); |
| | | qualityDialogVisible.value = true; |
| | | if (rate === null) return "info"; |
| | | if (rate >= 100) return "success"; |
| | | if (rate >= 75) return "primary"; |
| | | return "danger"; |
| | | }; |
| | | |
| | | onMounted(async () => { |
| | | // 初始加载列表 |
| | | await handleNpsNoSearch(); |
| | | |
| | | if (route.query.npsNo) { |
| | | const npsNo = route.query.npsNo; |
| | | const found = npsNoOptions.value.find(item => item.npsNo === npsNo); |
| | | const queryNpsNo = route.query.npsNo ? String(route.query.npsNo) : ""; |
| | | if (queryNpsNo) { |
| | | searchForm.npsNo = queryNpsNo; |
| | | } |
| | | await getList(); |
| | | if (!queryNpsNo) return; |
| | | const found = orderList.value.find(item => item.npsNo === queryNpsNo); |
| | | if (found) { |
| | | selectedNpsNo.value = found.id; |
| | | handleSearch(found.id); |
| | | handleSelectOrder(found); |
| | | } else { |
| | | // 如果列表中没有(可能是分页原因),则根据 npsNo 再次精准搜索 |
| | | try { |
| | | const res = await productOrderListPage({ |
| | | npsNo, |
| | | pageNum: -1, |
| | | pageSize: -1, |
| | | }); |
| | | const records = res.data?.records || res.rows || []; |
| | | if (records.length > 0) { |
| | | const item = records[0]; |
| | | npsNoOptions.value.unshift(item); |
| | | selectedNpsNo.value = item.id; |
| | | handleSearch(item.id); |
| | | } |
| | | } catch (error) { |
| | | console.error("获取路由参数对应的订单失败", error); |
| | | } |
| | | } |
| | | handleSelectOrder({ npsNo: queryNpsNo }); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .app-container { |
| | | padding: 20px; |
| | | background-color: #f5f7fa; |
| | | min-height: 100vh; |
| | | <style scoped lang="scss"> |
| | | .trace-page { |
| | | min-height: 100%; |
| | | } |
| | | |
| | | .card-header { |
| | | .filter-card { |
| | | margin-bottom: 16px; |
| | | border-radius: var(--radius-md); |
| | | |
| | | :deep(.el-card__body) { |
| | | padding: 18px 20px 0; |
| | | } |
| | | |
| | | :deep(.el-form-item) { |
| | | margin-bottom: 18px; |
| | | } |
| | | } |
| | | |
| | | .trace-layout { |
| | | display: grid; |
| | | grid-template-columns: minmax(360px, 440px) minmax(0, 1fr); |
| | | gap: 16px; |
| | | align-items: start; |
| | | } |
| | | |
| | | @media (max-width: 1280px) { |
| | | .trace-layout { |
| | | grid-template-columns: minmax(0, 1fr); |
| | | } |
| | | } |
| | | |
| | | .order-panel, |
| | | .detail-card, |
| | | .placeholder-card { |
| | | border-radius: var(--radius-md); |
| | | } |
| | | |
| | | .panel-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 0 10px; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .search-form { |
| | | width: 100%; |
| | | } |
| | | |
| | | .search-form .el-form-item { |
| | | margin-right: 10px; |
| | | } |
| | | |
| | | .detail-section { |
| | | margin-bottom: 24px; |
| | | background-color: #ffffff; |
| | | border-radius: 10px; |
| | | padding: 24px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .detail-section:hover { |
| | | box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.12); |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | margin-bottom: 20px; |
| | | color: #1a1a1a; |
| | | border-bottom: 2px solid #409eff; |
| | | padding-bottom: 10px; |
| | | .panel-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | font-weight: 600; |
| | | color: var(--text-primary); |
| | | } |
| | | |
| | | .section-title::before { |
| | | .panel-hint { |
| | | font-size: 12px; |
| | | font-weight: 400; |
| | | color: var(--text-tertiary); |
| | | } |
| | | |
| | | .order-cell { |
| | | display: flex; |
| | | flex-direction: column; |
| | | padding: 2px 0; |
| | | line-height: 1.4; |
| | | } |
| | | |
| | | .order-no { |
| | | font-weight: 600; |
| | | color: var(--text-primary); |
| | | } |
| | | |
| | | .order-sub { |
| | | font-size: 12px; |
| | | color: var(--text-tertiary); |
| | | margin-top: 2px; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | .mini-progress { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | |
| | | :deep(.el-progress) { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .mini-progress-text { |
| | | font-size: 12px; |
| | | color: var(--text-tertiary); |
| | | width: 34px; |
| | | text-align: right; |
| | | } |
| | | } |
| | | |
| | | .panel-pagination { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding-top: 10px; |
| | | } |
| | | |
| | | .placeholder-card { |
| | | :deep(.el-card__body) { |
| | | padding: 40px 32px; |
| | | } |
| | | } |
| | | |
| | | .placeholder { |
| | | text-align: center; |
| | | padding: 20px 0 8px; |
| | | } |
| | | |
| | | .placeholder-icon { |
| | | width: 72px; |
| | | height: 72px; |
| | | margin: 0 auto 18px; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 34px; |
| | | color: #fff; |
| | | background: linear-gradient(135deg, #3b82f6, #38bdf8); |
| | | box-shadow: 0 12px 24px rgba(37, 99, 235, 0.24); |
| | | } |
| | | |
| | | .placeholder-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: var(--text-primary); |
| | | margin: 0 0 8px; |
| | | } |
| | | |
| | | .placeholder-tip { |
| | | font-size: 13px; |
| | | color: var(--text-tertiary); |
| | | line-height: 1.8; |
| | | margin: 0 auto; |
| | | max-width: 460px; |
| | | } |
| | | |
| | | .quick-orders { |
| | | margin-top: 28px; |
| | | border-top: 1px dashed var(--surface-border); |
| | | padding-top: 20px; |
| | | } |
| | | |
| | | .quick-title { |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | color: var(--text-secondary); |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .quick-list { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); |
| | | gap: 10px; |
| | | } |
| | | |
| | | .quick-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 10px 12px; |
| | | border: 1px solid var(--surface-border); |
| | | border-radius: var(--radius-sm); |
| | | background: var(--surface-soft); |
| | | cursor: pointer; |
| | | transition: all 0.25s ease; |
| | | |
| | | &:hover { |
| | | border-color: var(--surface-border-strong); |
| | | box-shadow: var(--shadow-sm); |
| | | transform: translateY(-1px); |
| | | } |
| | | |
| | | .quick-no { |
| | | font-weight: 600; |
| | | color: var(--text-primary); |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .quick-name { |
| | | flex: 1; |
| | | min-width: 0; |
| | | font-size: 12px; |
| | | color: var(--text-tertiary); |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | } |
| | | } |
| | | |
| | | .detail-card { |
| | | margin-bottom: 16px; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | |
| | | .detail-head { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | gap: 16px; |
| | | padding-bottom: 16px; |
| | | border-bottom: 1px solid var(--surface-border); |
| | | margin-bottom: 18px; |
| | | } |
| | | |
| | | .detail-head-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 14px; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .product-thumb { |
| | | width: 60px; |
| | | height: 60px; |
| | | border-radius: var(--radius-sm); |
| | | border: 1px solid var(--surface-border); |
| | | overflow: hidden; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .product-thumb-empty { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 26px; |
| | | color: #94a3b8; |
| | | background: var(--surface-muted); |
| | | } |
| | | |
| | | .detail-head-info { |
| | | min-width: 0; |
| | | } |
| | | |
| | | .detail-title-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .detail-no { |
| | | font-size: 18px; |
| | | font-weight: 700; |
| | | color: var(--text-primary); |
| | | letter-spacing: 0.02em; |
| | | } |
| | | |
| | | .detail-sub { |
| | | margin-top: 6px; |
| | | font-size: 13px; |
| | | color: var(--text-tertiary); |
| | | display: flex; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .detail-head-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .stat-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); |
| | | gap: 10px; |
| | | margin-bottom: 22px; |
| | | } |
| | | |
| | | .stat-tile { |
| | | position: relative; |
| | | padding: 14px 14px 14px 18px; |
| | | border-radius: var(--radius-sm); |
| | | background: var(--surface-muted); |
| | | border: 1px solid var(--surface-border); |
| | | overflow: hidden; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 0; |
| | | top: 0; |
| | | bottom: 0; |
| | | width: 4px; |
| | | background: var(--tile-accent, #3b82f6); |
| | | } |
| | | |
| | | .stat-tile-value { |
| | | font-size: 20px; |
| | | font-weight: 700; |
| | | color: var(--text-primary); |
| | | line-height: 1.3; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | .stat-tile-label { |
| | | margin-top: 4px; |
| | | font-size: 12px; |
| | | color: var(--text-tertiary); |
| | | } |
| | | } |
| | | |
| | | .tone-blue { |
| | | --tile-accent: #3b82f6; |
| | | } |
| | | |
| | | .tone-green { |
| | | --tile-accent: #10b981; |
| | | } |
| | | |
| | | .tone-red { |
| | | --tile-accent: #ef4444; |
| | | } |
| | | |
| | | .tone-orange { |
| | | --tile-accent: #f59e0b; |
| | | } |
| | | |
| | | .tone-indigo { |
| | | --tile-accent: #6366f1; |
| | | } |
| | | |
| | | .tone-cyan { |
| | | --tile-accent: #06b6d4; |
| | | } |
| | | |
| | | .tone-purple { |
| | | --tile-accent: #a855f7; |
| | | } |
| | | |
| | | .tone-teal { |
| | | --tile-accent: #14b8a6; |
| | | } |
| | | |
| | | .sub-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: var(--text-primary); |
| | | margin: 0 0 12px; |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | display: inline-block; |
| | | width: 4px; |
| | | height: 16px; |
| | | background-color: #409eff; |
| | | margin-right: 8px; |
| | | width: 3px; |
| | | height: 14px; |
| | | background: var(--accent-primary, #2563eb); |
| | | border-radius: 2px; |
| | | margin-right: 8px; |
| | | } |
| | | } |
| | | |
| | | .sub-section-title { |
| | | font-size: 14px; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | margin-bottom: 16px; |
| | | color: var(--text-secondary); |
| | | margin: 18px 0 10px; |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | display: inline-block; |
| | | width: 3px; |
| | | height: 12px; |
| | | background: #67c23a; |
| | | border-radius: 2px; |
| | | margin-right: 8px; |
| | | } |
| | | } |
| | | |
| | | .flow-steps { |
| | | display: flex; |
| | | align-items: center; |
| | | overflow-x: auto; |
| | | padding: 6px 2px 10px; |
| | | } |
| | | |
| | | .flow-step { |
| | | display: flex; |
| | | align-items: center; |
| | | flex: 0 0 auto; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .flow-step-main { |
| | | width: 136px; |
| | | padding: 14px 10px 12px; |
| | | border-radius: var(--radius-sm); |
| | | border: 1px solid var(--surface-border); |
| | | background: var(--surface-soft); |
| | | text-align: center; |
| | | transition: all 0.25s ease; |
| | | |
| | | &:hover { |
| | | border-color: var(--surface-border-strong); |
| | | box-shadow: var(--shadow-sm); |
| | | transform: translateY(-2px); |
| | | } |
| | | } |
| | | |
| | | .flow-step-circle { |
| | | width: 48px; |
| | | height: 48px; |
| | | margin: 0 auto 10px; |
| | | border-radius: 50%; |
| | | border: 2px solid #cbd5e1; |
| | | background: #fff; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | color: var(--text-secondary); |
| | | } |
| | | |
| | | .flow-step.is-done .flow-step-circle { |
| | | border-color: #67c23a; |
| | | color: #67c23a; |
| | | } |
| | | |
| | | .flow-step.is-active .flow-step-circle { |
| | | border-color: #2563eb; |
| | | color: #2563eb; |
| | | } |
| | | |
| | | .flow-step-name { |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | color: var(--text-primary); |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | .flow-step-qty { |
| | | margin-top: 4px; |
| | | font-size: 12px; |
| | | color: var(--text-tertiary); |
| | | } |
| | | |
| | | .flow-step-badges { |
| | | margin-top: 8px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .flow-step-line { |
| | | width: 34px; |
| | | height: 2px; |
| | | margin: 0 6px; |
| | | background: linear-gradient( |
| | | 90deg, |
| | | rgba(148, 163, 184, 0.45), |
| | | rgba(148, 163, 184, 0.12) |
| | | ); |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .badge { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 1px 8px; |
| | | border-radius: 10px; |
| | | font-size: 12px; |
| | | line-height: 18px; |
| | | } |
| | | |
| | | .badge-report { |
| | | color: #2563eb; |
| | | background: rgba(37, 99, 235, 0.1); |
| | | } |
| | | |
| | | .badge-inspect { |
| | | color: #0f9d8f; |
| | | background: rgba(20, 184, 166, 0.12); |
| | | } |
| | | |
| | | .badge + .badge { |
| | | margin-left: 6px; |
| | | } |
| | | |
| | | .danger-text { |
| | | color: #ef4444; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | :deep(.el-descriptions__label) { |
| | | font-weight: 500; |
| | | color: var(--text-secondary); |
| | | } |
| | | |
| | | :deep(.el-table th) { |
| | | background-color: var(--surface-soft) !important; |
| | | font-weight: 600; |
| | | color: var(--text-secondary); |
| | | } |
| | | |
| | | :deep(.el-table .el-table__row) { |
| | | cursor: pointer; |
| | | } |
| | | |
| | | :deep(.el-tag) { |
| | | border-radius: 10px; |
| | | } |
| | | </style> |
| | | |
| | | <style lang="scss"> |
| | | .trace-dialog { |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .trace-dialog .el-dialog__header { |
| | | background-color: #f5f7fa; |
| | | padding: 18px 20px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | margin-right: 0; |
| | | } |
| | | |
| | | .trace-dialog .el-dialog__title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #1a1a1a; |
| | | } |
| | | |
| | | .trace-dialog .el-dialog__body { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .trace-dialog .detail-container { |
| | | max-height: 62vh; |
| | | overflow-y: auto; |
| | | padding: 0 4px; |
| | | } |
| | | |
| | | .trace-dialog .detail-section { |
| | | padding: 4px 0; |
| | | } |
| | | |
| | | .trace-dialog .section-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | margin: 0 0 16px; |
| | | color: #1a1a1a; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .trace-dialog .section-title::before { |
| | | content: ""; |
| | | display: inline-block; |
| | | width: 4px; |
| | | height: 15px; |
| | | background-color: #2563eb; |
| | | border-radius: 2px; |
| | | margin-right: 8px; |
| | | } |
| | | |
| | | .trace-dialog .sub-section-title { |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | margin: 18px 0 10px; |
| | | color: #303133; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .sub-section-title::before { |
| | | .trace-dialog .sub-section-title::before { |
| | | content: ""; |
| | | display: inline-block; |
| | | width: 3px; |
| | | height: 12px; |
| | | background-color: #67c23a; |
| | | margin-right: 8px; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .unit { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-left: 4px; |
| | | } |
| | | |
| | | :deep(.el-descriptions) { |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | :deep(.el-descriptions__row:nth-child(odd)) { |
| | | background-color: #f9f9f9; |
| | | } |
| | | |
| | | :deep(.el-descriptions__label) { |
| | | font-weight: 500; |
| | | color: #606266; |
| | | background-color: #f5f7fa; |
| | | } |
| | | |
| | | :deep(.el-descriptions__content) { |
| | | color: #303133; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .progress-container { |
| | | display: flex; |
| | | gap: 24px; |
| | | } |
| | | |
| | | .progress-section { |
| | | margin-bottom: 24px; |
| | | background-color: #ffffff; |
| | | border-radius: 10px; |
| | | padding: 24px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08); |
| | | flex: 1; |
| | | transition: all 0.3s ease; |
| | | width: 100%; |
| | | } |
| | | |
| | | .progress-section:hover { |
| | | box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.12); |
| | | } |
| | | |
| | | .order-item { |
| | | margin-bottom: 20px; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | :deep(.el-table) { |
| | | border-radius: 8px; |
| | | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | :deep(.el-table th) { |
| | | background-color: #f5f7fa !important; |
| | | font-weight: 600; |
| | | color: #606266; |
| | | } |
| | | |
| | | :deep(.el-progress-bar__inner) { |
| | | border-radius: 10px; |
| | | } |
| | | |
| | | :deep(.el-tag) { |
| | | border-radius: 12px; |
| | | padding: 2px 10px; |
| | | } |
| | | |
| | | /* 弹窗样式 */ |
| | | .detail-container { |
| | | max-height: 600px; |
| | | overflow-y: auto; |
| | | padding: 0 16px; |
| | | } |
| | | |
| | | .process-item { |
| | | margin-bottom: 24px; |
| | | padding: 20px; |
| | | background-color: #ffffff; |
| | | border-radius: 8px; |
| | | border: 1px solid #ebeef5; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| | | } |
| | | |
| | | .process-header { |
| | | margin-bottom: 20px; |
| | | padding-bottom: 12px; |
| | | border-bottom: 1px solid #f0f2f5; |
| | | } |
| | | |
| | | .process-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | margin-bottom: 12px; |
| | | color: #1a1a1a; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .process-title::before { |
| | | content: ""; |
| | | display: inline-block; |
| | | width: 4px; |
| | | height: 16px; |
| | | background-color: #409eff; |
| | | margin-right: 8px; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .process-info { |
| | | display: flex; |
| | | gap: 20px; |
| | | font-size: 13px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .process-label { |
| | | padding: 4px 12px; |
| | | background-color: #ecf5ff; |
| | | border-radius: 4px; |
| | | color: #409eff; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .process-details { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .num1 { |
| | | color: #1107cc; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .num2 { |
| | | color: #0fcf25; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .num3 { |
| | | color: #d31818; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | .trace-dialog .dialog-footer { |
| | | text-align: center; |
| | | padding: 20px; |
| | | border-top: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .dialog-footer .el-button { |
| | | min-width: 100px; |
| | | padding: 8px 20px; |
| | | } |
| | | |
| | | /* 自定义对话框样式 */ |
| | | :deep(.custom-dialog) { |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | :deep(.custom-dialog .el-dialog__header) { |
| | | background-color: #f5f7fa; |
| | | padding: 20px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | :deep(.custom-dialog .el-dialog__title) { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1a1a1a; |
| | | } |
| | | |
| | | :deep(.custom-dialog .el-dialog__body) { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .unit-text { |
| | | .trace-dialog .unit-text { |
| | | margin-left: 5px; |
| | | color: #909399; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .param-detail-list { |
| | | padding: 10px; |
| | | .trace-dialog .param-detail-list { |
| | | padding: 4px; |
| | | } |
| | | |
| | | .trace-dialog .inspect-files { |
| | | margin-top: 6px; |
| | | } |
| | | |
| | | .trace-dialog .inspect-file-list { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .trace-dialog .inspect-file-item { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | padding: 4px 10px; |
| | | border-radius: 8px; |
| | | border: 1px solid #e4e7ed; |
| | | background: #f8fafc; |
| | | color: #2563eb; |
| | | font-size: 13px; |
| | | text-decoration: none; |
| | | } |
| | | |
| | | .trace-dialog .inspect-file-item:hover { |
| | | border-color: #bfdbfe; |
| | | background: #eff6ff; |
| | | } |
| | | |
| | | .trace-dialog .inspect-file-item.is-disabled { |
| | | color: #64748b; |
| | | cursor: default; |
| | | } |
| | | |
| | | .trace-dialog .inspect-file-item.is-disabled:hover { |
| | | border-color: #e4e7ed; |
| | | background: #f8fafc; |
| | | } |
| | | </style> |