Files
MrleeAdmin/backend/plugin/produce/resource/template/web/pages/query/index.vue
T
2026-08-31 19:17:33 +08:00

179 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>