You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

910 lines
23 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="page-container">
<!-- 顶部导航栏 -->
<NavBar :title="t('moldOperate.tabUp')">
<template #right>
<view class="nav-right-btn" @click="goToHistory">
<uni-icons type="calendar" size="22" color="#ffffff"></uni-icons>
</view>
</template>
</NavBar>
<!-- ========== 上模 ========== -->
<!-- 操作按钮区 -->
<view class="action-row">
<view class="scan-input-row">
<input
id="mold-scan-input"
class="scan-input"
v-model="scanCodeInput"
placeholder="红外扫码或输入设备码"
confirm-type="done"
@confirm="onScanInputConfirm"
/>
</view>
<view class="action-btn select-btn" @click="openDevicePicker">
<text class="btn-text">{{ t('moldOperate.selectDevice') }}</text>
</view>
</view>
<!-- 已选设备卡片 -->
<view class="section-card">
<view class="section-title-bar">
<view class="section-bar-line"></view>
<text class="section-title">{{ t('moldOperate.selectedDevice') }}</text>
</view>
<view class="card-body-grid">
<view class="grid-row">
<view class="grid-cell">
<text class="grid-label">{{ t('moldOperate.deviceName') }}</text>
<text class="grid-value">{{ textValue(selectedDevice.deviceName) }}</text>
</view>
<view class="grid-cell">
<text class="grid-label">{{ t('moldOperate.deviceCode') }}</text>
<text class="grid-value">{{ textValue(selectedDevice.deviceCode) }}</text>
</view>
</view>
<view class="grid-row">
<view class="grid-cell">
<text class="grid-label">{{ t('moldOperate.productionLine') }}</text>
<text class="grid-value">{{ textValue(selectedDevice.workshopName) }}</text>
</view>
<view class="grid-cell">
<text class="grid-label">{{ t('moldOperate.currentMold') }}</text>
<text class="grid-value highlight">{{ currentMoldDisplay }}</text>
</view>
</view>
<view class="grid-row">
<view class="grid-cell full-width">
<text class="grid-label">{{ t('moldOperate.deviceStatus') }}</text>
<view class="status-tag" :class="deviceStatusClass">{{ deviceStatusLabel }}</view>
</view>
</view>
</view>
</view>
<!-- -->
<view class="section-card">
<view class="section-title-bar">
<view class="section-bar-line"></view>
<text class="section-title">{{ t('moldOperate.selectMountMold') }}</text>
</view>
<template v-if="selectedMountMolds.length > 0">
<view v-for="(mold, index) in selectedMountMolds" :key="mold.id || index" class="card-body-grid">
<view class="grid-row">
<view class="grid-cell">
<text class="grid-label">{{ t('moldOperate.moldName') }}</text>
<text class="grid-value highlight">{{ textValue(mold.name || mold.moldName) }}</text>
</view>
<view class="grid-cell">
<text class="grid-label">{{ t('moldOperate.moldCode') }}</text>
<text class="grid-value">{{ textValue(mold.code || mold.moldCode) }}</text>
</view>
</view>
<view class="grid-row">
<view class="grid-cell">
<text class="grid-label">{{ t('moldOperate.product') }}</text>
<text class="grid-value">{{ textValue(mold.productName) }}</text>
</view>
<view class="grid-cell">
<text class="grid-label">{{ t('moldOperate.status') }}</text>
<view :class="['status-tag', getMoldStatusClass(mold.status)]">{{ getMoldStatusText(mold.status) }}</view>
</view>
</view>
</view>
<!-- 更换上模对象按钮 -->
<view class="change-target-btn" @click="openMountMoldPicker">
<text class="change-icon">&#8644;</text>
<text class="change-text">{{ t('moldOperate.changeMountTarget') }}</text>
</view>
</template>
<view v-else class="empty-mold-hint" @click="openMountMoldPicker">
<text class="empty-mold-text">+ {{ t('moldOperate.clickSelectMold') }}</text>
</view>
</view>
<!-- 操作人与备注 -->
<view class="section-card">
<view class="section-title-bar">
<view class="section-bar-line"></view>
<text class="section-title">{{ t('moldOperate.operator') + ' & ' + t('moldOperate.remark') }}</text>
</view>
<view class="card-body-grid">
<view class="form-row">
<view class="form-cell">
<text class="form-label"><text class="required">*</text>{{ t('moldOperate.operator') }}</text>
<view class="dropdown-input readonly-input">
<text class="dropdown-value">{{ currentUserName }}</text>
</view>
</view>
</view>
<view class="form-row">
<view class="form-cell">
<text class="form-label">{{ t('moldOperate.remark') }}</text>
<input
class="form-input"
v-model="remarkText"
:placeholder="t('moldOperate.placeholderRemark')"
:maxlength="200"
/>
</view>
</view>
</view>
</view>
<!-- 底部操作栏 - 上模 -->
<view class="bottom-actions">
<view class="bottom-btn cancel-btn" @click="handleCancel">{{ t('functionCommon.cancel') }}</view>
<view class="bottom-btn confirm-btn" @click="handleConfirmMount">{{ t('moldOperate.confirmMount') }}</view>
</view>
<sv-focus-no-keyboard ref="focusNoKeyboardRef"></sv-focus-no-keyboard>
</view>
</template>
<script setup>
import { computed, ref, nextTick } from 'vue'
import { onReady, onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getDeviceLedgerList, createMoldOperate } from '@/api/mes/moldoperate'
import { getMoldBrandPage } from '@/api/mes/mold'
import { getDeviceLineTree } from '@/api/mes/deviceLine'
import useUserStore from '@/store/modules/user'
const { t } = useI18n()
const userStore = useUserStore()
// 当前登录用户
const currentUserName = computed(() => userStore.name || '未知用户')
const currentUserId = computed(() => userStore.userId)
// ---- 工具函数 ----
function textValue(v) {
if (v === 0) return '0'
if (v == null) return '-'
const s = String(v).trim()
return s || '-'
}
// ==================== 共享: 设备列表 ====================
const deviceOptions = ref([])
async function loadDevices() {
try {
const res = await getDeviceLedgerList({ pageNo: 1, pageSize: 100 })
const root = res && res.data !== undefined ? res.data : res
const data = Array.isArray(root)
? root
: Array.isArray(root?.list) ? root.list
: Array.isArray(root?.rows) ? root.rows
: Array.isArray(root?.records) ? root.records
: []
deviceOptions.value = data.map((d) => ({
value: d.id,
label: `${d.deviceCode || ''} ${d.deviceName || ''}`.trim(),
raw: d
}))
} catch (e) {
console.error('loadDevices error', e)
}
}
// 产线树 - 用于设备产线名称转换
const lineInfoMap = ref(new Map())
function flattenLineTree(nodes, parentId) {
if (!Array.isArray(nodes)) return
for (const node of nodes) {
if (node.id != null && node.name != null) {
lineInfoMap.value.set(String(node.id), {
id: node.id,
name: node.name,
parentId: node.parentId != null ? node.parentId : (parentId || null),
parentChain: node.parentChain || ''
})
}
if (Array.isArray(node.children)) {
flattenLineTree(node.children, node.id)
}
}
}
function getTopLineName(deviceLineId) {
if (deviceLineId == null) return '-'
const node = lineInfoMap.value.get(String(deviceLineId))
if (!node) return '-'
if (node.parentChain) {
const firstId = node.parentChain.split(',')[0]?.trim()
if (firstId) {
const topNode = lineInfoMap.value.get(firstId)
if (topNode) return topNode.name
}
}
let current = node
const visited = new Set()
while (current.parentId != null && current.parentId > 0 && !visited.has(current.id)) {
visited.add(current.id)
const parent = lineInfoMap.value.get(String(current.parentId))
if (!parent) break
current = parent
}
return current.name || '-'
}
async function loadLineTree() {
if (lineInfoMap.value.size > 0) return
try {
const res = await getDeviceLineTree()
const tree = (res && res.data !== undefined) ? res.data : res
const nodes = Array.isArray(tree) ? tree : (tree?.list || tree?.children || [])
flattenLineTree(nodes)
} catch (e) {
console.error('load line tree error', e)
}
}
// 设置设备产线名称(从产线树转换)
function setDeviceLineName(device) {
if (device && device.deviceLine != null) {
const lineName = getTopLineName(device.deviceLine)
if (lineName && lineName !== '-') {
device.workshopName = lineName
}
}
}
// ==================== 上模模块 ====================
const selectedDevice = ref({})
const selectedMountMolds = ref([])
const tempSelectedDeviceId = ref(null)
const remarkText = ref('')
const selectedOperator = ref(null)
const scanCodeInput = ref('') // 扫码/输入设备码
// 聚焦阻止键盘弹出
const focusNoKeyboardRef = ref(null)
const keywordInputSelector = '#mold-scan-input input, input#mold-scan-input'
function focusKeywordNoKeyboard() {
nextTick(() => {
setTimeout(() => {
focusNoKeyboardRef.value?.focus(keywordInputSelector)
}, 80)
})
}
// 设备状态 - 上模
const deviceStatusClass = computed(() => {
const status = Number(selectedDevice.value?.deviceStatus)
if (status === 0) return 'running-tag'
if (status === 1) return 'stop-tag'
if (status >= 2) return 'fault-tag'
return ''
})
const deviceStatusLabel = computed(() => {
const status = Number(selectedDevice.value?.deviceStatus)
const map = {
0: t('moldOperate.statusRunning'),
1: t('moldOperate.statusStop'),
2: t('moldOperate.statusFault'),
3: t('moldOperate.statusFault')
}
return map[status] || textValue(selectedDevice.value?.deviceStatus) || '-'
})
const MOLD_STATUS_MAP = { 0: '在机', 1: '待用', 2: '维修', 3: '报废', 4: '在库' }
function getMoldStatusText(s) { return MOLD_STATUS_MAP[s] || textValue(s) }
function getMoldStatusClass(s) {
if (s === 0) return 'inuse-tag'
if (s === 2) return 'repair-tag'
if (s === 3) return 'scrap-tag'
return 'standby-tag'
}
// 当前在机模具 - 通过模具分页接口查询machineId = 当前设备ID
const currentMoldList = ref([])
async function fetchCurrentMolds(deviceName) {
if (!deviceName) {
currentMoldList.value = []
return
}
try {
// 查模具型号表,按当前设备名称筛选
const params = { deviceName, pageSize: 100 }
const res = await getMoldBrandPage(params)
const root = res && res.data !== undefined ? res.data : res
const pageData = root?.pageResult || root
const list = Array.isArray(pageData) ? pageData : (Array.isArray(pageData?.list) ? pageData.list : [])
currentMoldList.value = list
} catch (e) {
console.error('fetchCurrentMolds error', e)
currentMoldList.value = []
}
}
const currentMoldDisplay = computed(() => {
if (!selectedDevice.value?.id) return '-'
if (currentMoldList.value.length > 0) {
return currentMoldList.value.map(m => m.name || '').filter(Boolean).join('') || '-'
}
return t('moldOperate.noMoldOnDevice')
})
// ---- 操作人自动设置为当前登录用户 ----
function autoSetOperator() {
if (currentUserId.value && currentUserName.value) {
selectedOperator.value = {
value: currentUserId.value,
label: currentUserName.value
}
}
}
function selectDevice(device) {
selectedDevice.value = device || {}
tempSelectedDeviceId.value = device ? device.id : null
selectedMountMolds.value = []
if (device?.deviceName) fetchCurrentMolds(device.deviceName)
}
function openDevicePicker() {
uni.navigateTo({ url: '/pages_function/pages/moldoperate/deviceSelect?fromType=up' })
}
function openMountMoldPicker() {
if (!selectedDevice.value?.id) {
uni.showToast({ title: t('moldOperate.validatorDeviceRequired'), icon: 'none' })
return
}
// 把已选模具 ID 传过去做回显
getApp().globalData._moldSelectPreSelected = selectedMountMolds.value.map((m) => String(m.id))
uni.navigateTo({ url: '/pages_function/pages/moldoperate/moldSelect' })
}
function validFormMount() {
if (!selectedDevice.value?.id) {
uni.showToast({ title: t('moldOperate.validatorDeviceRequired'), icon: 'none' })
return false
}
if (!selectedMountMolds.value.length) {
uni.showToast({ title: t('moldOperate.validatorMoldRequired'), icon: 'none' })
return false
}
if (!selectedOperator.value) {
uni.showToast({ title: t('moldOperate.validatorOperatorRequired'), icon: 'none' })
return false
}
return true
}
async function handleConfirmMount() {
if (!validFormMount()) return
try {
const moldIds = selectedMountMolds.value.map((m) => m.id)
const payload = {
operateType: '1',
deviceId: String(selectedDevice.value.id),
moldId: moldIds.length === 1 ? String(moldIds[0]) : moldIds.map(String).join(','),
lineName: selectedDevice.value.workshopName || '',
lineId: selectedDevice.value.deviceLine || ''
}
// 添加操作人和备注
if (selectedOperator.value) {
payload.operatorId = selectedOperator.value.value
}
if (remarkText.value.trim()) {
payload.remark = remarkText.value.trim()
}
console.log('=== 上模提交参数 ===', JSON.stringify(payload))
console.log('=== deviceId type:', typeof payload.deviceId, 'value:', payload.deviceId)
console.log('=== moldId type:', typeof payload.moldId, 'value:', payload.moldId)
const res = await createMoldOperate(payload)
console.log('=== 上模返回 ===', JSON.stringify(res))
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
selectedDevice.value = {}
selectedMountMolds.value = []
} catch (e) {
console.error('=== 上模失败 ===', e)
const errMsg = e?.msg || (typeof e === 'string' ? e : e?.message) || '系统异常'
console.error('=== 错误详情 ===', errMsg)
uni.showToast({ title: t('functionCommon.saveFailed') + ': ' + errMsg, icon: 'none', duration: 3000 })
}
}
// ==================== 共享: 扫码 / 取消 / 返回 ====================
function matchDeviceByCode(code) {
if (!code) return null
let matched = null
if (code.toUpperCase().startsWith('EQUIPMENT-')) {
const idFromQr = code.replace(/EQUIPMENT-/i, '')
matched = deviceOptions.value.find((d) => String(d.raw.id) === idFromQr)
}
if (!matched) {
matched = deviceOptions.value.find((d) =>
Object.values(d.raw).some((v) =>
typeof v === 'string' && v.trim().toUpperCase() === code.toUpperCase()
) || d.label.toUpperCase().includes(code.toUpperCase())
)
}
if (!matched) {
const idMatch = code.match(/(\d+)$/)
if (idMatch) {
matched = deviceOptions.value.find((d) => String(d.raw.id) === idMatch[1])
}
}
return matched
}
function handleScan() {
uni.scanCode({
onlyFromCamera: false,
scanType: ['qrCode', 'barCode'],
success(res) {
const code = (res.result || '').trim()
if (!code) return
scanCodeInput.value = code // 输入框显示扫码结果
const matched = matchDeviceByCode(code)
if (matched) {
setDeviceLineName(matched.raw)
selectDevice(matched.raw)
} else {
uni.showToast({ title: t('moldOperate.deviceNotFound'), icon: 'none' })
}
},
fail(err) {
if (err.errMsg && !err.errMsg.includes('cancel')) {
console.warn('scan failed:', err)
}
}
})
}
// 手动输入设备码后确认
function onScanInputConfirm() {
const code = scanCodeInput.value.trim()
if (!code) return
const matched = matchDeviceByCode(code)
if (matched) {
setDeviceLineName(matched.raw)
selectDevice(matched.raw)
} else {
uni.showToast({ title: t('moldOperate.deviceNotFound'), icon: 'none' })
}
}
function handleCancel() {
selectedDevice.value = {}
selectedMountMolds.value = []
remarkText.value = ''
// 返回上一页(管理页面)
uni.navigateBack({
fail: () => uni.switchTab({ url: '/pages/work' })
})
}
function goToHistory() {
uni.navigateTo({ url: '/pages_function/pages/moldoperate/history?type=up' })
}
onReady(() => {
focusKeywordNoKeyboard()
})
onShow(async () => {
autoSetOperator()
await Promise.allSettled([loadDevices(), loadLineTree()])
// 从 globalData 读取设备选择页回传的设备
const device = getApp().globalData._deviceSelectResult
if (device) {
getApp().globalData._deviceSelectResult = null
selectDevice(device)
}
// 从 globalData 读取模具选择页回传的模具列表
const molds = getApp().globalData._moldSelectResult
if (molds) {
getApp().globalData._moldSelectResult = null
selectedMountMolds.value = molds
}
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: #f5f6f8;
padding-bottom: 140rpx;
}
/* ====== 导航栏右侧按钮 ====== */
.nav-right-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
}
/* ====== 操作按钮区 ====== */
.action-row {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx 24rpx;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
height: 72rpx;
padding: 0 24rpx;
border-radius: 10rpx;
background: #1f4b79;
color: #fff;
font-size: 26rpx;
font-weight: 600;
white-space: nowrap;
.btn-icon-wrap {
width: 36rpx;
height: 36rpx;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon {
font-size: 30rpx;
color: #fff;
}
.btn-text {
font-size: 26rpx;
line-height: 1;
}
}
/* 扫码输入框行 */
.scan-input-row {
flex: 1;
display: flex;
align-items: center;
height: 72rpx;
border-radius: 10rpx;
overflow: hidden;
}
.scan-input {
flex: 1;
height: 72rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #333;
background: #fff;
border: 1rpx solid #d0d5dd;
border-radius: 10rpx;
}
/* ====== 卡片通用样式 ====== */
.section-card {
margin: 16rpx 24rpx;
background: #ffffff;
border-radius: 14rpx;
}
.section-title-bar {
display: flex;
align-items: center;
gap: 10rpx;
padding: 22rpx 24rpx 0;
}
.section-bar-line {
width: 6rpx;
height: 32rpx;
border-radius: 3rpx;
background: #2563eb;
}
.section-title {
font-size: 30rpx;
font-weight: 700;
color: #1a1a1a;
}
.card-body-grid {
padding: 20rpx 24rpx 28rpx;
}
/* ====== 网格布局 ====== */
.grid-row {
display: flex;
gap: 20rpx;
margin-top: 18rpx;
&:first-child {
margin-top: 0;
}
}
.grid-cell {
flex: 1;
min-width: 0;
&.full-width {
flex-basis: 100%;
display: flex;
align-items: center;
gap: 12rpx;
}
}
.grid-label {
display: block;
font-size: 24rpx;
color: #999;
margin-bottom: 6rpx;
}
.grid-value {
display: block;
font-size: 27rpx;
color: #333;
word-break: break-all;
&.highlight {
color: #2563eb;
font-weight: 600;
}
}
/* ====== 状态标签 ====== */
.status-tag {
display: inline-flex;
align-items: center;
padding: 6rpx 18rpx;
border-radius: 6rpx;
font-size: 24rpx;
font-weight: 500;
&.running-tag {
color: #059669; background: #ecfdf5; border: 1rpx solid #a7f3d0;
}
&.stop-tag {
color: #d97706; background: #fffbeb; border: 1rpx solid #fde68a;
}
&.fault-tag {
color: #dc2626; background: #fef2f2; border: 1rpx solid #fecaca;
}
// 模具状态
&.inuse-tag {
color: #059669; background: #ecfdf5; border: 1rpx solid #a7f3d0;
}
&.standby-tag {
color: #2563eb; background: #eff6ff; border: 1rpx solid #bfdbfe;
}
&.repair-tag {
color: #d97706; background: #fffbeb; border: 1rpx solid #fde68a;
}
&.scrap-tag {
color: #dc2626; background: #fef2f2; border: 1rpx solid #fecaca;
}
}
/* ====== 更换下模对象按钮 ====== */
.change-target-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
margin: 0 24rpx 24rpx;
padding: 20rpx 0;
border: 1rpx dashed #ccc;
border-radius: 10rpx;
&:active { background: #fafafa; }
.change-icon { font-size: 28rpx; color: #666; }
.change-text { font-size: 26rpx; color: #666; }
}
/* ====== 表单行(操作人/备注) ====== */
.form-row {
margin-top: 18rpx;
&:first-child {
margin-top: 0;
}
}
.form-cell {
width: 100%;
.form-label {
display: block;
font-size: 24rpx;
color: #999;
margin-bottom: 8rpx;
.required {
color: #dc2626;
margin-right: 4rpx;
}
}
}
/* ====== 下拉选择器(操作人) ====== */
.select-dropdown {
position: relative;
width: 100%;
}
.dropdown-input {
display: flex;
align-items: center;
height: 72rpx;
padding: 0 20rpx;
border: 1rpx solid #e0e0e0;
border-radius: 10rpx;
font-size: 27rpx;
color: #333;
background: #f9fafb;
&.readonly-input {
background: #f8fafc;
border-color: #f0f0f0;
}
}
.dropdown-value {
flex: 1;
&.placeholder {
color: #bbb;
}
}
.dropdown-arrow {
position: absolute;
right: 16rpx;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
}
/* ====== 下拉面板(在输入框下方展开) ====== */
.dropdown-panel {
position: absolute;
top: 76rpx;
left: 0;
right: 0;
z-index: 99;
background: #ffffff;
border: 1rpx solid #e0e0e0;
border-radius: 12rpx;
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.dropdown-scroll {
max-height: 400rpx;
}
.dropdown-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 22rpx 28rpx;
font-size: 27rpx;
color: #333;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
&:active {
background: #f5f7fa;
}
&.active {
color: #1f7cff;
background: #f0f7ff;
}
}
.dropdown-item-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dropdown-empty {
padding: 32rpx;
text-align: center;
font-size: 26rpx;
color: #999;
}
.form-input {
height: 72rpx;
padding: 0 20rpx;
border: 1rpx solid #e0e0e0;
border-radius: 10rpx;
font-size: 27rpx;
color: #333;
background: #f9fafb;
}
/* ====== 空提示 ====== */
.empty-mold-hint {
padding: 40rpx 24rpx;
text-align: center;
}
.empty-mold-text {
font-size: 28rpx;
color: #2563eb;
&.loading-text {
color: #999;
}
&.empty-gray {
color: #bbb;
}
}
/* ====== 底部操作栏 ====== */
.bottom-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
gap: 18rpx;
padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom));
background: #ffffff;
box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.06);
z-index: 99;
&.single-btn {
gap: 0;
}
}
.bottom-btn {
flex: 1;
height: 84rpx;
line-height: 84rpx;
text-align: center;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 600;
}
.confirm-btn {
background: #1f4b79;
color: #ffffff;
}
.cancel-btn {
background: #eef2f7;
color: #475569;
}
</style>