feat:模具管理-上下模模块

master
zhongwenkai 4 days ago
parent d4b43bc1e8
commit 78688a7358

@ -50,8 +50,8 @@ export function getLowerMoldList(id) {
export function getDeviceLedgerList(params = {}) {
return request({
url: '/admin-api/mes/device-ledger/list',
url: '/admin-api/mes/device-ledger/page',
method: 'get',
params
params: { pageNo: params.pageNo || 1, pageSize: params.pageSize || 100, ...params }
})
}

@ -211,6 +211,7 @@ function getUviewIconName(icon) {
function handleClick(menu) {
const url = resolveMenuUrl(menu)
console.log('[PermissionMenu] 点击菜单:', menu?.name, '| _splitKey:', menu?._splitKey, '| 路由:', url)
if (url) {
uni.navigateTo({ url })
return

@ -183,6 +183,7 @@ export default {
search: 'Search',
reset: 'Reset',
cancel: 'Cancel',
confirm: 'Confirm',
save: 'Save',
loading: 'Loading...',
loadingMore: 'Loading more...',
@ -339,7 +340,35 @@ export default {
validatorMoldRequired: 'Select at least one mold for mounting',
validatorLowerMoldRequired: 'Select at least one mold for dismounting',
loadEditFailed: 'Failed to load edit data',
confirmDelete: 'Confirm delete this mold operate record?'
confirmDelete: 'Confirm delete this mold operate record?',
scanDevice: 'Scan Device',
selectDevice: 'Select Device',
selectedDevice: 'Selected Device',
deviceCode: 'Device Code',
productionLine: 'Production Line',
currentMold: 'Current Mold',
deviceStatus: 'Device Status',
statusRunning: 'Running',
statusStop: 'Stopped',
statusFault: 'Fault',
selectMountMold: 'Select Mold to Mount',
product: 'Product',
status: 'Status',
pendingMount: 'Pending Mount',
confirmMount: 'Confirm Mount',
clickSelectMold: 'Click to select mold',
noDeviceData: 'No device data',
deviceNotFound: 'Device not found',
deviceInfo: 'Device Info',
currentMoldInfo: 'Current Mold on Machine',
mountTime: 'Mount Time',
useCount: 'Usage Count',
countUnit: '',
changeMountTarget: 'Change Mount Target',
changeTarget: 'Change Dismount Target',
confirmDismount: 'Confirm Dismount',
clickSelectDeviceFirst: 'Please select a device first',
noMoldOnDevice: 'No mold on this device'
},
moldInspectionItems: {
moduleName: 'Inspection Items',

@ -106,6 +106,8 @@ const literalMap = {
'模具入库': 'moldReturn.moduleName',
'模具入库详情': 'moldReturn.detailTitle',
'上下模': 'moldOperate.moduleName',
'上模': 'moldOperate.tabUp',
'下模': 'moldOperate.tabDown',
'上下模详情': 'moldOperate.detailTitle',
'点检项库': 'moldInspectionItems.moduleName',
'点检项库详情': 'moldInspectionItems.detailTitle',

@ -183,6 +183,7 @@ export default {
search: '查询',
reset: '重置',
cancel: '取消',
confirm: '确认',
save: '保存',
loading: '加载中...',
loadingMore: '正在加载更多...',
@ -339,7 +340,35 @@ export default {
validatorMoldRequired: '请至少选择一个上模模具',
validatorLowerMoldRequired: '请至少选择一个下模模具',
loadEditFailed: '加载编辑数据失败',
confirmDelete: '确认删除该上下模记录吗?'
confirmDelete: '确认删除该上下模记录吗?',
scanDevice: '扫设备码',
selectDevice: '选择设备',
selectedDevice: '已选设备',
deviceCode: '设备编码',
productionLine: '所属产线',
currentMold: '当前在机模具',
deviceStatus: '设备状态',
statusRunning: '运行中',
statusStop: '已停止',
statusFault: '故障',
selectMountMold: '选择待上模模具',
product: '产品',
status: '状态',
pendingMount: '待上模',
confirmMount: '确认上模',
clickSelectMold: '点击选择模具',
noDeviceData: '暂无设备数据',
deviceNotFound: '未找到对应设备',
deviceInfo: '设备信息',
currentMoldInfo: '当前在机模具',
mountTime: '上模时间',
useCount: '使用次数',
countUnit: '次',
changeMountTarget: '更换上模对象',
changeTarget: '更换下模对象',
confirmDismount: '确认下模',
clickSelectDeviceFirst: '请先选择设备',
noMoldOnDevice: '该设备暂无在机模具'
},
moldInspectionItems: {
moduleName: '点检项库',

@ -578,7 +578,7 @@
{
"path": "moldoperate/index",
"style": {
"navigationBarTitleText": "上模",
"navigationBarTitleText": "上模",
"navigationStyle": "custom"
}
},
@ -589,6 +589,27 @@
"navigationStyle": "custom"
}
},
{
"path": "moldoperate/dismount",
"style": {
"navigationBarTitleText": "下模",
"navigationStyle": "custom"
}
},
{
"path": "moldoperate/deviceSelect",
"style": {
"navigationBarTitleText": "选择设备",
"navigationStyle": "custom"
}
},
{
"path": "moldoperate/moldSelect",
"style": {
"navigationBarTitleText": "选择待上模模具",
"navigationStyle": "custom"
}
},
{
"path": "moldRepair/index",
"style": {

@ -0,0 +1,366 @@
<template>
<view class="page-container">
<NavBar :title="t('moldOperate.selectDevice')" />
<!-- 搜索区 -->
<view class="search-bar">
<view class="search-input-wrap">
<text class="search-icon iconfont icon-search"></text>
<input class="search-input" v-model="searchText" :placeholder="t('equipmentLedger.placeholderDeviceName')" @input="onSearch" placeholder-class="search-placeholder" />
<text v-if="searchText" class="search-clear" @click="clearSearch"></text>
</view>
</view>
<!-- 设备列表 -->
<scroll-view scroll-y class="device-list" v-if="filteredList.length > 0">
<view
v-for="device in filteredList"
:key="device.id"
class="device-card"
:class="{ active: selectedId === device.id }"
@click="selectedId = device.id"
>
<view class="device-card-header">
<text class="device-name">{{ textValue(device.deviceName) }}</text>
<view class="device-status-tag" :class="getStatusClass(device.deviceStatus)">
{{ getStatusLabel(device) }}
</view>
</view>
<view class="device-card-body">
<view class="device-info-row">
<text class="info-label">{{ t('moldOperate.deviceCode') }}</text>
<text class="info-value">{{ textValue(device.deviceCode) }}</text>
</view>
<view class="device-info-row">
<text class="info-label">{{ t('moldOperate.productionLine') }}</text>
<text class="info-value">{{ textValue(device.workshopName) }}</text>
</view>
<view class="device-info-row">
<text class="info-label">{{ t('moldOperate.currentMold') }}</text>
<text class="info-value">{{ getCurrentMold(device) }}</text>
</view>
<view class="device-info-row">
<text class="info-label">{{ t('moldOperate.deviceStatus') }}</text>
<text class="info-value">{{ getStatusLabel(device) }}</text>
</view>
</view>
</view>
</scroll-view>
<!-- 空状态 -->
<view v-else class="empty-wrap">
<text v-if="loading" class="empty-text">{{ t('functionCommon.loading') }}</text>
<text v-else class="empty-text">{{ t('moldOperate.noDeviceData') }}</text>
</view>
<!-- 底部确认按钮 -->
<view class="bottom-actions">
<view class="bottom-btn confirm-btn" @click="handleConfirm">
{{ t('functionCommon.confirm') }}
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getDeviceLedgerList } from '@/api/mes/moldoperate'
const { t } = useI18n()
const deviceList = ref([])
const selectedId = ref(null)
const searchText = ref('')
const loading = ref(false)
function textValue(v) {
if (v === 0) return '0'
if (v == null) return '-'
const s = String(v).trim()
return s || '-'
}
function getStatusClass(status) {
const s = Number(status)
if (s === 0) return 'running-tag'
if (s === 1) return 'stop-tag'
if (s >= 2) return 'fault-tag'
return ''
}
function getStatusLabel(device) {
const status = Number(device.deviceStatus)
const map = {
0: t('moldOperate.statusRunning'),
1: t('moldOperate.statusStop'),
2: t('moldOperate.statusFault'),
3: t('moldOperate.statusFault')
}
return map[status] || textValue(device.deviceStatus) || '-'
}
// - fallback
function getCurrentMold(device) {
const deviceId = device?.id
const deviceCode = device?.deviceCode
// 1. _mountedMoldInfoMap deviceId deviceCode fallback
try {
const map = uni.getStorageSync('_mountedMoldInfoMap') || {}
// id
const key = String(deviceId)
let info = deviceId != null ? map[key] : null
// deviceCode id
if (!info && deviceCode) {
const found = Object.values(map).find((item) => item?.deviceCode === deviceCode)
if (found) info = found
}
//
if (!getCurrentMold._logged) {
getCurrentMold._logged = true
console.log('[deviceSelect] === 调试信息 ===')
console.log('[deviceSelect] storage map keys:', Object.keys(map))
console.log('[deviceSelect] storage map values:', map)
console.log('[deviceSelect] 当前设备 deviceId:', deviceId, 'type:', typeof deviceId, 'key:', key)
console.log('[deviceSelect] 当前设备 deviceCode:', deviceCode)
console.log('[deviceSelect] id匹配结果:', info ? '命中' : '未命中')
if (!info) {
console.log('[deviceSelect] 尝试 deviceCode fallback...')
for (const [k, v] of Object.entries(map)) {
console.log('[deviceSelect] storage[', k, '].deviceCode =', v?.deviceCode, 'vs 设备 code =', deviceCode, '匹配:', v?.deviceCode === deviceCode)
}
}
// fallback
const saved = uni.getStorageSync('_mountedMoldInfo') || null
console.log('[deviceSelect] 旧版 _mountedMoldInfo:', saved)
console.log('[deviceSelect] 设备台账字段 currentMold:', device?.currentMold, 'moldName:', device?.moldName, 'moldId:', device?.moldId)
}
if (info?.mold) {
return info.mold.name || info.mold.moldName || '-'
}
} catch (e) {
console.warn('[deviceSelect] read _mountedMoldInfoMap error', e)
}
// 2. _mountedMoldInfo
let saved = null
try { saved = uni.getStorageSync('_mountedMoldInfo') || null } catch {}
if (saved) {
const idMatch = deviceId != null && String(saved.deviceId) === String(deviceId)
const codeMatch = deviceCode && saved.deviceCode === deviceCode
if (idMatch || codeMatch) {
return saved.mold?.name || saved.mold?.moldName || '-'
}
}
// 3. fallback
const staticMold = textValue(device.currentMold || device.moldName || device.moldId)
return staticMold === '-' ? t('moldOperate.noMoldOnDevice') : staticMold
}
const filteredList = computed(() => {
const keyword = searchText.value.trim().toLowerCase()
if (!keyword) return deviceList.value
return deviceList.value.filter((d) => {
return (d.deviceName || '').toLowerCase().includes(keyword) ||
(d.deviceCode || '').toLowerCase().includes(keyword) ||
(d.workshopName || '').toLowerCase().includes(keyword)
})
})
function onSearch() {}
function clearSearch() {
searchText.value = ''
}
async function loadDevices() {
loading.value = true
try {
const res = await getDeviceLedgerList({ pageNo: 1, pageSize: 100 })
const root = res && res.data !== undefined ? res.data : res
deviceList.value = Array.isArray(root)
? root
: Array.isArray(root?.list) ? root.list
: Array.isArray(root?.rows) ? root.rows
: Array.isArray(root?.records) ? root.records
: []
} catch (e) {
console.error('loadDevices error', e)
} finally {
loading.value = false
}
}
function handleConfirm() {
if (!selectedId.value) {
uni.showToast({ title: t('moldOperate.validatorDeviceRequired'), icon: 'none' })
return
}
const device = deviceList.value.find((d) => d.id === selectedId.value)
// globalData onShow
getApp().globalData._deviceSelectResult = device || null
uni.navigateBack()
}
onShow(async () => {
await loadDevices()
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: #f5f6f8;
padding-bottom: 140rpx;
}
/* 搜索栏 */
.search-bar {
padding: 16rpx 24rpx;
background: #fff;
}
.search-input-wrap {
display: flex;
align-items: center;
height: 72rpx;
padding: 0 16rpx;
background: #f5f6f8;
border-radius: 36rpx;
}
.search-icon {
font-size: 32rpx;
color: #999;
margin-right: 12rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #333;
}
.search-placeholder {
color: #bbb;
}
.search-clear {
font-size: 28rpx;
color: #999;
padding: 8rpx;
}
/* 设备列表 */
.device-list {
padding: 16rpx 24rpx;
}
.device-card {
background: #fff;
border-radius: 14rpx;
padding: 24rpx;
margin-bottom: 16rpx;
border: 2rpx solid transparent;
&.active {
border-color: #2563eb;
background: #f0f5ff;
}
}
.device-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.device-name {
font-size: 30rpx;
font-weight: 700;
color: #1a1a1a;
}
.device-status-tag {
padding: 4rpx 16rpx;
border-radius: 6rpx;
font-size: 22rpx;
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; }
}
.device-card-body {
border-top: 1rpx solid #f0f0f0;
padding-top: 16rpx;
}
.device-info-row {
display: flex;
align-items: center;
margin-bottom: 10rpx;
&:last-child {
margin-bottom: 0;
}
}
.info-label {
width: 140rpx;
font-size: 24rpx;
color: #999;
flex-shrink: 0;
}
.info-value {
font-size: 26rpx;
color: #333;
}
/* 空状态 */
.empty-wrap {
padding: 120rpx 0;
text-align: center;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
/* 底部确认按钮 */
.bottom-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 99;
}
.bottom-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 12rpx;
font-size: 30rpx;
font-weight: 600;
}
.confirm-btn {
background: #2563eb;
color: #fff;
}
</style>

@ -0,0 +1,733 @@
<template>
<view class="page-container">
<NavBar :title="t('moldOperate.tabDown')" />
<!-- 操作按钮区 -->
<view class="action-row">
<view class="action-btn scan-btn" @click="handleScan">
<view class="btn-icon-wrap">
<text class="iconfont icon-scan btn-icon"></text>
</view>
<text class="btn-text">{{ t('moldOperate.scanDevice') }}</text>
</view>
<view class="action-btn select-btn" @click="openDevicePicker">
<view class="btn-icon-wrap">
<text class="iconfont icon-device btn-icon"></text>
</view>
<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.deviceInfo') }}</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.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.currentMoldInfo') }}</text>
</view>
<template v-if="selectedMold && selectedMold.id">
<view 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(selectedMold.moldName) }}</text>
</view>
<view class="grid-cell">
<text class="grid-label">{{ t('moldOperate.moldCode') }}</text>
<text class="grid-value">{{ textValue(selectedMold.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(selectedMold.productName) }}</text>
</view>
<view class="grid-cell">
<text class="grid-label">{{ t('moldOperate.mountTime') }}</text>
<text class="grid-value">{{ textValue(selectedMold.mountTime) }}</text>
</view>
</view>
<view class="grid-row">
<view class="grid-cell full-width">
<text class="grid-label">{{ t('moldOperate.useCount') }}</text>
<text class="grid-value highlight">{{ formatUseCount(selectedMold.useCount) }}</text>
</view>
</view>
</view>
<!-- 更换下模对象按钮 -->
<view class="change-target-btn" @click="openLowerMoldPicker">
<text class="change-icon">&#8644;</text>
<text class="change-text">{{ t('moldOperate.changeTarget') }}</text>
</view>
</template>
<!-- 已选设备但无在机模具 -->
<view v-else-if="selectedDevice.id && moldsLoaded" class="empty-mold-hint">
<text class="empty-mold-text empty-gray">{{ t('moldOperate.noMoldOnDevice') }}</text>
</view>
<!-- 未选设备 -->
<view v-else class="empty-mold-hint" @click="handleSelectDeviceFirst">
<text class="empty-mold-text">+ {{ t('moldOperate.clickSelectDeviceFirst') }}</text>
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-actions">
<view class="bottom-btn confirm-btn" @click="handleConfirm">{{ t('moldOperate.confirmDismount') }}</view>
</view>
<!-- 在机模具选择弹框更换下模对象 -->
<uni-popup ref="lowerMoldPickerRef" type="bottom">
<view class="picker-popup">
<view class="picker-header">
<text class="picker-close-left" @click="closeLowerMoldPicker">{{ t('functionCommon.cancel') }}</text>
<text class="picker-title">{{ t('moldOperate.currentMoldInfo') }}</text>
<text class="picker-close-right" @click="confirmLowerMoldSelection">{{ t('functionCommon.confirm') }}</text>
</view>
<scroll-view scroll-y class="picker-scroll">
<view v-for="m in lowerMoldOptions" :key="m.id || m.moldId"
class="picker-item" :class="{ active: tempSelectedMoldId === String(m.id || m.moldId) }"
@click="tempSelectedMoldId = String(m.id || m.moldId)">
<view class="picker-item-content">
<text class="picker-item-name">{{ textValue(m.moldName || m.name) }}</text>
<text class="picker-item-code">{{ textValue(m.moldCode || m.code) }}</text>
</view>
<text class="picker-check-icon" v-if="tempSelectedMoldId === String(m.id || m.moldId)">&#10003;</text>
</view>
<view v-if="lowerMoldLoading" class="picker-hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!lowerMoldOptions.length" class="picker-empty">{{ t('moldOperate.noMoldOnDevice') }}</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getLowerMoldList, getDeviceLedgerList, createMoldOperate } from '@/api/mes/moldoperate'
const { t } = useI18n()
// ---- ----
const selectedDevice = ref({})
const deviceOptions = ref([])
// ---- ----
const selectedMold = ref({})
const lowerMoldPickerRef = ref(null)
const lowerMoldOptions = ref([])
const lowerMoldLoading = ref(false)
const moldsLoaded = ref(false)
const tempSelectedMoldId = ref(null)
// ---- ----
function textValue(v) {
if (v === 0) return '0'
if (v == null) return '-'
const s = String(v).trim()
return s || '-'
}
function formatUseCount(count) {
if (count == null) return '-'
const num = Number(count)
if (isNaN(num)) return String(count)
return num.toLocaleString() + ' ' + t('moldOperate.countUnit')
}
function formatTime(time) {
if (!time) return '-'
if (typeof time === 'string') {
if (time.includes('-') || time.includes('T')) {
return time.replace(/T/, ' ').substring(0, 19)
}
const ts = Number(time)
if (!isNaN(ts) && ts > 1e10) return new Date(ts).toLocaleString()
else if (!isNaN(ts) && ts > 1e7) return new Date(ts * 1000).toLocaleString()
return time
}
if (typeof time === 'number') {
if (time > 1e10) return new Date(time).toLocaleString()
if (time > 1e7) return new Date(time * 1000).toLocaleString()
return String(time)
}
return '-'
}
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) || '-'
})
// ---- ----
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
: []
deviceOptions.value = data.map((d) => ({
value: d.id,
label: `${d.deviceCode || ''} ${d.deviceName || ''}`.trim(),
raw: d
}))
} catch (e) {
console.error('loadDevices error', e)
}
}
//
function findArr(obj, d = 0) {
if (!obj || typeof obj !== 'object' || d > 3) return []
if (Array.isArray(obj)) return obj
for (const k of ['list', 'rows', 'records', 'items', 'data']) {
if (Array.isArray(obj[k])) return obj[k]
}
for (const v of Object.values(obj)) {
const found = findArr(v, d + 1)
if (found.length) return found
}
return []
}
async function loadLowerMolds(deviceId) {
lowerMoldLoading.value = true
try {
// ====== 1ID======
let map = {}
try { map = uni.getStorageSync('_mountedMoldInfoMap') || {} } catch {}
const savedInfo = map[String(deviceId)] || null
console.log('[下模] 持久化在机模具信息 =', savedInfo ? JSON.stringify(savedInfo) : 'null', 'deviceId=', deviceId)
if (savedInfo && savedInfo.mold) {
console.log('[下模] 命中持久化数据, deviceId=', deviceId)
const m = savedInfo.mold
lowerMoldOptions.value = [{
id: m.id,
moldId: m.id,
moldName: m.name || m.moldName || '-',
moldCode: m.code || m.moldCode || '-',
productName: m.productName || '-',
mountTime: formatTime(savedInfo.mountTime),
useCount: '-'
}]
console.log('[下模] 使用持久化数据 =', JSON.stringify(lowerMoldOptions.value[0]))
return
}
console.log('[下模] 未命中持久化数据,进入接口查询')
// ====== 2fallback - ======
let list = []
try {
const res = await getLowerMoldList(deviceId)
const root = (res && res.data !== undefined) ? res.data : res
list = findArr(root)
} catch (apiErr) {
console.warn('[下模] getLowerMoldList 接口异常', apiErr)
}
lowerMoldOptions.value = list
console.log('[下模] loadLowerMolds 返回 list.length =', list.length)
} catch (e) {
console.error('loadLowerMolds error', e)
} finally {
lowerMoldLoading.value = false
}
}
// ---- ----
function handleScan() {
uni.scanCode({
onlyFromCamera: false,
scanType: ['qrCode', 'barCode'],
success(res) {
const code = res.result?.trim()
if (!code) return
const matched = deviceOptions.value.find((d) =>
d.raw.deviceCode === code || String(d.raw.code) === code || d.label.includes(code)
)
if (matched) {
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 selectDevice(device) {
selectedDevice.value = device || {}
moldsLoaded.value = false
selectedMold.value = {}
if (device?.id) {
loadLowerMolds(device.id).then(() => {
moldsLoaded.value = true
if (lowerMoldOptions.value.length > 0) {
const first = lowerMoldOptions.value[0]
selectedMold.value = {
id: first.id || first.moldId,
moldName: first.moldName || first.name,
moldCode: first.moldCode || first.code,
productName: first.productName || '-',
mountTime: first.mountTime || '-',
useCount: first.useCount ?? '-'
}
}
})
}
}
function openDevicePicker() {
uni.navigateTo({ url: '/pages_function/pages/moldoperate/deviceSelect' })
}
// ---- ----
function openLowerMoldPicker() {
if (!selectedDevice.value?.id) {
uni.showToast({ title: t('moldOperate.validatorDeviceRequired'), icon: 'none' })
return
}
tempSelectedMoldId.value = selectedMold.value ? String(selectedMold.value.id || selectedMold.value.moldId) : null
loadLowerMolds(selectedDevice.value.id).then(() => {
lowerMoldPickerRef.value?.open()
})
}
function confirmLowerMoldSelection() {
const mold = lowerMoldOptions.value.find(
(m) => String(m.id || m.moldId) === tempSelectedMoldId.value
)
if (mold) {
selectedMold.value = {
id: mold.id || mold.moldId,
moldName: mold.moldName || mold.name,
moldCode: mold.moldCode || mold.code,
productName: mold.productName || '-',
mountTime: mold.mountTime || '-',
useCount: mold.useCount ?? '-'
}
}
lowerMoldPickerRef.value?.close()
}
function closeLowerMoldPicker() { lowerMoldPickerRef.value?.close() }
function handleSelectDeviceFirst() {
openDevicePicker()
}
// ---- ----
function validForm() {
if (!selectedDevice.value?.id) {
uni.showToast({ title: t('moldOperate.validatorDeviceRequired'), icon: 'none' })
return false
}
if (!selectedMold.value?.id) {
uni.showToast({ title: t('moldOperate.validatorLowerMoldRequired'), icon: 'none' })
return false
}
return true
}
async function handleConfirm() {
if (!validForm()) return
try {
const payload = {
operateType: '2',
deviceId: String(selectedDevice.value.id),
moldId: String(selectedMold.value.id)
}
console.log('[下模] 提交参数 =', JSON.stringify(payload))
await createMoldOperate(payload)
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
//
if (selectedDevice.value?.id) {
try {
const map = uni.getStorageSync('_mountedMoldInfoMap') || {}
delete map[String(selectedDevice.value.id)]
uni.setStorageSync('_mountedMoldInfoMap', map)
} catch {}
}
//
selectedDevice.value = {}
selectedMold.value = {}
lowerMoldOptions.value = []
} catch (e) {
console.error('[下模] 保存失败 =', e)
const errMsg = e?.msg || (typeof e === 'string' ? e : e?.message) || '系统异常'
uni.showToast({ title: t('functionCommon.saveFailed') + ': ' + errMsg, icon: 'none', duration: 3000 })
}
}
onShow(async () => {
await Promise.allSettled([loadDevices()])
// globalData
const device = getApp().globalData._deviceSelectResult
if (device) {
getApp().globalData._deviceSelectResult = null
selectDevice(device)
}
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: #f5f6f8;
padding-bottom: 140rpx;
}
/* ====== 操作按钮区 ====== */
.action-row {
display: flex;
gap: 20rpx;
padding: 20rpx 24rpx;
}
.action-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
height: 96rpx;
border-radius: 12rpx;
background: #2563eb;
color: #fff;
font-size: 28rpx;
font-weight: 600;
.btn-icon-wrap {
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon {
font-size: 36rpx;
color: #fff;
}
.btn-text {
font-size: 28rpx;
}
}
/* ====== 卡片通用样式 ====== */
.section-card {
margin: 16rpx 24rpx;
background: #ffffff;
border-radius: 14rpx;
overflow: hidden;
}
.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;
}
&.pending-tag {
color: #2563eb;
background: #eff6ff;
border: 1rpx solid #bfdbfe;
}
}
/* ====== 更换下模对象按钮 ====== */
.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;
.change-icon {
font-size: 28rpx;
color: #666;
}
.change-text {
font-size: 26rpx;
color: #666;
}
&:active {
background: #fafafa;
}
}
/* ====== 空提示 ====== */
.empty-mold-hint {
padding: 40rpx 24rpx;
text-align: center;
}
.empty-mold-text {
font-size: 28rpx;
color: #2563eb;
}
/* ====== 底部操作栏 ====== */
.bottom-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 99;
}
.bottom-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 12rpx;
font-size: 30rpx;
font-weight: 600;
}
.confirm-btn {
background: #2563eb;
color: #fff;
}
/* ====== 弹窗通用 ====== */
.picker-popup {
background: #fff;
border-radius: 24rpx 24rpx 0 0;
max-height: 75vh;
}
.picker-header {
height: 96rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.picker-close-left,
.picker-close-right {
font-size: 28rpx;
color: #2563eb;
padding: 8rpx 0;
}
.picker-title {
font-size: 30rpx;
font-weight: 700;
color: #1a1a1a;
}
.picker-scroll {
max-height: calc(75vh - 96rpx);
padding: 12rpx 0 32rpx;
}
.picker-item {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 24rpx 12rpx;
padding: 22rpx 20rpx;
border-radius: 12rpx;
background: #f8fafc;
&.active {
background: #eff6ff;
border: 1rpx solid #bfdbfe;
}
}
.picker-item-content {
flex: 1;
min-width: 0;
}
.picker-item-name {
display: block;
font-size: 28rpx;
color: #1a1a1a;
font-weight: 500;
}
.picker-item-code {
display: block;
font-size: 23rpx;
color: #999;
margin-top: 4rpx;
}
.picker-check-icon {
font-size: 30rpx;
color: #2563eb;
font-weight: 700;
margin-left: 16rpx;
flex-shrink: 0;
}
.picker-empty,
.picker-hint {
text-align: center;
color: #999;
padding: 60rpx 0;
font-size: 26rpx;
}
</style>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,351 @@
<template>
<view class="page-container">
<NavBar :title="t('moldOperate.selectMountMold')" />
<!-- 搜索区 -->
<view class="search-bar">
<view class="search-input-wrap">
<text class="search-icon iconfont icon-search"></text>
<input class="search-input" v-model="searchText" :placeholder="t('moldOperate.searchName')" @input="onSearch" placeholder-class="search-placeholder" />
<text v-if="searchText" class="search-clear" @click="clearSearch"></text>
</view>
</view>
<!-- 模具列表 -->
<scroll-view scroll-y class="mold-list" v-if="filteredList.length > 0">
<view
v-for="mold in filteredList"
:key="mold.id"
class="mold-card"
:class="{ active: selectedIds.has(String(mold.id)) }"
@click="toggleMold(mold)"
>
<view class="mold-card-header">
<text class="mold-name">{{ textValue(mold.name) }}</text>
<text class="mold-check-icon" v-if="selectedIds.has(String(mold.id))">&#10003;</text>
</view>
<view class="mold-card-body">
<view class="info-row">
<text class="info-label">{{ t('moldOperate.moldCode') }}</text>
<text class="info-value">{{ textValue(mold.code) }}</text>
</view>
<view class="info-row">
<text class="info-label">产品型号</text>
<text class="info-value">{{ textValue(mold.productName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldOperate.status') }}</text>
<view :class="['status-tag', getStatusClass(mold.status)]">{{ getStatusText(mold.status) }}</view>
</view>
</view>
</view>
</scroll-view>
<!-- 空状态 -->
<view v-else class="empty-wrap">
<text v-if="loading" class="empty-text">{{ t('functionCommon.loading') }}</text>
<text v-else class="empty-text">{{ t('moldOperate.noMoldData') }}</text>
</view>
<!-- 底部确认按钮 -->
<view class="bottom-actions">
<view class="bottom-btn confirm-btn" @click="handleConfirm">
{{ t('functionCommon.confirm') }}{{ selectedIds.size > 0 ? `(${selectedIds.size})` : '' }}
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getMoldBrandPage } from '@/api/mes/mold'
const { t } = useI18n()
const moldList = ref([])
const selectedIds = ref(new Set())
const searchText = ref('')
const loading = ref(false)
function textValue(v) {
if (v === 0) return '0'
if (v == null) return '-'
const s = String(v).trim()
return s || '-'
}
const STATUS_MAP = { 0: '在机', 1: '待用', 2: '维修', 3: '报废' }
function getStatusText(s) { return STATUS_MAP[s] || textValue(s) }
function getStatusClass(s) {
if (s === 0) return 'in-use-tag'
if (s === 2) return 'repairing-tag'
if (s === 3) return 'scrapped-tag'
return 'standby-tag'
}
const filteredList = computed(() => {
const keyword = searchText.value.trim().toLowerCase()
if (!keyword) return moldList.value
return moldList.value.filter((m) => {
return (m.name || '').toLowerCase().includes(keyword) ||
(m.code || '').toLowerCase().includes(keyword) ||
(m.productName || '').toLowerCase().includes(keyword)
})
})
function onSearch() {}
function clearSearch() {
searchText.value = ''
}
async function loadMolds() {
loading.value = true
try {
const res = await getMoldBrandPage({ pageNo: 1, pageSize: 100 })
const root = (res && res.data !== undefined) ? res.data : res
// list/rows/records
function findList(obj, depth = 0) {
if (!obj || typeof obj !== 'object' || depth > 3) return []
if (Array.isArray(obj)) return obj
for (const key of ['list', 'rows', 'records', 'items', 'data']) {
if (Array.isArray(obj[key])) return obj[key]
}
for (const val of Object.values(obj)) {
const found = findList(val, depth + 1)
if (found.length) return found
}
return []
}
moldList.value = findList(root)
//
if (!moldList.value.length) console.error('moldSelect: 未找到模具数据', root)
} catch (e) {
console.error('loadMolds error', e)
} finally {
loading.value = false
}
}
function toggleMold(mold) {
const id = String(mold.id)
const set = new Set(selectedIds.value)
if (set.has(id)) {
set.delete(id)
} else {
//
set.clear()
set.add(id)
}
selectedIds.value = set
}
function handleConfirm() {
if (selectedIds.value.size === 0) {
uni.showToast({ title: t('moldOperate.validatorMoldRequired'), icon: 'none' })
return
}
const selected = moldList.value.filter((m) => selectedIds.value.has(String(m.id)))
.map((m) => ({
id: m.id,
name: m.name,
code: m.code,
productName: m.productName || '-',
status: m.status
}))
getApp().globalData._moldSelectResult = selected
uni.navigateBack()
}
onShow(async () => {
// index.vue
const preSelected = getApp().globalData._moldSelectPreSelected
if (preSelected && Array.isArray(preSelected)) {
selectedIds.value = new Set(preSelected.map((id) => String(id)))
getApp().globalData._moldSelectPreSelected = null
} else {
selectedIds.value = new Set()
}
await loadMolds()
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: #f5f6f8;
padding-bottom: 140rpx;
}
/* 搜索栏 */
.search-bar {
padding: 16rpx 24rpx;
background: #fff;
}
.search-input-wrap {
display: flex;
align-items: center;
height: 72rpx;
padding: 0 16rpx;
background: #f5f6f8;
border-radius: 36rpx;
}
.search-icon {
font-size: 32rpx;
color: #999;
margin-right: 12rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #333;
}
.search-placeholder {
color: #bbb;
}
.search-clear {
font-size: 28rpx;
color: #999;
padding: 8rpx;
}
/* 模具列表 */
.mold-list {
padding: 16rpx 24rpx;
}
.mold-card {
background: #fff;
border-radius: 14rpx;
padding: 24rpx;
margin-bottom: 16rpx;
border: 2rpx solid transparent;
&.active {
border-color: #2563eb;
background: #f0f5ff;
}
}
.mold-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.mold-name {
font-size: 30rpx;
font-weight: 700;
color: #1a1a1a;
}
.mold-check-icon {
font-size: 32rpx;
color: #2563eb;
font-weight: 700;
flex-shrink: 0;
width: 40rpx;
height: 40rpx;
line-height: 36rpx;
text-align: center;
border: 2rpx solid #2563eb;
border-radius: 50%;
}
.mold-card-body {
border-top: 1rpx solid #f0f0f0;
padding-top: 16rpx;
}
.info-row {
display: flex;
align-items: center;
margin-bottom: 10rpx;
&:last-child {
margin-bottom: 0;
}
}
.info-label {
width: 140rpx;
font-size: 24rpx;
color: #999;
flex-shrink: 0;
}
.info-value {
font-size: 26rpx;
color: #333;
}
/* 状态标签 */
.status-tag {
display: inline-flex;
align-items: center;
padding: 4rpx 16rpx;
border-radius: 6rpx;
font-size: 22rpx;
font-weight: 500;
&.standby-tag {
color: #2563eb; background: #eff6ff; border: 1rpx solid #bfdbfe;
}
&.in-use-tag {
color: #059669; background: #ecfdf5; border: 1rpx solid #a7f3d0;
}
&.repairing-tag {
color: #d97706; background: #fffbeb; border: 1rpx solid #fde68a;
}
&.scrapped-tag {
color: #dc2626; background: #fef2f2; border: 1rpx solid #fecaca;
}
}
/* 空状态 */
.empty-wrap {
padding: 120rpx 0;
text-align: center;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
/* 底部确认按钮 */
.bottom-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 99;
}
.bottom-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 12rpx;
font-size: 30rpx;
font-weight: 600;
}
.confirm-btn {
background: #2563eb;
color: #fff;
}
</style>

@ -73,6 +73,8 @@ const MENU_ROUTE_MAP = {
moldget: '/pages_function/pages/moldget/index',
moldreturn: '/pages_function/pages/moldreturn/index',
moldoperate: '/pages_function/pages/moldoperate/index',
moldmount: '/pages_function/pages/moldoperate/index',
molddismount: '/pages_function/pages/moldoperate/dismount',
mold: '/pages_function/pages/mold/index',
equipment: '/pages_function/pages/equipment/index',
spare: '/pages_function/pages/spare/index',
@ -228,6 +230,32 @@ export function findTabMenuByPage(menus, pagePath) {
return dynamicMenus.find((menu) => normalizePagePath(resolveMenuUrl(menu)) === targetPath) || null
}
// 上下模拆分映射
const MOLD_OPERATE_SPLIT_MAP = {
'上下模': [
{ name: '上模', key: 'moldmount' },
{ name: '下模', key: 'molddismount' }
],
moldoperate: [
{ name: '上模', key: 'moldmount' },
{ name: '下模', key: 'molddismount' }
]
}
function splitMoldOperateEntry(entry) {
const keys = [String(entry.name || '').trim(), normalizeMenuKey(entry.component || ''), normalizeMenuKey(entry.enName || '')]
for (const k of keys) {
if (MOLD_OPERATE_SPLIT_MAP[k]) {
return MOLD_OPERATE_SPLIT_MAP[k].map((item) => ({
...entry,
name: item.name,
_splitKey: item.key
}))
}
}
return null
}
export function buildPageModules(tabMenu) {
return toArray(tabMenu?.children)
.map((module) => {
@ -236,12 +264,18 @@ export function buildPageModules(tabMenu) {
if (hasNestedEntries) {
return {
...module,
children: directChildren.flatMap((child) => toArray(child.children))
children: directChildren.flatMap((child) => toArray(child.children).flatMap((entry) => {
const splitted = splitMoldOperateEntry(entry)
return splitted || [entry]
}))
}
}
return {
...module,
children: directChildren
children: directChildren.flatMap((entry) => {
const splitted = splitMoldOperateEntry(entry)
return splitted || [entry]
})
}
})
.filter((module) => (module.children || []).length > 0)
@ -252,6 +286,11 @@ export function resolveMenuUrl(menu) {
return '/pages/index'
}
// 拆分后的上下模子项,优先用 _splitKey 匹配(必须在 directRoute 之前)
if (menu?._splitKey && MENU_ROUTE_MAP[menu._splitKey]) {
return MENU_ROUTE_MAP[menu._splitKey]
}
const directRoute = getDirectRoute(menu?.path) || getDirectRoute(menu?.component)
if (directRoute) {
return directRoute

@ -21,6 +21,10 @@ const request = <T>(config: RequestConfig): Promise<ResponseData<T>> => {
if (getToken() && !isToken) {
config.header['Authorization'] = 'Bearer ' + getToken()
}
// POST/PUT 请求明确设置 Content-Type 为 application/json
if ((config.method === 'POST' || config.method === 'PUT') && config.data) {
config.header['Content-Type'] = 'application/json'
}
// get请求映射params参数
if (config.params) {
let url = config.url + '?' + tansParams(config.params)
@ -65,7 +69,8 @@ const request = <T>(config: RequestConfig): Promise<ResponseData<T>> => {
return
} else if (code === 500) {
toast(msg)
reject('500')
// 将完整的 data 对象 reject便于调用方拿到后端错误信息
reject(data)
return
} else if (code !== 200) {
toast(msg)

Loading…
Cancel
Save