179 lines
15 KiB
Vue
179 lines
15 KiB
Vue
<template>
|
||
<div class="produce-query-page">
|
||
<div class="page-shell">
|
||
<section class="page-hero">
|
||
<div>
|
||
<span class="eyebrow">生产追溯中心</span>
|
||
<h1>综合查询工作台</h1>
|
||
<p>按订单号查看生产、异常和维修全过程;也可以独立查询各类业务记录。</p>
|
||
</div>
|
||
<div class="hero-badge"><strong>{{ timeline.total }}</strong><span>订单时间线记录</span></div>
|
||
</section>
|
||
|
||
<section class="panel timeline-panel">
|
||
<div class="panel-heading"><div><h2>订单全流程时间线</h2><p>订单号查询会汇总扫码、整机、异常和维修记录,并按发生时间排序。</p></div></div>
|
||
<div class="search-row">
|
||
<el-input v-model="timelineOrderNo" size="large" clearable placeholder="请输入订单号" @keyup.enter="loadTimeline">
|
||
<template #prefix><el-icon><Search /></el-icon></template>
|
||
</el-input>
|
||
<el-button type="primary" size="large" :loading="timelineLoading" @click="loadTimeline">查询时间线</el-button>
|
||
</div>
|
||
<el-alert v-if="timelineMessage" :title="timelineMessage" :type="timeline.total ? 'success' : 'info'" :closable="false" class="timeline-alert" />
|
||
<el-empty v-if="timelineLoaded && !timelineLoading && !timeline.items.length" description="该订单暂无相关记录" :image-size="90" />
|
||
<div v-else-if="timeline.items.length" class="timeline-list">
|
||
<el-table :data="timeline.items" stripe class="timeline-table" empty-text="暂无时间线记录">
|
||
<el-table-column label="记录类型" width="112">
|
||
<template #default="scope"><el-tag :type="tagType(scope.row.record_type)" effect="light" round>{{ scope.row.record_type_name }}</el-tag></template>
|
||
</el-table-column>
|
||
<el-table-column label="订单 / 产品" min-width="220" show-overflow-tooltip>
|
||
<template #default="scope">
|
||
<div class="timeline-content-cell">
|
||
<span class="content-mark" aria-hidden="true" />
|
||
<div class="timeline-main-text">
|
||
<strong>{{ scope.row.order_no || timeline.order_no || '-' }}</strong>
|
||
<small v-if="scope.row.product_code">产品:{{ scope.row.product_code }}</small>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="工序节点" min-width="160" show-overflow-tooltip>
|
||
<template #default="scope">
|
||
<div class="timeline-node-cell">
|
||
{{ [scope.row.process_name, scope.row.step_name].filter(Boolean).join(' / ') || '—' }}
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态 / 结果" min-width="155" show-overflow-tooltip>
|
||
<template #default="scope">
|
||
<span class="timeline-status">{{ [scope.row.status_name, scope.row.result_name].filter(Boolean).join(' · ') || '已记录' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="quantity" label="数量" width="78" align="right">
|
||
<template #default="scope">{{ scope.row.quantity ?? '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="业务详情" min-width="280" show-overflow-tooltip>
|
||
<template #default="scope">
|
||
<div v-if="scope.row.record_type === 'exception'" class="timeline-detail-cell">
|
||
<span><b>异常原因</b>{{ scope.row.exception_reason || '未填写' }}</span>
|
||
<span><b>具体问题</b>{{ scope.row.exception_content || '未填写' }}</span>
|
||
</div>
|
||
<div v-else-if="scope.row.record_type === 'repair'" class="timeline-detail-cell">
|
||
<span><b>物料分类</b>{{ scope.row.material_category || '未填写' }}</span>
|
||
<span><b>故障描述</b>{{ scope.row.fault_description || '未填写' }}</span>
|
||
</div>
|
||
<span v-else class="muted-placeholder">—</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作人" min-width="130" show-overflow-tooltip>
|
||
<template #default="scope">
|
||
<div class="timeline-operator-cell">
|
||
<span>{{ scope.row.operator || '未记录' }}</span>
|
||
<small v-if="scope.row.user_code">工号:{{ scope.row.user_code }}</small>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="event_date" label="时间" width="170" />
|
||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||
</el-table>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel independent-panel">
|
||
<div class="panel-heading"><div><h2>独立记录查询</h2><p>切换记录类型,按条件分页查询。</p></div><el-button @click="resetIndependent">重置</el-button></div>
|
||
<el-tabs v-model="filters.record_type" class="query-tabs" @tab-change="handleRecordTypeChange">
|
||
<el-tab-pane label="扫码记录" name="scan" />
|
||
<el-tab-pane label="整机记录" name="product" />
|
||
<el-tab-pane label="异常记录" name="exception" />
|
||
<el-tab-pane label="维修记录" name="repair" />
|
||
</el-tabs>
|
||
<el-form :inline="true" class="filter-form" @submit.prevent="loadRecords">
|
||
<el-form-item label="关键词"><el-input v-model="filters.keyword" clearable :placeholder="keywordPlaceholder" /></el-form-item>
|
||
<el-form-item label="订单号"><el-input v-model="filters.order_no" clearable placeholder="精确订单号" /></el-form-item>
|
||
<el-form-item v-if="filters.record_type !== 'repair'" label="用户工号"><el-input v-model="filters.user_code" clearable placeholder="用户工号" /></el-form-item>
|
||
<el-form-item label="日期范围"><el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" /></el-form-item>
|
||
<el-form-item><el-button type="primary" :loading="recordLoading" @click="loadRecords">查询</el-button></el-form-item>
|
||
</el-form>
|
||
<el-table v-loading="recordLoading" :data="recordPage.items" stripe class="record-table" empty-text="暂无查询记录" @row-dblclick="showRecordDetail">
|
||
<el-table-column prop="record_type_name" label="记录类型" width="112"><template #default="scope"><el-tag :type="tagType(scope.row.record_type)" effect="light" round>{{ scope.row.record_type_name }}</el-tag></template></el-table-column>
|
||
<el-table-column prop="order_no" label="订单号" min-width="160" show-overflow-tooltip />
|
||
<el-table-column prop="product_code" label="产品编码" min-width="145" show-overflow-tooltip />
|
||
<el-table-column label="工序节点" min-width="160" show-overflow-tooltip><template #default="scope">{{ [scope.row.process_name, scope.row.step_name].filter(Boolean).join(' / ') || '—' }}</template></el-table-column>
|
||
<el-table-column label="状态 / 结果" min-width="145" show-overflow-tooltip><template #default="scope">{{ [scope.row.status_name, scope.row.result_name].filter(Boolean).join(' · ') || '—' }}</template></el-table-column>
|
||
<el-table-column label="操作人" min-width="125" show-overflow-tooltip><template #default="scope">{{ scope.row.operator || scope.row.user_code || '未记录' }}</template></el-table-column>
|
||
<el-table-column prop="event_date" label="时间" width="170" />
|
||
<el-table-column prop="remark" label="备注" min-width="190" show-overflow-tooltip />
|
||
</el-table>
|
||
<div class="pagination-row"><span>共 {{ recordPage.total }} 条</span><el-pagination v-model:current-page="recordPage.page" v-model:page-size="recordPage.limit" layout="prev, pager, next, sizes" :page-sizes="[20, 50, 100]" :total="recordPage.total" @current-change="loadRecords" @size-change="loadRecords" /></div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { Search } from '@element-plus/icons-vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { queryProduceRecords, queryProduceTimeline, type ProduceQueryRecord, type ProduceQueryRecordType } from '~/apps/produce/api/query'
|
||
|
||
const timelineOrderNo = ref('')
|
||
const timelineLoading = ref(false)
|
||
const timelineLoaded = ref(false)
|
||
const timelineMessage = ref('')
|
||
const timeline = reactive<{ order_no: string; total: number; items: ProduceQueryRecord[] }>({ order_no: '', total: 0, items: [] })
|
||
const filters = reactive<{ record_type: ProduceQueryRecordType; keyword: string; order_no: string; user_code: string; start_date?: string; end_date?: string }>({ record_type: 'scan', keyword: '', order_no: '', user_code: '' })
|
||
const dateRange = ref<string[]>([])
|
||
const recordLoading = ref(false)
|
||
const recordPage = reactive({ items: [] as ProduceQueryRecord[], total: 0, page: 1, limit: 20 })
|
||
|
||
const tagTypes: Record<ProduceQueryRecordType, 'primary' | 'success' | 'danger' | 'warning'> = { scan: 'primary', product: 'success', exception: 'danger', repair: 'warning' }
|
||
const tagType = (type: ProduceQueryRecordType) => tagTypes[type]
|
||
const keywordPlaceholder = computed(() => filters.record_type === 'repair' ? '维修单、产品、人员或备注' : '订单、产品、人员或备注')
|
||
const handleRecordTypeChange = () => { recordPage.page = 1; void loadRecords() }
|
||
const showRecordDetail = (row: ProduceQueryRecord) => { ElMessage.info(`${row.record_type_name}详情功能待接入`) }
|
||
const loadTimeline = async () => { const orderNo = timelineOrderNo.value.trim(); if (!orderNo) return ElMessage.warning('请输入订单号'); timelineLoading.value = true; timelineLoaded.value = false; timelineMessage.value = ''; try { const data = await queryProduceTimeline(orderNo); timeline.order_no = data?.order_no || orderNo; timeline.total = Number(data?.total || 0); timeline.items = Array.isArray(data?.items) ? data.items : []; timelineMessage.value = timeline.total ? `已找到 ${timeline.total} 条全流程记录` : '该订单暂无相关记录'; timelineLoaded.value = true } catch { timeline.order_no = orderNo; timeline.total = 0; timeline.items = []; timelineMessage.value = '订单查询失败,请检查订单号后重试'; timelineLoaded.value = true } finally { timelineLoading.value = false } }
|
||
const loadRecords = async () => { recordLoading.value = true; try { const data = await queryProduceRecords({ ...filters, ...(dateRange.value.length ? { start_date: dateRange.value[0], end_date: dateRange.value[1] } : {}), page: recordPage.page, limit: recordPage.limit }); recordPage.items = Array.isArray(data?.items) ? data.items : []; recordPage.total = Number(data?.total || 0) } catch { recordPage.items = []; recordPage.total = 0; ElMessage.error('记录查询失败,请稍后重试') } finally { recordLoading.value = false } }
|
||
const resetIndependent = () => { filters.record_type = 'scan'; filters.keyword = ''; filters.order_no = ''; filters.user_code = ''; dateRange.value = []; recordPage.page = 1; void loadRecords() }
|
||
onMounted(() => { void loadRecords() })
|
||
</script>
|
||
|
||
<style scoped>
|
||
.produce-query-page { min-height: calc(100vh - 60px); padding: 28px; color: #17233d; background: #f5f7fb; }
|
||
.page-shell { max-width: 1440px; margin: 0 auto; }
|
||
.page-hero { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 32px 36px; color: #fff; background: linear-gradient(135deg, #315efb, #6a67df); border-radius: 22px; box-shadow: 0 14px 32px rgba(49, 94, 251, .16); }
|
||
.eyebrow { font-size: 13px; opacity: .85; }
|
||
.page-hero h1 { margin: 8px 0 0; font-size: 30px; }
|
||
.page-hero p { margin: 8px 0 0; opacity: .86; }
|
||
.hero-badge { min-width: 150px; padding: 16px 20px; text-align: right; background: rgba(255, 255, 255, .16); border-radius: 14px; }
|
||
.hero-badge strong, .hero-badge span { display: block; }
|
||
.hero-badge strong { font-size: 28px; }
|
||
.hero-badge span { margin-top: 5px; font-size: 13px; opacity: .82; }
|
||
.panel { margin-top: 20px; padding: 26px; background: #fff; border: 1px solid #e7edf5; border-radius: 18px; box-shadow: 0 8px 24px rgba(36, 59, 92, .04); }
|
||
.panel-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
|
||
.panel-heading h2 { margin: 0; font-size: 18px; }
|
||
.panel-heading p { margin: 6px 0 0; color: #8a94a6; font-size: 13px; }
|
||
.search-row { display: flex; gap: 12px; }
|
||
.search-row .el-input { flex: 1; }
|
||
.timeline-alert { margin-top: 16px; }
|
||
.timeline-list { margin: 24px 0 0; overflow-x: auto; }
|
||
.timeline-table, .record-table { min-width: 1100px; overflow: hidden; border: 1px solid #e7edf5; border-radius: 12px; }
|
||
.timeline-table :deep(.el-table__header th), .record-table :deep(.el-table__header th) { height: 48px; color: #5d6b82; font-size: 13px; font-weight: 600; background: #f7f9fc; }
|
||
.timeline-table :deep(.el-table__cell), .record-table :deep(.el-table__cell) { padding: 12px 0; color: #34425b; font-size: 13px; }
|
||
.timeline-table :deep(.el-table__row:hover > td), .record-table :deep(.el-table__row:hover > td) { background: #f4f7ff !important; }
|
||
.timeline-content-cell, .timeline-node-cell { display: flex; align-items: center; min-width: 0; }
|
||
.timeline-content-cell { gap: 10px; }
|
||
.timeline-main-text, .timeline-node-cell, .timeline-operator-cell { min-width: 0; }
|
||
.timeline-content-cell strong, .timeline-node-cell strong { display: block; overflow: hidden; color: #24324b; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
|
||
.timeline-content-cell small, .timeline-node-cell small, .timeline-operator-cell small { display: block; margin-top: 4px; overflow: hidden; color: #8a94a6; font-size: 12px; line-height: 1.2; text-overflow: ellipsis; white-space: nowrap; }
|
||
.content-mark { width: 8px; height: 8px; flex: none; background: #315efb; border-radius: 50%; box-shadow: 0 0 0 4px #e5ebff; }
|
||
.timeline-status { color: #3157c8; font-weight: 500; }
|
||
.timeline-detail-cell { display: grid; gap: 5px; min-width: 0; line-height: 1.35; }
|
||
.timeline-detail-cell span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.timeline-detail-cell b { display: inline-block; min-width: 64px; margin-right: 6px; color: #8a94a6; font-size: 12px; font-weight: 500; }
|
||
.muted-placeholder { color: #b5bdca; }
|
||
.filter-form { display: flex; flex-wrap: wrap; margin-bottom: 4px; }
|
||
.record-table { margin-top: 14px; }
|
||
.pagination-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 18px; color: #8a94a6; font-size: 13px; }
|
||
.pagination-row .el-pagination { margin-left: auto; }
|
||
@media (max-width: 640px) { .produce-query-page { padding: 16px; } .page-hero { align-items: flex-start; flex-direction: column; padding: 24px; } .hero-badge { align-self: stretch; text-align: left; } .search-row { flex-direction: column; } .panel { padding: 18px; } .pagination-row { align-items: flex-start; flex-direction: column; } .pagination-row .el-pagination { margin-left: 0; } }
|
||
</style>
|