完善生产查询与登录权限配置

This commit is contained in:
2026-09-03 19:56:19 +08:00
parent 793f140a45
commit 02c8fb7021
438 changed files with 1595 additions and 573 deletions
@@ -152,6 +152,8 @@ export const crudSchema = (): CrudSchema => ({
{ field: "order_product", title: "订单号/商品编码" },
{ field: "exception_type_name", title: "异常分类" },
{ field: "exception_reason", title: "异常原因" },
{ field: "process_name", title: "维修工序" },
{ field: "step_name", title: "维修步骤" },
{ field: "part_category", title: "物料分类" },
{ field: "part_code", title: "物料编码" },
{ field: "part_name", title: "更换备件" },
@@ -221,6 +223,8 @@ export const crudSchema = (): CrudSchema => ({
},
{ prop: "exception_type_name", label: "异常分类", minWidth: 120, cellStyle: { whiteSpace: "nowrap" } },
{ prop: "exception_reason", label: "异常原因", minWidth: 180, cellStyle: { whiteSpace: "nowrap" } },
{ prop: "process_name", label: "维修工序", minWidth: 140, cellStyle: { whiteSpace: "nowrap" } },
{ prop: "step_name", label: "维修步骤", minWidth: 140, cellStyle: { whiteSpace: "nowrap" } },
{
prop: "part_category",
label: "物料分类",
@@ -380,6 +384,22 @@ export const crudSchema = (): CrudSchema => ({
ifDetail: true,
colSpan: 24,
},
{
component: "Input",
prop: "process_name",
label: "维修工序",
componentProps: { readonly: true },
ifDetail: true,
colSpan: 12,
},
{
component: "Input",
prop: "step_name",
label: "维修步骤",
componentProps: { readonly: true },
ifDetail: true,
colSpan: 12,
},
{
component: "Input",
prop: "fault_descriptions",
@@ -118,6 +118,8 @@
"title": "Frontend Settings",
"site_name": "Site Name",
"site_url": "Site URL",
"require_login": "Require login to access pages",
"require_login_options": { "enabled": "Yes", "disabled": "No" },
"logo": "Site Logo",
"seo_title": "SEO Title",
"seo_keywords": "SEO Keywords",
@@ -261,4 +263,4 @@
"submit": "Save"
}
}
}
}
@@ -118,6 +118,8 @@
"title": "前台设置",
"site_name": "站点名称",
"site_url": "站点URL",
"require_login": "访问页面需要登录",
"require_login_options": { "enabled": "是", "disabled": "否" },
"logo": "站点Logo",
"seo_title": "SEO标题",
"seo_keywords": "SEO关键词",
@@ -261,4 +263,4 @@
"submit": "保存"
}
}
}
}
@@ -99,6 +99,14 @@
key: '5',
name: $t('setting.config.common.title'),
loadKey: 0
},
{
component: markRaw(
defineAsyncComponent(() => import('@/views/setting/config/modules/auth-token.vue'))
),
key: '10',
name: '登录Token',
loadKey: 0
}
])
</script>
@@ -0,0 +1,51 @@
<template>
<div class="auth-token-config">
<BasicForm />
<el-button class="config-submit" type="primary" @click="onSubmit">保存</el-button>
</div>
</template>
<script setup lang="tsx">
import { ElMessage } from 'element-plus'
import { useForm } from '@/components/form'
import ConfigService from '@/api/setting/config/config'
const schema = [
{ prop: 'admin_access_ttl', label: 'Admin 端 Access Token 有效期(秒)', component: 'Input', componentProps: { type: 'number', min: 60, style: { width: '320px' } } },
{ prop: 'web_access_ttl', label: 'Web 端 Access Token 有效期(秒)', component: 'Input', componentProps: { type: 'number', min: 60, style: { width: '320px' } } },
{ prop: 'app_access_ttl', label: 'APP 端 Access Token 有效期(秒)', component: 'Input', componentProps: { type: 'number', min: 60, style: { width: '320px' } } },
{ prop: 'admin_refresh_ttl', label: 'Admin 端 Refresh Token 有效期(秒)', component: 'Input', componentProps: { type: 'number', min: 300, style: { width: '320px' } } },
{ prop: 'web_refresh_ttl', label: 'Web 端 Refresh Token 有效期(秒)', component: 'Input', componentProps: { type: 'number', min: 300, style: { width: '320px' } } },
{ prop: 'app_refresh_ttl', label: 'APP 端 Refresh Token 有效期(秒)', component: 'Input', componentProps: { type: 'number', min: 300, style: { width: '320px' } } }
]
const [BasicForm, formApi] = useForm({ labelPosition: 'right', labelWidth: '260px', schema: schema as any })
const retrieveData = async () => {
const data = await ConfigService.getByCode('auth_token_setting') as any
await formApi.setValues({
admin_access_ttl: data?.admin_access_ttl ?? 3600,
web_access_ttl: data?.web_access_ttl ?? 3600,
app_access_ttl: data?.app_access_ttl ?? 3600,
admin_refresh_ttl: data?.admin_refresh_ttl ?? data?.refresh_ttl ?? 604800,
web_refresh_ttl: data?.web_refresh_ttl ?? data?.refresh_ttl ?? 604800,
app_refresh_ttl: data?.app_refresh_ttl ?? data?.refresh_ttl ?? 604800
})
}
const onSubmit = async () => {
const values = await formApi.getValues()
const content = Object.fromEntries(Object.entries(values).map(([key, value]) => {
const minimum = key.includes('refresh') ? 300 : 60
return [key, Math.max(minimum, Number(value) || 0)]
}))
await ConfigService.update('auth_token_setting', { group_code: 'system', code: 'auth_token_setting', name: '登录Token有效期', content })
ElMessage.success('保存成功')
}
onMounted(retrieveData)
</script>
<style scoped>
.config-submit { margin-left: 260px; }
</style>
@@ -37,6 +37,19 @@
},
rules: []
},
{
prop: 'require_login',
label: $t('setting.config.web_site.require_login'),
component: 'RadioGroup',
defaultValue: 1,
componentProps: {
options: [
{ label: $t('setting.config.web_site.require_login_options.enabled'), value: 1 },
{ label: $t('setting.config.web_site.require_login_options.disabled'), value: 0 }
]
},
rules: []
},
{
prop: 'logo',
label: $t('setting.config.web_site.logo'),
@@ -169,7 +182,10 @@
const retrieveData = async () => {
const record = (await ConfigService.getByCode('web_site_setting')) as any
await formApi.setValues({ ...convertStringNumbers(record) })
// 兼容旧配置数据:未保存该字段时默认要求登录
const values = convertStringNumbers(record || {})
values.require_login = values.require_login ?? 1
await formApi.setValues(values)
}
onMounted(async () => {
+87 -1
View File
@@ -1,13 +1,81 @@
<template>
<div class="sys-post-page art-full-height">
<BasicCrud />
<el-dialog v-model="previewVisible" title="浏览附件" width="min(900px, 92vw)" destroy-on-close>
<div class="file-preview-container">
<img v-if="previewType === 'image'" :src="previewUrl" alt="附件预览" class="file-preview-media" />
<video v-else-if="previewType === 'video'" :src="previewUrl" controls class="file-preview-media" />
<audio v-else-if="previewType === 'audio'" :src="previewUrl" controls class="file-preview-audio" />
<iframe v-else-if="previewType === 'pdf'" :src="previewUrl" title="PDF预览" class="file-preview-frame" />
<div v-else class="file-preview-empty">该文件类型不支持在线预览,请点击下载。</div>
</div>
<template #footer>
<el-button @click="previewVisible = false">关闭</el-button>
<el-button type="primary" @click="openPreviewInNewTab">新窗口打开</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="tsx">
import { ref } from 'vue'
import { useCrud } from '@/components/crud'
import { crudSchema } from './schemas'
import { $t } from '@/locales'
import { FilesService } from '@/api/tools/files'
import { ElMessage } from 'element-plus'
const onFilePreview = (event: Event) => {
const record = (event as CustomEvent).detail
if (record) handlePreview(record)
}
const previewVisible = ref(false)
const previewUrl = ref('')
const previewType = ref<'image' | 'video' | 'audio' | 'pdf' | 'other'>('other')
let previewObjectUrl = ''
const getFileUrl = (record: any) => record.url || record.path || record.base_path || ''
const getPreviewType = (record: any) => {
const ext = String(record.ext || record.extension || '').toLowerCase().replace('.', '')
const mime = String(record.content_type || record.mime_type || '').toLowerCase()
if (mime.startsWith('image/') || ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(ext)) return 'image'
if (mime.startsWith('video/') || ['mp4', 'webm', 'ogg', 'mov'].includes(ext)) return 'video'
if (mime.startsWith('audio/') || ['mp3', 'wav', 'm4a', 'aac', 'flac'].includes(ext)) return 'audio'
if (mime === 'application/pdf' || ext === 'pdf') return 'pdf'
return 'other'
}
const handlePreview = async (record: any) => {
const url = getFileUrl(record)
if (!url) {
ElMessage.warning('附件地址不存在')
return
}
previewType.value = getPreviewType(record)
if (previewObjectUrl) URL.revokeObjectURL(previewObjectUrl)
if (previewType.value === 'other') {
previewUrl.value = url
previewVisible.value = true
return
}
try {
const blob = await FilesService.downloadById(record.id)
previewObjectUrl = URL.createObjectURL(blob instanceof Blob ? blob : new Blob([blob]))
previewUrl.value = previewObjectUrl
} catch (error) {
ElMessage.error('附件加载失败,请稍后重试')
return
}
previewVisible.value = true
}
const openPreviewInNewTab = () => {
if (previewUrl.value) window.open(previewUrl.value, '_blank', 'noopener,noreferrer')
}
onMounted(() => window.addEventListener('file-preview', onFilePreview))
onBeforeUnmount(() => window.removeEventListener('file-preview', onFilePreview))
const [BasicCrud, crudApi] = useCrud({
...crudSchema(),
@@ -32,6 +100,15 @@
dropDownToolbarActions: [],
// 表格行操作
tableActions: [
{
label: '浏览',
type: 'primary',
sort: 1010,
link: true,
icon: 'ant-design:eye-outlined',
auth: 'upload:files:read',
onClick: (_e: Event, record: any) => handlePreview(record)
},
{
label: $t('tools.files.list.table.columns.actions.download'),
type: 'primary',
@@ -88,4 +165,13 @@
})
}
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.file-preview-container { min-height: 240px; display: flex; align-items: center; justify-content: center; background: #f5f7fa; }
.file-preview-media { max-width: 100%; max-height: 65vh; object-fit: contain; }
.file-preview-audio { width: min(560px, 100%); }
.file-preview-frame { width: 100%; height: 65vh; border: 0; }
.file-preview-empty { color: #909399; padding: 60px 20px; }
:global(.file-list-thumbnail) { display: block; width: 50px; height: 50px; max-width: 50px; max-height: 50px; margin: 0 auto; object-fit: cover; border-radius: 6px; border: 1px solid #ebeef5; cursor: pointer; }
:global(.el-table--small .file-list-thumbnail) { width: 38px; height: 38px; max-width: 38px; max-height: 38px; }
:global(.el-table--large .file-list-thumbnail) { width: 58px; height: 58px; max-width: 58px; max-height: 58px; }
</style>
@@ -2,6 +2,39 @@ import { CrudSchema } from '@/components/crud'
import { $t } from '@/locales'
import { } from '@/api/setting/gateway'
import { FilesService } from '@/api/tools/files'
import { h } from 'vue'
import { Icon } from '@/components/icon'
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico']
const getFileExtension = (row: any) => String(row?.ext || row?.extension || '').toLowerCase().replace('.', '')
const getFileIcon = (ext: string) => {
if (['pdf'].includes(ext)) return { name: 'ant-design:file-pdf-outlined', color: '#f56c6c' }
if (['doc', 'docx', 'odt', 'txt', 'md'].includes(ext)) return { name: 'ant-design:file-word-outlined', color: '#409eff' }
if (['xls', 'xlsx', 'csv', 'ods'].includes(ext)) return { name: 'ant-design:file-excel-outlined', color: '#67c23a' }
if (['ppt', 'pptx', 'odp'].includes(ext)) return { name: 'ant-design:file-ppt-outlined', color: '#e6a23c' }
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) return { name: 'ant-design:file-zip-outlined', color: '#909399' }
if (['mp4', 'webm', 'mov', 'avi'].includes(ext)) return { name: 'ant-design:video-camera-outlined', color: '#8e44ad' }
if (['mp3', 'wav', 'm4a', 'aac'].includes(ext)) return { name: 'ant-design:audio-outlined', color: '#13c2c2' }
return { name: 'ant-design:file-outlined', color: '#909399' }
}
const renderFilePreview = (row: any) => {
const ext = getFileExtension(row)
const url = row?.url || row?.path || row?.base_path || ''
if (imageExtensions.includes(ext) && url) {
return h('img', {
src: url,
alt: row?.original_filename || row?.filename || '图片',
class: 'file-list-thumbnail',
title: '点击预览图片',
onClick: () => window.dispatchEvent(new CustomEvent('file-preview', { detail: row }))
})
}
const icon = getFileIcon(ext)
return h(Icon, { icon: icon.name, width: 32, height: 32, color: icon.color })
}
// crud配置
export const crudSchema = (): CrudSchema => {
@@ -42,6 +75,13 @@ export const crudSchema = (): CrudSchema => {
{
type: 'selection'
},
{
prop: 'file_preview',
label: '预览',
width: 80,
align: 'center',
formatter: (row: any) => renderFilePreview(row)
},
{
prop: 'filename',
label: $t('tools.files.list.table.columns.filename'),
@@ -127,4 +167,4 @@ export const crudSchema = (): CrudSchema => {
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
import request from '@/utils/request.js'
/** 查询生产记录(扫码、整机、异常、维修)。 */
export function queryProduceRecords(params = {}) {
return request.get('/produce/query/records', params, { showError: false })
}
/** 查询指定订单的生产时间线。 */
export function queryProduceTimeline(orderNo) {
return request.get('/produce/query/timeline', {
order_no: String(orderNo || '').trim()
}, { showError: false })
}
@@ -6,7 +6,7 @@ const config = {
tokenKey: 'scanapp_token',
tokenHeaderKey: 'Authorization',
channelHeaderKey: 'channel',
channel: 'scanapp'
channel: 'app'
}
export default config
@@ -3,13 +3,13 @@
<view class="query-header">
<view>
<text class="query-title">记录查询</text>
<text class="query-subtitle">快速定位扫码、批量与异常记录</text>
<text class="query-subtitle">快速定位扫码、整机、异常与维修记录</text>
</view>
<u-icon name="search" color="#2f6cf6" size="34" />
</view>
<view class="query-tabs">
<view v-for="item in queryTypes" :key="item.value" class="query-tab" :class="{ 'is-active': activeQueryType === item.value }" @tap="activeQueryType = item.value">
<view v-for="item in queryTypes" :key="item.value" class="query-tab" :class="{ 'is-active': activeQueryType === item.value }" @tap="handleRecordTypeChange(item.value)">
<u-icon :name="item.icon" :color="activeQueryType === item.value ? '#ffffff' : '#9aa4b4'" size="28" />
<text>{{ item.label }}</text>
</view>
@@ -35,17 +35,20 @@
</view>
<view v-if="showAdvanced" class="advanced-panel">
<view class="filter-label">工序</view>
<view class="chip-row compact-chip-row">
<view v-for="item in processOptions" :key="item.value" class="chip" :class="{ 'is-active': activeProcess === item.value }" @tap="activeProcess = item.value">
{{ item.label }}
</view>
<view class="filter-select-row" @tap="showDatePicker = true">
<text class="filter-input-label">时间区间</text>
<text class="filter-select-value">{{ dateRangeLabel }}</text>
<u-icon name="arrow-right" color="#a0a7b5" size="24" />
</view>
<view class="filter-label status-filter-label">状态</view>
<view class="chip-row compact-chip-row">
<view v-for="item in statusOptions" :key="item.value" class="chip" :class="{ 'is-active': activeStatus === item.value }" @tap="activeStatus = item.value">
{{ item.label }}
</view>
<view class="filter-select-row" @tap="chooseProcess">
<text class="filter-input-label">工序</text>
<text class="filter-select-value">{{ processLabel }}</text>
<u-icon name="arrow-right" color="#a0a7b5" size="24" />
</view>
<view class="filter-select-row" :class="{ 'is-disabled': !activeProcess }" @tap="chooseStep">
<text class="filter-input-label">步骤</text>
<text class="filter-select-value">{{ stepLabel }}</text>
<u-icon name="arrow-right" color="#a0a7b5" size="24" />
</view>
<view class="filter-actions">
<text @tap="resetFilter">重置条件</text>
@@ -54,12 +57,13 @@
</view>
<view class="result-header">
<text>找到 {{ filteredRecords.length }} 条结果</text>
<text>{{ hasSearched ? `找到 ${recordTotal} 条结果` : '请输入条件后点击查询' }}</text>
<text>{{ currentTypeLabel }}</text>
</view>
<view v-if="hasSearched && recordLoading" class="query-loading">正在查询,请稍候…</view>
<view v-if="filteredRecords.length" class="record-list">
<view v-for="item in filteredRecords" :key="item.id" class="record-card compact-record" @tap="showRecordDetail(item)">
<view v-if="hasSearched && filteredRecords.length && !recordLoading" class="record-list">
<view v-for="item in filteredRecords" :key="item.id" class="record-card compact-record" @tap="showRecordDetail(item)" @click="showRecordDetail(item)">
<view class="record-top">
<view class="record-number">
<u-icon name="list" color="#44c653" size="28" />
@@ -69,7 +73,7 @@
</view>
<view class="record-detail-grid">
<view class="record-detail"><text>岗位:</text><text>{{ item.position }}</text></view>
<view class="record-detail"><text>工序:</text><text>{{ item.process }}</text></view>
<view class="record-detail"><text>步骤:</text><text>{{ item.process }} / {{ item.step }}</text></view>
<view class="record-detail"><text>状态:</text><text class="record-status" :class="item.statusClass">{{ item.statusLabel }}</text></view>
<view class="record-detail"><text>操作员:</text><text>{{ item.operator }}</text></view>
</view>
@@ -79,7 +83,7 @@
</view>
</view>
</view>
<u-empty v-else mode="data" text="暂无查询结果" />
<u-empty v-else-if="hasSearched && !recordLoading" mode="data" text="暂无查询结果" />
<view v-if="detailVisible && selectedRecord" class="detail-mask" @tap="closeRecordDetail">
<view class="detail-sheet" @tap.stop>
@@ -102,6 +106,7 @@
<view class="sheet-remark"><text>备注</text><text>{{ selectedRecord.remark }}</text></view>
</view>
</view>
<u-calendar v-model="showDatePicker" mode="range" @change="handleDateChange" />
</view>
</template>
@@ -112,132 +117,65 @@ import { openCodeScan } from '@/uni_modules/mas-code-scan'
// #endif
import { getScanRegionSetting } from '@/utils/scan-settings.js'
import { queryProduceRecords } from '@/api/produce/query.js'
import { getProcessOptions, getStepOptions } from '@/api/produce/data-entry.js'
export default {
data() {
return {
keyword: '',
orderNo: '',
userCode: '',
startDate: '',
endDate: '',
showDatePicker: false,
pdaMode: false,
activeQueryType: 'all',
showAdvanced: false,
isScanning: false,
activeProcess: 'all',
activeStatus: 'all',
hasSearched: false,
recordLoading: false,
recordTotal: 0,
recordPage: 1,
recordLimit: 20,
activeProcess: '',
activeStep: '',
detailVisible: false,
selectedRecord: null,
queryTypes: [
{ label: '全部', value: 'all', icon: 'search' },
{ label: '扫码记录', value: 'scan', icon: 'scan' },
{ label: '批量记录', value: 'batch', icon: 'grid-fill' },
{ label: '异常记录', value: 'abnormal', icon: 'warning-fill' }
{ label: '订单', value: 'scan', icon: 'scan' },
{ label: '整机', value: 'product', icon: 'grid-fill' },
{ label: '异常', value: 'exception', icon: 'warning-fill' },
{ label: '维修', value: 'repair', icon: 'wrench' }
],
processOptions: [
{ label: '全部', value: 'all' },
{ label: '总装', value: '总装' },
{ label: '质检', value: '质检' },
{ label: '包装', value: '包装' },
{ label: '组装', value: '组装' }
],
statusOptions: [
{ label: '全部', value: 'all' },
{ label: '成功', value: 'success' },
{ label: '失败', value: 'failed' }
],
records: [
{
id: 1,
queryType: 'scan',
typeLabel: '扫码记录',
mainNo: 'SO20260808001',
position: 'L1C-H',
process: '总装',
step: '开始',
operator: 'U001',
status: 'success',
statusLabel: '成功',
statusClass: 'is-success',
scanTime: '2026-08-08 18:42:13',
remark: '录入正常',
modeLabel: '订单模式',
productCode: '',
quantity: ''
},
{
id: 2,
queryType: 'scan',
typeLabel: '扫码记录',
mainNo: 'MO20260808004',
position: 'L1W-A',
process: '质检',
step: '完成',
operator: 'U003',
status: 'success',
statusLabel: '成功',
statusClass: 'is-success',
scanTime: '2026-08-08 18:31:05',
remark: '整机数据已提交',
modeLabel: '整机模式',
productCode: 'P10086',
quantity: '2'
},
{
id: 3,
queryType: 'abnormal',
typeLabel: '异常记录',
mainNo: 'SO20260808005',
position: 'L1Z-K',
process: '包装',
step: '复核',
operator: 'U002',
status: 'failed',
statusLabel: '失败',
statusClass: 'is-failed',
scanTime: '2026-08-08 18:12:44',
remark: '订单号格式不符合规则',
modeLabel: '订单模式',
productCode: '',
quantity: ''
},
{
id: 4,
queryType: 'batch',
typeLabel: '批量记录',
mainNo: 'MO20260808006',
position: 'L1P-D',
process: '总装',
step: '开始',
operator: 'U004',
status: 'success',
statusLabel: '成功',
statusClass: 'is-success',
scanTime: '2026-08-08 17:58:07',
remark: '录入正常',
modeLabel: '整机模式',
productCode: 'P10092',
quantity: '1'
}
]
processOptions: [],
stepOptions: [],
records: []
}
},
computed: {
filteredRecords() {
const keyword = this.keyword.trim().toLowerCase()
return this.records.filter((item) => {
const matchKeyword = !keyword
|| String(item.mainNo).toLowerCase().includes(keyword)
|| String(item.position || '').toLowerCase().includes(keyword)
|| String(item.productCode || '').toLowerCase().includes(keyword)
|| String(item.operator).toLowerCase().includes(keyword)
const matchType = this.activeQueryType === 'all' || item.queryType === this.activeQueryType
const matchProcess = this.activeProcess === 'all' || item.process === this.activeProcess
const matchStatus = this.activeStatus === 'all' || item.status === this.activeStatus
return matchKeyword && matchType && matchProcess && matchStatus
const matchProcess = !this.activeProcess || item.processId === String(this.activeProcess) || item.process === this.processLabel
const matchStep = !this.activeStep || item.stepId === String(this.activeStep) || item.step === this.stepLabel
return matchType && matchProcess && matchStep
})
},
currentTypeLabel() {
const current = this.queryTypes.find((item) => item.value === this.activeQueryType)
return current ? current.label : '全部类型'
},
processLabel() {
return this.processOptions.find((item) => String(item.value) === String(this.activeProcess))?.label || '全部工序'
},
stepLabel() {
return this.stepOptions.find((item) => String(item.value) === String(this.activeStep))?.label || '全部步骤'
},
dateRangeLabel() {
return this.startDate && this.endDate ? `${this.startDate} 至 ${this.endDate}` : '不限时间'
},
successCount() {
return this.filteredRecords.filter((item) => item.status === 'success').length
},
@@ -248,19 +186,147 @@ export default {
watch: {
filteredRecords: {
immediate: true,
handler(list) {
if (!list.length) {
this.selectedRecord = null
this.detailVisible = false
return
}
if (!this.selectedRecord || !list.some((item) => item.id === this.selectedRecord.id)) {
this.selectedRecord = list[0]
}
handler(list) {
if (!list.length) {
this.selectedRecord = null
this.detailVisible = false
return
}
if (!this.selectedRecord || !list.some((item) => item.id === this.selectedRecord.id)) {
this.detailVisible = false
this.selectedRecord = list[0]
}
}
}
},
methods: {
isLoggedIn() {
return Boolean(uni.getStorageSync('scanapp_token'))
},
requireLogin() {
if (this.isLoggedIn()) return true
uni.showToast({ title: '请先登录后再查询', icon: 'none' })
return false
},
mapRecord(item) {
const type = item.record_type || 'scan'
const resultName = String(item.result_name || '').trim()
const statusName = String(item.status_name || '').trim()
const status = resultName === '成功' ? 'success' : resultName === '失败' ? 'failed' : this.mapStatus(statusName)
return {
id: `${type}-${item.id}`,
queryType: type,
typeLabel: item.record_type_name || '记录',
mainNo: item.order_no || item.repair_no || '-',
position: item.position || '-',
process: item.process_name || '-',
step: item.step_name || '-',
processId: item.process_id || '',
stepId: item.step_id || '',
operator: item.operator || item.user_code || '未记录',
userCode: item.user_code || '',
status,
statusLabel: resultName || statusName || '已记录',
statusClass: status === 'failed' ? 'is-failed' : status === 'success' ? 'is-success' : '',
scanTime: item.event_date || this.formatEventDate(item.event_at),
remark: item.remark || '',
modeLabel: item.record_type_name || '记录',
productCode: item.product_code || '',
quantity: item.quantity ?? '',
exceptionReason: item.exception_reason || '',
exceptionContent: item.exception_content || '',
materialCategory: item.material_category || '',
faultDescription: item.fault_description || ''
}
},
formatEventDate(timestamp) {
const value = Number(timestamp || 0)
if (!value) return '-'
const date = new Date(value * 1000)
if (Number.isNaN(date.getTime())) return '-'
const pad = (part) => String(part).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
},
mapStatus(statusName) {
const statusMap = { 待处理: 'pending', 处理中: 'processing', 已完成: 'done', 已维修: 'repaired' }
return statusMap[statusName] || statusName || 'recorded'
},
async loadRecords() {
if (this.recordLoading) return
this.detailVisible = false
this.selectedRecord = null
this.recordLoading = true
const types = this.activeQueryType === 'all' ? ['scan', 'product', 'exception', 'repair'] : [this.activeQueryType]
const params = {
keyword: this.keyword.trim(),
order_no: this.orderNo.trim(),
user_code: this.userCode.trim(),
start_date: this.startDate,
end_date: this.endDate,
process_id: this.activeProcess,
step_id: this.activeStep,
page: this.recordPage,
limit: this.recordLimit
}
try {
const responses = await Promise.all(types.map((recordType) => queryProduceRecords({ ...params, record_type: recordType })))
const items = responses.flatMap((data) => Array.isArray(data?.items) ? data.items.map((item) => this.mapRecord(item)) : [])
items.sort((a, b) => String(b.scanTime).localeCompare(String(a.scanTime)))
this.records = items
this.recordTotal = responses.reduce((total, data) => total + Number(data?.total || 0), 0)
} catch (error) {
this.records = []
this.recordTotal = 0
uni.showToast({ title: '查询失败,请稍后重试', icon: 'none' })
} finally {
this.recordLoading = false
}
},
async loadProcessOptions() {
try {
const options = await getProcessOptions()
this.processOptions = Array.isArray(options) ? options : []
} catch (error) {
this.processOptions = []
}
},
chooseProcess() {
const labels = ['全部工序', ...this.processOptions.map((item) => item.label)]
uni.showActionSheet({ itemList: labels, success: async (result) => {
this.activeProcess = result.tapIndex === 0 ? '' : this.processOptions[result.tapIndex - 1]?.value || ''
this.activeStep = ''
this.stepOptions = []
if (this.activeProcess) {
try {
const options = await getStepOptions(this.activeProcess)
this.stepOptions = Array.isArray(options) ? options : []
} catch (error) {
this.stepOptions = []
}
}
} })
},
chooseStep() {
if (!this.activeProcess) {
uni.showToast({ title: '请先选择工序', icon: 'none' })
return
}
const labels = ['全部步骤', ...this.stepOptions.map((item) => item.label)]
uni.showActionSheet({ itemList: labels, success: (result) => {
this.activeStep = result.tapIndex === 0 ? '' : this.stepOptions[result.tapIndex - 1]?.value || ''
} })
},
handleDateChange(result) {
this.startDate = result.startDate || ''
this.endDate = result.endDate || ''
this.showDatePicker = false
},
handleRecordTypeChange(type) {
if (this.activeQueryType === type) return
this.activeQueryType = type
this.recordPage = 1
if (this.hasSearched) this.loadRecords()
},
getCurrentUserCode() {
const user = uni.getStorageSync('scanapp_user') || {}
return user.username || user.nickname || 'U001'
@@ -282,23 +348,36 @@ export default {
// #endif
},
handleSearch() {
if (!this.filteredRecords.length) {
const hasInput = this.keyword.trim() || this.orderNo.trim() || this.userCode.trim()
if (!hasInput) {
this.hasSearched = false
this.records = []
this.recordTotal = 0
this.selectedRecord = null
this.detailVisible = false
uni.showToast({ title: '请输入查询内容', icon: 'none' })
return
}
if (!this.selectedRecord || !this.filteredRecords.some((item) => item.id === this.selectedRecord.id)) {
this.selectedRecord = this.filteredRecords[0]
}
this.recordPage = 1
this.hasSearched = true
this.loadRecords()
},
resetFilter() {
this.keyword = ''
this.orderNo = ''
this.userCode = ''
this.startDate = ''
this.endDate = ''
this.activeQueryType = 'all'
this.activeProcess = 'all'
this.activeStatus = 'all'
this.activeProcess = ''
this.activeStep = ''
this.stepOptions = []
this.showAdvanced = false
this.detailVisible = false
this.selectedRecord = this.records[0] || null
this.recordPage = 1
this.hasSearched = false
this.records = []
this.recordTotal = 0
},
scanKeyword() {
if (this.isScanning) return
@@ -334,6 +413,7 @@ export default {
// #endif
},
showRecordDetail(item) {
if (!item) return
this.selectedRecord = item
this.detailVisible = true
},
@@ -345,11 +425,17 @@ export default {
}
},
onLoad() {
if (!this.requireLogin()) return
this.loadPdaMode()
this.selectedRecord = this.records[0] || null
this.loadProcessOptions()
},
onShow() {
if (!this.isLoggedIn()) return
this.loadPdaMode()
},
onHide() {
this.detailVisible = false
this.selectedRecord = null
}
}
</script>
@@ -651,7 +737,7 @@ export default {
.query-tabs {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(5, 1fr);
gap: 10rpx;
margin-top: 8rpx;
}
@@ -772,6 +858,53 @@ export default {
box-shadow: 0 8rpx 20rpx rgba(23, 35, 61, 0.04);
}
.filter-input-row {
display: flex;
align-items: center;
min-height: 64rpx;
margin-bottom: 12rpx;
padding: 0 14rpx;
border: 1rpx solid #e3e7ef;
border-radius: 12rpx;
background: #f6f8fb;
box-sizing: border-box;
}
.filter-input-label {
width: 112rpx;
flex: none;
color: #5f6b7a;
font-size: 22rpx;
}
.filter-input-row :deep(.u-input) {
flex: 1;
min-width: 0;
}
.filter-select-row {
display: flex;
align-items: center;
min-height: 64rpx;
margin-bottom: 12rpx;
padding: 0 14rpx;
border: 1rpx solid #e3e7ef;
border-radius: 12rpx;
background: #f6f8fb;
box-sizing: border-box;
}
.filter-select-row.is-disabled {
opacity: 0.55;
}
.filter-select-value {
flex: 1;
min-width: 0;
color: #2b313b;
font-size: 22rpx;
}
.status-filter-label {
margin-top: 10rpx;
}
@@ -819,6 +952,13 @@ export default {
line-height: 1.35;
}
.query-loading {
padding: 28rpx 0;
color: #7d8798;
font-size: 20rpx;
text-align: center;
}
.record-list {
margin-top: 2rpx;
}
@@ -909,10 +1049,16 @@ export default {
.detail-mask {
position: fixed;
inset: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
z-index: 999;
display: flex;
align-items: flex-end;
flex-direction: column;
align-items: stretch;
justify-content: flex-end;
background: rgba(0, 0, 0, 0.32);
}
@@ -1018,6 +1164,8 @@ export default {
.theme-dark .query-tab:not(.is-active),
.theme-dark .query-input-wrap,
.theme-dark .filter-input-row,
.theme-dark .filter-select-row,
.theme-dark .chip,
.theme-dark .record-info,
.theme-dark .sheet-detail-item,
@@ -1028,6 +1176,7 @@ export default {
.theme-dark .query-title,
.theme-dark .advanced-title,
.theme-dark .filter-select-value,
.theme-dark .record-main,
.theme-dark .record-detail text:last-child,
.theme-dark .detail-sheet-header,
@@ -1055,4 +1204,178 @@ export default {
.theme-dark :deep(.u-input__input) {
color: #f2f5fa !important;
}
/* 查询页统一视觉层级:保持移动端紧凑、清晰的生产工具风格。 */
.query-page.query-layout {
padding: 24rpx 24rpx 56rpx;
background: linear-gradient(180deg, #edf5ff 0%, #f5f7fb 30%, #f5f7fb 100%);
}
.query-header {
padding: 24rpx 22rpx;
border: 1rpx solid #e5ebf4;
border-radius: 22rpx;
background: rgba(255, 255, 255, 0.94);
box-shadow: 0 12rpx 30rpx rgba(39, 74, 125, 0.08);
}
.query-title {
letter-spacing: 1rpx;
}
.query-tabs {
display: flex;
gap: 10rpx;
margin-top: 18rpx;
padding: 8rpx;
border: 1rpx solid #e5ebf4;
border-radius: 18rpx;
background: #ffffff;
box-shadow: 0 8rpx 22rpx rgba(39, 74, 125, 0.05);
overflow-x: auto;
}
.query-tab {
min-width: 120rpx;
flex: 1;
flex-shrink: 0;
border-radius: 12rpx;
transition: background 0.2s ease, color 0.2s ease;
}
.query-search-row {
margin-top: 18rpx;
padding: 10rpx;
border-radius: 18rpx;
box-shadow: 0 10rpx 26rpx rgba(39, 74, 125, 0.07);
}
.query-input-wrap {
background: #f7f9fc;
}
.camera-button,
.query-button {
box-shadow: 0 8rpx 18rpx rgba(47, 108, 246, 0.2);
}
.advanced-trigger,
.advanced-panel {
box-shadow: 0 8rpx 22rpx rgba(39, 74, 125, 0.05);
}
.advanced-trigger {
margin-top: 16rpx;
padding: 20rpx 18rpx;
}
.advanced-panel {
padding: 20rpx 18rpx 18rpx;
}
.filter-select-row {
margin-bottom: 14rpx;
background: #f8faff;
}
.filter-input-label {
font-weight: 600;
}
.result-header {
margin-top: 24rpx;
padding: 0 8rpx 10rpx;
color: #738199;
}
.record-card.compact-record {
margin-top: 12rpx;
padding: 22rpx 20rpx 18rpx;
border-radius: 18rpx;
box-shadow: 0 10rpx 24rpx rgba(39, 74, 125, 0.06);
}
.record-detail-grid {
gap: 10rpx 16rpx;
margin-top: 18rpx;
padding-top: 16rpx;
}
.record-detail text:last-child {
min-width: 0;
word-break: break-all;
}
.record-footer {
margin-top: 16rpx;
}
.theme-dark.query-page.query-layout {
background: #151923;
}
.theme-dark .query-header,
.theme-dark .query-tabs {
border-color: #364152;
background: #222936;
}
.theme-dark .query-search-row,
.theme-dark .advanced-trigger,
.theme-dark .advanced-panel,
.theme-dark .record-card.compact-record {
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.16);
}
/* 防止记录类型标签换行,并让输入文字在控件内垂直居中。 */
.query-tab {
min-width: 0;
padding: 0 4rpx;
flex: 1 1 0;
margin-right: 10rpx;
white-space: nowrap;
}
.query-tabs {
gap: 0;
}
.query-tab:last-child {
margin-right: 0;
}
.query-tab text {
white-space: nowrap;
}
.query-input-wrap {
display: flex;
align-items: center;
height: 80rpx;
}
.camera-button,
.query-button {
height: 80rpx;
}
.camera-button {
width: 80rpx;
}
.query-button {
width: 128rpx;
}
.query-input-wrap :deep(.u-input) {
height: 100%;
display: flex;
align-items: center;
padding: 0 !important;
}
.query-input-wrap :deep(.u-input__input) {
height: 100%;
line-height: 80rpx;
}
</style>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Some files were not shown because too many files have changed in this diff Show More