550 lines
21 KiB
Vue
550 lines
21 KiB
Vue
<template>
|
||||
|
|
<div class="entry-mode-panel">
|
|||
|
|
<div class="main-top-grid">
|
|||
|
|
<section class="workbench-panel entry-column-panel">
|
|||
|
|
<div class="panel-header column-header">
|
|||
|
|
<div>
|
|||
|
|
<div class="panel-title">整机扫码输入</div>
|
|||
|
|
<div class="panel-subtitle">当前工序:{{ processLabel || '未选择' }} / {{ stepLabel || '未选择' }}</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="panel-metric-card">
|
|||
|
|
<span class="panel-metric-label">总数</span>
|
|||
|
|
<strong class="panel-metric-value">{{ hideProcessTotal ? '***' : statistics.today_total }}</strong>
|
|||
|
|
<button type="button" class="metric-eye-button" :class="{ 'is-hidden': hideProcessTotal }" @click="hideProcessTotal = !hideProcessTotal">
|
|||
|
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|||
|
|
<path d="M12 5C6.5 5 2.12 8.44 1 12c1.12 3.56 5.5 7 11 7s9.88-3.44 11-7c-1.12-3.56-5.5-7-11-7Zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9Zm0-2.5A2 2 0 1 0 12 10a2 2 0 0 0 0 4Z" fill="currentColor" />
|
|||
|
|
</svg>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="workbench-form">
|
|||
|
|
<div class="field-group field-primary">
|
|||
|
|
<label>加工单号</label>
|
|||
|
|
<el-input
|
|||
|
|
ref="processOrderNoInputRef"
|
|||
|
|
v-model="processOrderNo"
|
|||
|
|
clearable
|
|||
|
|
size="large"
|
|||
|
|
placeholder="请扫描或输入加工单号"
|
|||
|
|
@keyup.enter="handleProcessOrderNoComplete"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="field-row field-row-split">
|
|||
|
|
<div class="field-group">
|
|||
|
|
<label>商品编码</label>
|
|||
|
|
<el-input
|
|||
|
|
ref="productCodeInputRef"
|
|||
|
|
v-model="productCode"
|
|||
|
|
clearable
|
|||
|
|
size="large"
|
|||
|
|
placeholder="请扫描或输入商品编码"
|
|||
|
|
@keyup.enter="handleProductCodeComplete"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div class="field-group field-group-quantity">
|
|||
|
|
<label>数量</label>
|
|||
|
|
<el-input-number
|
|||
|
|
ref="quantityInputRef"
|
|||
|
|
v-model="quantity"
|
|||
|
|
:min="1"
|
|||
|
|
:step="1"
|
|||
|
|
size="large"
|
|||
|
|
controls-position="right"
|
|||
|
|
class="quantity-input"
|
|||
|
|
@keyup.enter="handleQuantityComplete"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="field-row">
|
|||
|
|
<div class="field-group">
|
|||
|
|
<div class="field-label-row">
|
|||
|
|
<label>用户编码</label>
|
|||
|
|
<div class="lock-switch">
|
|||
|
|
<span>锁定用户编码</span>
|
|||
|
|
<el-switch v-model="autoUserCode" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<el-input
|
|||
|
|
ref="userCodeInputRef"
|
|||
|
|
v-model="userCode"
|
|||
|
|
:disabled="autoUserCode"
|
|||
|
|
clearable
|
|||
|
|
size="large"
|
|||
|
|
placeholder="请扫描或输入用户编码"
|
|||
|
|
@update:model-value="userCode = String($event || '').toUpperCase()"
|
|||
|
|
@input="handleUserCodeInput"
|
|||
|
|
@keyup.enter="handleUserCodeComplete"
|
|||
|
|
@blur="runUserCodeCheck(userCode, true)"
|
|||
|
|
/>
|
|||
|
|
<div v-if="userCodeCheckError" class="field-error-tip">{{ userCodeCheckError }}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="action-row">
|
|||
|
|
<el-button type="primary" size="large" :loading="submitting" @click="submitEntry">提交</el-button>
|
|||
|
|
<el-button size="large" @click="resetForm">重置输入</el-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section class="workbench-panel stats-column-panel">
|
|||
|
|
<div class="dashboard-header column-header">
|
|||
|
|
<div class="dashboard-title">工位统计</div>
|
|||
|
|
<span class="dashboard-caption">当日扫码量</span>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="userStats.length" class="station-grid">
|
|||
|
|
<div v-for="item in userStats" :key="item.user_code" class="station-card">
|
|||
|
|
<div class="station-icon">工</div>
|
|||
|
|
<div class="station-info">
|
|||
|
|
<strong>{{ item.user_code }}</strong>
|
|||
|
|
</div>
|
|||
|
|
<b>{{ item.count }}</b>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<el-empty v-else description="暂无数据" :image-size="72" />
|
|||
|
|
</section>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<section class="records-panel">
|
|||
|
|
<div class="dashboard-header records-header">
|
|||
|
|
<div class="dashboard-title">最近记录</div>
|
|||
|
|
<span class="dashboard-caption">实时刷新最近 10 条</span>
|
|||
|
|
</div>
|
|||
|
|
<el-table :data="recentRecords" stripe>
|
|||
|
|
<el-table-column prop="process_order_no" label="加工单号" min-width="180" />
|
|||
|
|
<el-table-column prop="product_code" label="商品编码" min-width="180" />
|
|||
|
|
<el-table-column prop="quantity" label="数量" width="100" />
|
|||
|
|
<el-table-column prop="user_code" label="用户编码" min-width="120" />
|
|||
|
|
<el-table-column label="工序" min-width="140">
|
|||
|
|
<template #default="scope">{{ scope.row.process?.process_name || '-' }}</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="步骤" min-width="140">
|
|||
|
|
<template #default="scope">{{ scope.row.step?.step_name || '-' }}</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="状态" min-width="120">
|
|||
|
|
<template #default="scope">{{ scope.row.status?.status_name || '-' }}</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="结果" width="100">
|
|||
|
|
<template #default="scope">
|
|||
|
|
<el-tag :type="Number(scope.row.scan_result) === 1 ? 'success' : 'danger'">
|
|||
|
|
{{ Number(scope.row.scan_result) === 1 ? '成功' : '失败' }}
|
|||
|
|
</el-tag>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column prop="operator" label="操作人" min-width="120" />
|
|||
|
|
<el-table-column prop="created_date" label="扫码时间" min-width="180" />
|
|||
|
|
</el-table>
|
|||
|
|
</section>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ElMessage } from 'element-plus'
|
|||
|
|
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|||
|
|
import {
|
|||
|
|
checkMemberUser,
|
|||
|
|
getDataEntryConfig,
|
|||
|
|
getProductRecentRecords,
|
|||
|
|
getProductStatistics,
|
|||
|
|
getProductUserCodeStats,
|
|||
|
|
submitProductRecord,
|
|||
|
|
type ProduceRecordItem,
|
|||
|
|
type ProduceDataEntrySpecificRule,
|
|||
|
|
type ProduceStatistics,
|
|||
|
|
type ProduceUserStatsItem,
|
|||
|
|
} from '~/apps/produce/api/data-entry'
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
selectedProcess?: string | number
|
|||
|
|
selectedStep?: string | number
|
|||
|
|
processLabel?: string
|
|||
|
|
stepLabel?: string
|
|||
|
|
scanMode?: 'manual' | 'scanner'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const props = withDefaults(defineProps<Props>(), {
|
|||
|
|
selectedProcess: '',
|
|||
|
|
selectedStep: '',
|
|||
|
|
processLabel: '',
|
|||
|
|
stepLabel: '',
|
|||
|
|
scanMode: 'manual',
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
'play-success': []
|
|||
|
|
'play-error': []
|
|||
|
|
'speak-success': []
|
|||
|
|
'speak-error': []
|
|||
|
|
}>()
|
|||
|
|
|
|||
|
|
const STORAGE_KEY_LOCK = 'produce_web_machine_entry_lock'
|
|||
|
|
const STORAGE_KEY_USER_CODE = 'produce_web_machine_entry_user_code'
|
|||
|
|
const STORAGE_KEY_HIDE_PROCESS_TOTAL = 'produce_web_machine_entry_hide_process_total'
|
|||
|
|
|
|||
|
|
const processOrderNo = ref('')
|
|||
|
|
const productCode = ref('')
|
|||
|
|
const quantity = ref(1)
|
|||
|
|
const userCode = ref('')
|
|||
|
|
const autoUserCode = ref(false)
|
|||
|
|
const hideProcessTotal = ref(false)
|
|||
|
|
const submitting = ref(false)
|
|||
|
|
const recentRecords = ref<ProduceRecordItem[]>([])
|
|||
|
|
const userStats = ref<ProduceUserStatsItem[]>([])
|
|||
|
|
const statistics = ref<ProduceStatistics>({ today_total: 0, success_total: 0, failed_total: 0 })
|
|||
|
|
const basicSettings = ref<Record<string, any>>({})
|
|||
|
|
const specificRules = ref<ProduceDataEntrySpecificRule[]>([])
|
|||
|
|
const checkingUserCode = ref(false)
|
|||
|
|
const userCodeCheckError = ref('')
|
|||
|
|
const processOrderNoInputRef = ref()
|
|||
|
|
const productCodeInputRef = ref()
|
|||
|
|
const quantityInputRef = ref()
|
|||
|
|
const userCodeInputRef = ref()
|
|||
|
|
let userCodeCheckTimer: ReturnType<typeof setTimeout> | null = null
|
|||
|
|
let scannerSubmitTimer: ReturnType<typeof setTimeout> | null = null
|
|||
|
|
let statisticsRequestToken = 0
|
|||
|
|
let recentRecordsRequestToken = 0
|
|||
|
|
let dataEntryConfigRequestToken = 0
|
|||
|
|
let initialized = false
|
|||
|
|
|
|||
|
|
const restoreLocalState = () => {
|
|||
|
|
if (typeof window === 'undefined') return
|
|||
|
|
autoUserCode.value = localStorage.getItem(STORAGE_KEY_LOCK) === 'true'
|
|||
|
|
hideProcessTotal.value = localStorage.getItem(STORAGE_KEY_HIDE_PROCESS_TOTAL) === 'true'
|
|||
|
|
if (autoUserCode.value) {
|
|||
|
|
userCode.value = localStorage.getItem(STORAGE_KEY_USER_CODE) || ''
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const saveLocalState = () => {
|
|||
|
|
if (typeof window === 'undefined') return
|
|||
|
|
localStorage.setItem(STORAGE_KEY_LOCK, String(autoUserCode.value))
|
|||
|
|
localStorage.setItem(STORAGE_KEY_HIDE_PROCESS_TOTAL, String(hideProcessTotal.value))
|
|||
|
|
if (autoUserCode.value && userCode.value.trim()) {
|
|||
|
|
localStorage.setItem(STORAGE_KEY_USER_CODE, userCode.value)
|
|||
|
|
} else {
|
|||
|
|
localStorage.removeItem(STORAGE_KEY_USER_CODE)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const focusPrimaryInput = () => { setTimeout(() => processOrderNoInputRef.value?.focus?.(), 80) }
|
|||
|
|
const focusProductCodeInput = () => { setTimeout(() => productCodeInputRef.value?.focus?.(), 80) }
|
|||
|
|
const focusQuantityInput = () => { setTimeout(() => quantityInputRef.value?.focus?.(), 80) }
|
|||
|
|
const focusUserCodeInput = () => { setTimeout(() => userCodeInputRef.value?.focus?.(), 80) }
|
|||
|
|
|
|||
|
|
const clearScannerSubmitTimer = () => {
|
|||
|
|
if (!scannerSubmitTimer) return
|
|||
|
|
clearTimeout(scannerSubmitTimer)
|
|||
|
|
scannerSubmitTimer = null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleProcessOrderNoComplete = () => {
|
|||
|
|
if (props.scanMode !== 'scanner') {
|
|||
|
|
void submitEntry()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (!submitting.value) focusProductCodeInput()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleProductCodeComplete = () => {
|
|||
|
|
if (props.scanMode !== 'scanner') {
|
|||
|
|
void submitEntry()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (!submitting.value) focusQuantityInput()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleQuantityComplete = () => {
|
|||
|
|
if (props.scanMode !== 'scanner') return
|
|||
|
|
if (submitting.value) return
|
|||
|
|
if (autoUserCode.value) {
|
|||
|
|
void submitEntry()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
focusUserCodeInput()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleUserCodeComplete = () => {
|
|||
|
|
if (props.scanMode !== 'scanner') {
|
|||
|
|
void submitEntry()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (submitting.value) return
|
|||
|
|
clearScannerSubmitTimer()
|
|||
|
|
userCodeInputRef.value?.blur?.()
|
|||
|
|
scannerSubmitTimer = setTimeout(() => {
|
|||
|
|
scannerSubmitTimer = null
|
|||
|
|
if (!submitting.value) void submitEntry()
|
|||
|
|
}, 50)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleUserCodeInput = () => {
|
|||
|
|
if (props.scanMode !== 'scanner' || !userCode.value.trim() || submitting.value) return
|
|||
|
|
clearScannerSubmitTimer()
|
|||
|
|
scannerSubmitTimer = setTimeout(() => {
|
|||
|
|
scannerSubmitTimer = null
|
|||
|
|
handleUserCodeComplete()
|
|||
|
|
}, 300)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const resetDashboardData = () => {
|
|||
|
|
recentRecords.value = []
|
|||
|
|
userStats.value = []
|
|||
|
|
statistics.value = { today_total: 0, success_total: 0, failed_total: 0 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const isSameSelection = (processId: string | number, stepId: string | number) => String(props.selectedProcess) === String(processId) && String(props.selectedStep) === String(stepId)
|
|||
|
|
const resetUserCodeCheckState = () => { userCodeCheckError.value = '' }
|
|||
|
|
|
|||
|
|
const runUserCodeCheck = async (value: string, silent: boolean = false) => {
|
|||
|
|
const normalizedValue = value.trim()
|
|||
|
|
if (!normalizedValue) {
|
|||
|
|
checkingUserCode.value = false
|
|||
|
|
resetUserCodeCheckState()
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
checkingUserCode.value = true
|
|||
|
|
try {
|
|||
|
|
await checkMemberUser(normalizedValue)
|
|||
|
|
userCodeCheckError.value = ''
|
|||
|
|
return true
|
|||
|
|
} catch (error: any) {
|
|||
|
|
userCodeCheckError.value = error?.message || '用户编码不存在'
|
|||
|
|
if (!silent) ElMessage.warning(userCodeCheckError.value)
|
|||
|
|
return false
|
|||
|
|
} finally {
|
|||
|
|
checkingUserCode.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const scheduleUserCodeCheck = (value: string) => {
|
|||
|
|
if (userCodeCheckTimer) clearTimeout(userCodeCheckTimer)
|
|||
|
|
const normalizedValue = value.trim()
|
|||
|
|
if (!normalizedValue) {
|
|||
|
|
resetUserCodeCheckState()
|
|||
|
|
checkingUserCode.value = false
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
userCodeCheckTimer = setTimeout(() => { void runUserCodeCheck(normalizedValue, true) }, 360)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadStatisticsData = async () => {
|
|||
|
|
if (!props.selectedProcess || !props.selectedStep) {
|
|||
|
|
resetDashboardData()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const requestToken = ++statisticsRequestToken
|
|||
|
|
const processSnapshot = props.selectedProcess
|
|||
|
|
const stepSnapshot = props.selectedStep
|
|||
|
|
const params = { process_id: processSnapshot, step_id: stepSnapshot }
|
|||
|
|
const [statsRes, userStatsRes] = await Promise.all([
|
|||
|
|
getProductStatistics(params).catch(() => ({ today_total: 0, success_total: 0, failed_total: 0 })),
|
|||
|
|
getProductUserCodeStats(params).catch(() => []),
|
|||
|
|
])
|
|||
|
|
if (requestToken !== statisticsRequestToken || !isSameSelection(processSnapshot, stepSnapshot)) return
|
|||
|
|
statistics.value = {
|
|||
|
|
today_total: Number(statsRes?.today_total || 0),
|
|||
|
|
success_total: Number(statsRes?.success_total || 0),
|
|||
|
|
failed_total: Number(statsRes?.failed_total || 0),
|
|||
|
|
}
|
|||
|
|
userStats.value = Array.isArray(userStatsRes) ? userStatsRes : []
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadRecentData = async () => {
|
|||
|
|
if (!props.selectedProcess || !props.selectedStep) {
|
|||
|
|
recentRecords.value = []
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const requestToken = ++recentRecordsRequestToken
|
|||
|
|
const processSnapshot = props.selectedProcess
|
|||
|
|
const stepSnapshot = props.selectedStep
|
|||
|
|
const res = await getProductRecentRecords({ process_id: processSnapshot, step_id: stepSnapshot, limit: 10 }).catch(() => ({ items: [] }))
|
|||
|
|
if (requestToken !== recentRecordsRequestToken || !isSameSelection(processSnapshot, stepSnapshot)) return
|
|||
|
|
recentRecords.value = Array.isArray(res?.items) ? res.items : []
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadDataEntryConfig = async () => {
|
|||
|
|
const requestToken = ++dataEntryConfigRequestToken
|
|||
|
|
const processSnapshot = props.selectedProcess
|
|||
|
|
const stepSnapshot = props.selectedStep
|
|||
|
|
const processOrderNoSnapshot = processOrderNo.value.trim()
|
|||
|
|
const productCodeSnapshot = productCode.value.trim()
|
|||
|
|
const userCodeSnapshot = userCode.value.trim()
|
|||
|
|
const res = await getDataEntryConfig({
|
|||
|
|
process_id: processSnapshot,
|
|||
|
|
step_id: stepSnapshot,
|
|||
|
|
apply_mode: 'machine',
|
|||
|
|
process_order_no: processOrderNoSnapshot,
|
|||
|
|
product_code: productCodeSnapshot,
|
|||
|
|
user_code: userCodeSnapshot,
|
|||
|
|
order_no: processOrderNoSnapshot,
|
|||
|
|
}).catch(() => null)
|
|||
|
|
if (requestToken !== dataEntryConfigRequestToken || !isSameSelection(processSnapshot, stepSnapshot) || processOrderNo.value.trim() !== processOrderNoSnapshot || productCode.value.trim() !== productCodeSnapshot || userCode.value.trim() !== userCodeSnapshot) return
|
|||
|
|
basicSettings.value = res?.basic_setting || {}
|
|||
|
|
specificRules.value = Array.isArray(res?.specific_rules) ? res.specific_rules : []
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const matchSpecificRule = (rule: ProduceDataEntrySpecificRule, valueMap: Record<string, string>) => {
|
|||
|
|
const matchField = rule.match_field || 'order_no'
|
|||
|
|
const matchValue = String(valueMap[matchField] || '').trim()
|
|||
|
|
const pattern = String(rule.rule_pattern || '').trim()
|
|||
|
|
if (!matchValue || !pattern) {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
return new RegExp(pattern).test(matchValue)
|
|||
|
|
} catch {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const validateSpecificRules = () => {
|
|||
|
|
if (!specificRules.value.length) {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const context = {
|
|||
|
|
process_order_no: processOrderNo.value.trim(),
|
|||
|
|
product_code: productCode.value.trim(),
|
|||
|
|
user_code: userCode.value.trim(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const blockedRule = specificRules.value.find((rule: ProduceDataEntrySpecificRule) => {
|
|||
|
|
if ((rule.apply_mode || 'order') !== 'machine' && rule.apply_mode !== 'both') {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
return Number(rule.rule_type) === 2 && matchSpecificRule(rule, context)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if (blockedRule) {
|
|||
|
|
ElMessage.warning(blockedRule.rule_name || '当前整机数据命中特定规则,禁止提交')
|
|||
|
|
const matchField = blockedRule.match_field || 'process_order_no'
|
|||
|
|
if (matchField === 'product_code') {
|
|||
|
|
focusProductCodeInput()
|
|||
|
|
} else if (matchField === 'user_code') {
|
|||
|
|
focusUserCodeInput()
|
|||
|
|
} else {
|
|||
|
|
focusPrimaryInput()
|
|||
|
|
}
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const validateBeforeSubmit = () => {
|
|||
|
|
if (!props.selectedProcess) { ElMessage.warning('请选择工序'); return false }
|
|||
|
|
if (!props.selectedStep) { ElMessage.warning('请选择步骤'); return false }
|
|||
|
|
if (!processOrderNo.value.trim()) { ElMessage.warning('请输入加工单号'); focusPrimaryInput(); return false }
|
|||
|
|
const processOrderNoRegex = String(basicSettings.value.process_order_no_regex || '').trim()
|
|||
|
|
if (processOrderNoRegex) {
|
|||
|
|
try {
|
|||
|
|
const regex = new RegExp(processOrderNoRegex)
|
|||
|
|
if (!regex.test(processOrderNo.value.trim())) { ElMessage.warning('加工单号格式不符合生产配置规则'); focusPrimaryInput(); return false }
|
|||
|
|
} catch {}
|
|||
|
|
}
|
|||
|
|
if (!productCode.value.trim()) { ElMessage.warning('请输入商品编码'); focusProductCodeInput(); return false }
|
|||
|
|
const productCodeMinLength = Number(basicSettings.value.product_code_min_length || 0)
|
|||
|
|
if (productCodeMinLength > 0 && productCode.value.trim().length < productCodeMinLength) { ElMessage.warning(`商品编码长度不能少于 ${productCodeMinLength} 位`); focusProductCodeInput(); return false }
|
|||
|
|
const productCodeMaxLength = Number(basicSettings.value.product_code_max_length || 0)
|
|||
|
|
if (productCodeMaxLength > 0 && productCode.value.trim().length > productCodeMaxLength) { ElMessage.warning(`商品编码长度不能超过 ${productCodeMaxLength} 位`); focusProductCodeInput(); return false }
|
|||
|
|
const productCodeRegex = String(basicSettings.value.product_code_regex || '').trim()
|
|||
|
|
if (productCodeRegex) {
|
|||
|
|
try {
|
|||
|
|
const regex = new RegExp(productCodeRegex)
|
|||
|
|
if (!regex.test(productCode.value.trim())) { ElMessage.warning('商品编码格式不符合生产配置规则'); focusProductCodeInput(); return false }
|
|||
|
|
} catch {}
|
|||
|
|
}
|
|||
|
|
if (!Number.isFinite(Number(quantity.value)) || Number(quantity.value) < 1) { ElMessage.warning('数量必须大于 0'); focusQuantityInput(); return false }
|
|||
|
|
if (!userCode.value.trim()) { ElMessage.warning('请输入用户编码'); focusUserCodeInput(); return false }
|
|||
|
|
const userCodeRegex = String(basicSettings.value.user_code_regex || '').trim()
|
|||
|
|
if (userCodeRegex) {
|
|||
|
|
try {
|
|||
|
|
const regex = new RegExp(userCodeRegex)
|
|||
|
|
if (!regex.test(userCode.value.trim())) { ElMessage.warning('用户编码格式不符合基础设置规则'); focusUserCodeInput(); return false }
|
|||
|
|
} catch {}
|
|||
|
|
}
|
|||
|
|
if (userCodeCheckError.value) { ElMessage.warning(userCodeCheckError.value); focusUserCodeInput(); return false }
|
|||
|
|
if (!validateSpecificRules()) { return false }
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const resetForm = () => {
|
|||
|
|
processOrderNo.value = ''
|
|||
|
|
productCode.value = ''
|
|||
|
|
quantity.value = 1
|
|||
|
|
if (!autoUserCode.value) {
|
|||
|
|
userCode.value = ''
|
|||
|
|
resetUserCodeCheckState()
|
|||
|
|
}
|
|||
|
|
saveLocalState()
|
|||
|
|
focusPrimaryInput()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const submitEntry = async () => {
|
|||
|
|
if (!validateBeforeSubmit()) return
|
|||
|
|
const passed = await runUserCodeCheck(userCode.value)
|
|||
|
|
if (!passed) { emit('play-error'); nextTick(() => focusUserCodeInput()); return }
|
|||
|
|
submitting.value = true
|
|||
|
|
try {
|
|||
|
|
const result = await submitProductRecord({
|
|||
|
|
process_order_no: processOrderNo.value.trim(),
|
|||
|
|
product_code: productCode.value.trim(),
|
|||
|
|
quantity: Number(quantity.value) || 1,
|
|||
|
|
process_id: props.selectedProcess,
|
|||
|
|
step_id: props.selectedStep,
|
|||
|
|
user_code: userCode.value.trim(),
|
|||
|
|
scan_result: 1,
|
|||
|
|
})
|
|||
|
|
if (Number(result?.code) !== 0) throw new Error(result?.msg || '录入失败')
|
|||
|
|
ElMessage.success('录入成功')
|
|||
|
|
emit('play-success')
|
|||
|
|
emit('speak-success')
|
|||
|
|
saveLocalState()
|
|||
|
|
resetForm()
|
|||
|
|
await Promise.all([loadStatisticsData(), loadRecentData()])
|
|||
|
|
} catch (error: any) {
|
|||
|
|
ElMessage.error(error?.message || '录入失败')
|
|||
|
|
emit('play-error')
|
|||
|
|
emit('speak-error')
|
|||
|
|
} finally {
|
|||
|
|
submitting.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const refreshBySelection = async () => {
|
|||
|
|
if (!props.selectedProcess || !props.selectedStep) {
|
|||
|
|
basicSettings.value = {}
|
|||
|
|
specificRules.value = []
|
|||
|
|
resetDashboardData()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
resetDashboardData()
|
|||
|
|
await Promise.all([loadStatisticsData(), loadRecentData(), loadDataEntryConfig()])
|
|||
|
|
focusPrimaryInput()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
watch(() => [props.selectedProcess, props.selectedStep], () => { if (initialized) void refreshBySelection() })
|
|||
|
|
watch(processOrderNo, () => { if (initialized) { saveLocalState(); void loadDataEntryConfig() } })
|
|||
|
|
watch([productCode, quantity], () => { if (initialized) { saveLocalState(); void loadDataEntryConfig() } })
|
|||
|
|
watch(userCode, () => { if (initialized) { scheduleUserCodeCheck(userCode.value); if (autoUserCode.value || userCode.value.trim()) saveLocalState(); void loadDataEntryConfig() } })
|
|||
|
|
watch([autoUserCode, hideProcessTotal], () => { if (initialized) saveLocalState() })
|
|||
|
|
|
|||
|
|
onMounted(async () => {
|
|||
|
|
restoreLocalState()
|
|||
|
|
await refreshBySelection()
|
|||
|
|
initialized = true
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onBeforeUnmount(() => {
|
|||
|
|
if (userCodeCheckTimer) clearTimeout(userCodeCheckTimer)
|
|||
|
|
clearScannerSubmitTimer()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
defineExpose({ focusPrimaryInput, focusProductCodeInput, focusQuantityInput, focusUserCodeInput, submitEntry, resetForm })
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped src="./entry-panel.css"></style>
|