ck-chenkang 6 days ago
commit bac605c5ab

@ -61,3 +61,12 @@ export function getSimpleUserList() {
method: 'get'
})
}
// 仓库库区列表根据仓库ID获取
export function getWarehouseAreaSimpleList(warehouseId) {
return request({
url: '/admin-api/erp/warehouse-area/simple-list',
method: 'get',
params: { warehouseId }
})
}

@ -0,0 +1,22 @@
import request from '@/utils/request'
// 备件列表(分页查询全部产品,前端按 categoryType=5 过滤备件)
export function getSparepartSimpleList(pageNo = 1) {
return request({
url: '/admin-api/erp/product/page',
method: 'get',
params: {
pageNo,
pageSize: 100
}
})
}
// 备件详情(含供应商、图片等完整信息)
export function getSparepartDetail(id) {
return request({
url: '/admin-api/erp/product/get',
method: 'get',
params: { id }
})
}

@ -0,0 +1,36 @@
import request from '@/utils/request'
// 备件入库单创建
export function createSparepartInbound(data) {
return request({
url: '/admin-api/erp/stock-in/create',
method: 'post',
data: { ...data, inType: '备件入库' }
})
}
// 备件入库单分页查询(待入库/待审核)
export function getSparepartInboundPage(params = {}) {
return request({
url: '/admin-api/erp/stock-in/page',
method: 'get',
params: { ...params, inType: '备件入库' }
})
}
// 备件入库单审核status: 20=通过, 1=驳回)
export function auditSparepartInbound(data) {
return request({
url: '/admin-api/erp/stock-in/audit',
method: 'put',
data
})
}
// 备件入库单删除
export function deleteSparepartInbound(id) {
return request({
url: '/admin-api/erp/stock-in/delete',
method: 'delete',
params: { ids: String(id) }
})
}

@ -0,0 +1,28 @@
import request from '@/utils/request'
// 备件出库单分页查询
export function getSparepartOutboundPage(params = {}) {
return request({
url: '/admin-api/erp/stock-out/page',
method: 'get',
params: { ...params, outType: '备件出库' }
})
}
// 备件出库单审核status: 20=通过, 1=驳回)
export function auditSparepartOutbound(data) {
return request({
url: '/admin-api/erp/stock-out/audit',
method: 'put',
data
})
}
// 备件出库单删除
export function deleteSparepartOutbound(id) {
return request({
url: '/admin-api/erp/stock-out/delete',
method: 'delete',
params: { ids: String(id) }
})
}

@ -22,10 +22,11 @@ export function getUserProfile() {
})
}
export function getSimpleUserList() {
export function getSimpleUserList(params) {
return request({
url: '/admin-api/system/user/simple-list',
method: 'get'
method: 'get',
params
})
}

@ -37,7 +37,7 @@
<!-- <view class="module-icon" :style="{ background: getModuleColor(moduleIndex) }">
<text class="icon-text">{{ getMenuSymbol(module.name, moduleIndex) }}</text>
</view> -->
<text class="module-title">{{ translateLiteral(module.name) }}</text>
<text class="module-title">{{ getDisplayName(module) }}</text>
</view>
<view class="function-grid">
@ -60,9 +60,9 @@
size="24"
:color="getModuleColor(moduleIndex)"
></u-icon>
<text v-else class="icon-inner">{{ getMenuSymbol(entry.name, entryIndex) }}</text>
<text v-else class="icon-inner">{{ getMenuSymbol(getDisplayName(entry), entryIndex) }}</text>
</view>
<text class="function-name">{{ translateLiteral(entry.name) }}</text>
<text class="function-name">{{ getDisplayName(entry) }}</text>
</view>
</view>
</view>
@ -77,10 +77,10 @@
<script setup>
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import AppEmptyState from '@/components/common/AppEmptyState.vue'
import { translateLiteral } from '@/locales'
import useUserStore from '@/store/modules/user'
import { buildPageModules, findTabMenuByPage, getMenuSymbol, getModuleColor, resolveMenuUrl } from '@/utils/permissionMenu'
import { buildPageModules, findTabMenuByPage, getLocalizedMenuName, getMenuSymbol, getModuleColor, resolveMenuUrl } from '@/utils/permissionMenu'
const props = defineProps({
pagePath: {
@ -118,6 +118,7 @@ const props = defineProps({
})
const userStore = useUserStore()
const { locale } = useI18n()
const menuSearchKeyword = ref('')
const scrollTop = ref(0)
const currentScrollTop = ref(0)
@ -138,11 +139,11 @@ const filteredModules = computed(() => {
.map((module) => ({
...module,
children: (module.children || []).filter((entry) => {
const target = `${module.name}|${entry.name}`.toLowerCase()
const target = `${getDisplayName(module)}|${getDisplayName(entry)}`.toLowerCase()
return target.includes(keyword)
})
}))
.filter((module) => (module.children || []).length > 0 || String(module.name || '').toLowerCase().includes(keyword))
.filter((module) => (module.children || []).length > 0 || getDisplayName(module).toLowerCase().includes(keyword))
})
const hasMenuPermission = computed(() => modules.value.length > 0)
@ -209,16 +210,22 @@ function getUviewIconName(icon) {
return String(icon || '').replace(/^uview-plus:/, '').trim()
}
function getDisplayName(menu) {
locale.value
return getLocalizedMenuName(menu)
}
function handleClick(menu) {
const url = resolveMenuUrl(menu)
console.log('[PermissionMenu] 点击菜单:', menu?.name, '| _splitKey:', menu?._splitKey, '| 路由:', url)
const displayName = getDisplayName(menu)
console.log('[PermissionMenu] 点击菜单:', displayName, '| _splitKey:', menu?._splitKey, '| 路由:', url)
if (url) {
uni.navigateTo({ url })
return
}
uni.showToast({
title: `暂未配置${menu?.name || '该菜单'}页面`,
title: `暂未配置${displayName || '该菜单'}页面`,
icon: 'none'
})
}

@ -25,9 +25,8 @@
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import useUserStore from '@/store/modules/user'
import { storeToRefs } from 'pinia'
import { getTabBarMenus } from '@/utils/permissionMenu'
import { getLocalizedMenuName, getTabBarMenus } from '@/utils/permissionMenu'
import { useI18n } from 'vue-i18n'
import { translateLiteral } from '@/locales'
const TABBAR_VISIBILITY_EVENT = 'tabbar-visibility-change'
@ -87,7 +86,7 @@ function resolveTabIconMeta(menu) {
function createTabItem(menu) {
const iconMeta = resolveTabIconMeta(menu)
return {
text: translateLiteral(String(menu.name || menu.enName || '').trim()),
text: getLocalizedMenuName(menu),
icon: iconMeta.icon,
selectedIcon: iconMeta.selectedIcon,
iconType: iconMeta.iconType,

@ -20,6 +20,12 @@ export default {
work: 'Manage',
mine: 'Mine'
},
work:{
mold: '模具',
equipmentMaintenance: 'Equipment Maintenance',
keypart: '关键件',
spare: '备件',
},
nav: {
home: 'Home',
mine: 'Profile',
@ -194,6 +200,7 @@ export default {
loadFailed: 'Load failed',
deleteSuccess: 'Deleted successfully',
deleteFailed: 'Delete failed',
saveSuccess: 'Saved successfully',
saveFailed: 'Save failed',
createSuccess: 'Created successfully',
updateSuccess: 'Updated successfully',
@ -515,7 +522,7 @@ export default {
creatorName: 'Creator',
createTime: 'Created At',
updateTime: 'Updated At',
searchPlaceholder: 'Enter task name',
searchPlaceholder: 'Enter task name/device code/device name',
empty: 'No equipment inspection tasks',
createTicketSuccess: 'Work order created successfully',
createTicketFail: 'Work order creation failed',
@ -543,7 +550,7 @@ export default {
jobResultOk: 'Pass',
jobResultNg: 'Fail',
taskTime: 'Job Time',
searchPlaceholder: 'Enter task no.',
searchPlaceholder: 'Enter task no/device code/device name',
empty: 'No equipment inspection records',
progressTitle: 'Progress',
inspectionMethod: 'Inspection Method',
@ -850,7 +857,7 @@ export default {
confirmDeleteContent: 'Confirm delete category [{name}]?'
},
equipmentLedger: {
moduleName: 'Equipment Ledger',
moduleName: 'Equipment Ledger11',
subTitle: 'Equipment ledger management',
detailTitle: 'Equipment Detail',
basicInfo: 'Basic Info',
@ -955,7 +962,7 @@ export default {
autoCode: 'Auto Generate',
deviceLabel: 'Equipment',
reportTimeLabel: 'Report Time',
searchPlaceholder: 'Enter order no. or equipment code',
searchPlaceholder: 'Enter order no/device code/device name',
empty: 'No repair records',
statusPending: 'Pending Repair',
statusPassed: 'Passed',
@ -1326,6 +1333,7 @@ export default {
placeholderRequireDate: 'Select require date',
placeholderAcceptedBy: 'Select repair user',
placeholderConfirmBy: 'Select confirm user',
placeholderUserSearch: 'Search by nickname',
placeholderMold: 'Select mold',
placeholderMoldNameAuto: 'Auto filled',
placeholderMoldCodeAuto: 'Auto filled',
@ -1345,6 +1353,7 @@ export default {
moldNotFound: 'Mold not found',
scanFailed: 'Scan failed',
maxUploadCount: 'Max 9 images',
noUserData: 'No user data',
saving: 'Saving',
saveSuccess: 'Saved successfully',
submitSuccess: 'Submitted successfully',
@ -1358,6 +1367,7 @@ export default {
validatorFaultLevelRequired: 'Fault level is required',
validatorIsShutdownRequired: 'Is shutdown is required',
validatorFaultPhenomenonRequired: 'Fault phenomenon is required',
validatorUserRequired: 'Please select a user',
validatorRepairStatusRequired: 'Repair result is required',
validatorFinishDateRequired: 'Finish date is required',
validatorConfirmDateRequired: 'Confirm date is required'

@ -130,6 +130,7 @@ const literalMap = {
'模具点检': 'moldCheck.moduleName',
'点检详情': 'moldCheck.detailTitle',
'新增点检': 'moldCheck.addTitle',
'设备运维': 'work.equipmentMaintenance',
'模具保养': 'moldMaintain.moduleName',
'保养详情': 'moldMaintain.detailTitle',
'新增保养': 'moldMaintain.addTitle',

@ -20,6 +20,12 @@ export default {
work: '管理',
mine: '我的'
},
work:{
mold: '模具',
equipmentMaintenance: '设备运维',
keypart: '关键件',
spare: '备件',
},
nav: {
home: '首页',
mine: '个人中心',
@ -194,6 +200,7 @@ export default {
loadFailed: '加载失败',
deleteSuccess: '删除成功',
deleteFailed: '删除失败',
saveSuccess: '保存成功',
saveFailed: '保存失败',
createSuccess: '新增成功',
updateSuccess: '更新成功',
@ -349,8 +356,8 @@ export default {
productionLine: '所属产线',
currentMold: '当前在机模具',
deviceStatus: '设备状态',
statusRunning: '运行中',
statusStop: '已停止',
statusRunning: '正常',
statusStop: '停用',
statusFault: '故障',
selectMountMold: '选择待上模模具',
product: '产品',
@ -515,7 +522,7 @@ export default {
creatorName: '创建人',
createTime: '创建时间',
updateTime: '更新时间',
searchPlaceholder: '请输入任务名称',
searchPlaceholder: '请输入任务名称/设备编码/设备名称',
empty: '暂无设备点检任务数据',
createTicketSuccess: '工单创建成功',
createTicketFail: '工单创建失败',
@ -543,7 +550,7 @@ export default {
jobResultOk: '通过',
jobResultNg: '不通过',
taskTime: '作业时间',
searchPlaceholder: '请输入任务编号',
searchPlaceholder: '请输入任务编号/设备编码/设备名称',
empty: '暂无设备点检记录数据',
progressTitle: '执行进度',
inspectionMethod: '检验方式',
@ -749,7 +756,7 @@ export default {
barCode: '物料条码',
name: '物料名称',
category: '物料分类',
unit: '单位',
unit: '库存单位',
standard: '规格',
expiryDay: '保质期天数',
status: '状态',
@ -787,7 +794,7 @@ export default {
code: 'BOM编码',
version: '版本',
product: '产品',
unit: '单位',
unit: '库存单位',
yieldRate: '良品率',
isEnable: '是否启用',
enableYes: '是',
@ -958,7 +965,7 @@ export default {
autoCode: '自动生成',
deviceLabel: '设备',
reportTimeLabel: '报修时间',
searchPlaceholder: '请输入单号或设备编码',
searchPlaceholder: '请输入单号/设备编码/设备名称',
empty: '暂无维修记录',
statusPending: '待维修',
statusPassed: '通过',
@ -1329,6 +1336,7 @@ export default {
placeholderRequireDate: '请选择报修日期',
placeholderAcceptedBy: '请选择维修人员',
placeholderConfirmBy: '请选择验收人员',
placeholderUserSearch: '请输入姓名搜索',
placeholderMold: '请选择模具',
placeholderMoldNameAuto: '自动带出',
placeholderMoldCodeAuto: '自动带出',
@ -1348,6 +1356,7 @@ export default {
moldNotFound: '未找到对应模具',
scanFailed: '扫码失败',
maxUploadCount: '最多上传 9 张图片',
noUserData: '暂无人员数据',
saving: '保存中',
saveSuccess: '保存成功',
submitSuccess: '提交成功',
@ -1361,8 +1370,70 @@ export default {
validatorFaultLevelRequired: '请选择故障等级',
validatorIsShutdownRequired: '请选择是否停机',
validatorFaultPhenomenonRequired: '请输入故障现象',
validatorUserRequired: '请选择人员',
validatorRepairStatusRequired: '请选择维修结果',
validatorFinishDateRequired: '请选择完成日期',
validatorConfirmDateRequired: '请选择验收日期'
},
sparepartInbound: {
moduleName: '备件入库',
tabPending: '待提交',
tabAuditing: '待审核',
searchPlaceholder: '搜索入库单号',
sparepartInfo: '备件信息',
inboundTime: '入库时间',
creator: '创建人',
quantity: '数量',
reviewer: '审核人',
approve: '已通过',
reject: '驳回',
confirmApprove: '确定审核通过该入库单吗?',
confirmReject: '确定驳回该入库单吗?',
approveSuccess: '审核通过',
rejectSuccess: '已驳回',
deleteSuccess: '删除成功',
empty: '暂无入库单据',
createTitle: '新增备件入库',
scanSparepart: '扫备件码',
selectSparepart: '选择备件',
searchSparepartPlaceholder: '搜索备件编码/名称',
sparepartCode: '备件编码',
category: '分类',
spec: '规格',
unit: '库存单位',
purchaseUnit: '采购单位',
convertRatio: '换算关系',
defaultWarehouse: '默认仓库/库区',
selectSparepart: '选择备件',
selectedSpareparts: '已选备件',
noSelectedSparepart: '请扫码或选择备件',
alreadySelected: '该备件已添加',
confirmRemove: '确定移除此备件吗?',
currentStock: '当前库存',
minStockUnit: '最小库存单位',
inboundQuantity: '入库数量',
qtyPlaceholder: '请输入',
noSparepartData: '暂无备件数据',
validatorSparepartRequired: '请选择备件'
},
sparepartOutbound: {
moduleName: '备件出库',
tabPending: '待提交',
tabAuditing: '待审核',
searchPlaceholder: '搜索出库单号',
sparepartInfo: '备件信息',
outboundTime: '出库时间',
creator: '创建人',
quantity: '数量',
reviewer: '审核人',
approve: '已通过',
reject: '驳回',
confirmApprove: '确定审核通过该出库单吗?',
confirmReject: '确定驳回该出库单吗?',
approveSuccess: '审核通过',
rejectSuccess: '已驳回',
deleteSuccess: '删除成功',
empty: '暂无出库单据',
createTitle: '新增备件出库'
}
}

@ -22,7 +22,9 @@
"delay" : 0
},
/* */
"modules" : {},
"modules" : {
"Camera" : {}
},
/* */
"distribute" : {
/* android */

@ -365,6 +365,44 @@
"navigationStyle": "custom"
}
},
{
"path": "sparepartInbound/index",
"style": {
"navigationBarTitleText": "备件入库",
"navigationStyle": "custom"
}
},
{
"path": "sparepartInbound/create",
"style": {
"navigationBarTitleText": "新增备件入库",
"navigationStyle": "custom"
}
},
{
"path": "sparepartInbound/sparepartSelect",
"style": {
"navigationBarTitleText": "选择备件",
"navigationStyle": "custom"
}
},
{
"path": "sparepartOutbound/index",
"style": {
"navigationBarTitleText": "备件出库",
"navigationStyle": "custom"
}
},
{
"path": "sparepartOutbound/create",
"style": {
"navigationBarTitleText": "新增备件出库",
"navigationStyle": "custom"
}
},
{
"path": "keypart/index",
"style": {
@ -631,6 +669,13 @@
"navigationStyle": "custom"
}
},
{
"path": "moldRepair/userSelect",
"style": {
"navigationBarTitleText": "选择人员",
"navigationStyle": "custom"
}
},
{
"path": "moldInspectionItems/index",
"style": {

@ -82,10 +82,10 @@
<view class="image-list">
<view v-for="(img, imgIndex) in parseImages(item.images)" :key="imgIndex" class="image-item">
<image
:src="img"
:src="encodeURI(img)"
class="result-image"
mode="aspectFill"
@click="previewImage(img, parseImages(item.images))"
@click="previewImage(encodeURI(img), parseImages(item.images).map(i => encodeURI(i)))"
/>
<view v-if="isEditableItem(item)" class="image-remove" @click="removeImage(item, imgIndex)">×</view>
</view>
@ -346,6 +346,7 @@ async function handleSave() {
try {
await batchUpdateMoldCheckResults(payload)
detailData.jobStatus = 1
uni.setStorageSync('moldCheckListNeedRefresh', '1')
uni.showToast({ title: t('functionCommon.saveSuccess'), icon: 'success' })
await fetchResults()
} catch (error) {

@ -82,12 +82,12 @@
<view class="image-list">
<view v-for="(img, imgIndex) in parseImages(item.images)" :key="imgIndex" class="image-item">
<image
:src="img"
:src="encodeURI(img)"
class="result-image"
mode="aspectFill"
@click="previewImage(img, parseImages(item.images))"
@click="previewImage(encodeURI(img), parseImages(item.images).map(i => encodeURI(i)))"
/>
<view v-if="isEditableItem(item)" class="image-remove" @click="removeImage(item, imgIndex)"></view>
<view v-if="isEditableItem(item)" class="image-remove" @click="removeImage(item, imgIndex)">×</view>
</view>
<view v-if="isEditableItem(item) && parseImages(item.images).length < 3" class="image-upload" @click="chooseImages(item)">
<text class="image-upload-icon">+</text>
@ -346,6 +346,7 @@ async function handleSave() {
try {
await batchUpdateMoldCheckResults(payload)
detailData.jobStatus = 1
uni.setStorageSync('moldMaintainListNeedRefresh', '1')
uni.showToast({ title: t('functionCommon.saveSuccess'), icon: 'success' })
await fetchResults()
} catch (error) {

@ -3,8 +3,7 @@
<NavBar :title="t('moldPressureNet.moduleName')">
<template #right>
<view class="nav-right-btn" @click="goHistory">
<uni-icons type="clock" size="22" color="#1f7cff"></uni-icons>
<text class="nav-right-text">{{ t('moldPressureNet.history') }}</text>
<uni-icons type="calendar" size="22" color="#22486e"></uni-icons>
</view>
</template>
</NavBar>
@ -83,7 +82,12 @@
</view>
<view class="form-field">
<text class="form-label">{{ t('moldPressureNet.pressureNetTime') }}<text class="required-star">*</text></text>
<uni-datetime-picker v-model="pressureNetTime" type="datetime" :clear-icon="false" />
<uni-datetime-picker
v-model="pressureNetTime"
type="datetime"
:clear-icon="false"
@change="onPressureNetTimeChange"
/>
</view>
<view class="form-field">
<text class="form-label">{{ t('moldPressureNet.remark') }}</text>
@ -317,6 +321,14 @@ function goHistory() {
uni.navigateTo({ url: '/pages_function/pages/moldPressureNet/history' })
}
function onPressureNetTimeChange(value) {
const normalizedValue = normalizePressureNetTime(value)
pressureNetTime.value = normalizedValue
setTimeout(() => {
pressureNetTime.value = normalizePressureNetTime(pressureNetTime.value || value) || normalizedValue
}, 0)
}
async function handleSubmit() {
if (submitLoading.value) return
if (!selectedBrand.id) {
@ -327,7 +339,8 @@ async function handleSubmit() {
uni.showToast({ title: t('moldPressureNet.selectSubMoldError'), icon: 'none' })
return
}
if (!pressureNetTime.value) {
const normalizedPressureNetTime = normalizePressureNetTime(pressureNetTime.value)
if (!normalizedPressureNetTime) {
uni.showToast({ title: t('moldPressureNet.selectReplaceTimeError'), icon: 'none' })
return
}
@ -342,12 +355,12 @@ async function handleSubmit() {
moldBrandName: selectedBrand.name,
moldId: moldId,
moldName: selected?.name || '',
pressureNetTime: pressureNetTime.value,
pressureNetTime: normalizedPressureNetTime,
remark: remark.value.trim() || undefined
}
})
await createPressureNetRecord(createReqVOList)
uni.showToast({ title: t('moldPressureNet.submitSuccess'), icon: 'success' })
uni.showToast({ title: t('functionCommon.saveSuccess'), icon: 'success' })
//
selectedBrand.id = ''
selectedBrand.name = ''
@ -362,12 +375,31 @@ async function handleSubmit() {
submitLoading.value = false
}
}
function normalizePressureNetTime(value) {
const text = String(value || '').trim().replace(/\//g, '-')
const dateOnlyMatch = text.match(/^(\d{4}-\d{2}-\d{2})(?:\s+(?:undefined|null|选择时间|select time))?$/i)
if (dateOnlyMatch) {
return `${dateOnlyMatch[1]} ${getCurrentTime()}`
}
const dateTimeMatch = text.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/)
if (!dateTimeMatch) return ''
const [, date, hour, minute, second = '00'] = dateTimeMatch
return `${date} ${hour}:${minute}:${second}`
}
function getCurrentTime() {
const date = new Date()
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
const second = String(date.getSeconds()).padStart(2, '0')
return `${hour}:${minute}:${second}`
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f5f7fb; }
.nav-right-btn { display: flex; align-items: center; gap: 6rpx; padding: 8rpx 18rpx; background: #ffffff; border-radius: 999rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.08); }
.nav-right-text { font-size: 24rpx; color: #374151; font-weight: 500; }
.nav-right-btn { width: 64rpx; height: 64rpx; display: flex; align-items: center; justify-content: center; background: #ffffff; border-radius: 50%; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.08); }
.detail-scroll { height: calc(100vh - 172rpx); }
.content-section { padding: 20rpx 24rpx 28rpx; }
.section-card { background: #ffffff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 20rpx; border: 1rpx solid #eef2f7; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.04); }

@ -37,16 +37,18 @@
<view class="form-item">
<text class="form-label">{{ t('moldRepair.acceptedBy') }}</text>
<picker :range="userLabels" :value="acceptedByIndex" :disabled="readonlyMeta" @change="(e) => onUserChange('acceptedBy', e)">
<view :class="['picker-field', !formData.acceptedBy ? 'is-placeholder' : '']">{{ acceptedByLabel }}</view>
</picker>
<view :class="['picker-field', !formData.acceptedBy ? 'is-placeholder' : '', readonlyMeta ? 'is-disabled' : '']" @click="openUserSelect('acceptedBy')">
<text>{{ acceptedByLabel }}</text>
<uni-icons v-if="!readonlyMeta" type="right" size="16" color="#9ca3af"></uni-icons>
</view>
</view>
<view class="form-item">
<text class="form-label">{{ t('moldRepair.confirmBy') }}</text>
<picker :range="userLabels" :value="confirmByIndex" :disabled="readonlyMeta" @change="(e) => onUserChange('confirmBy', e)">
<view :class="['picker-field', !formData.confirmBy ? 'is-placeholder' : '']">{{ confirmByLabel }}</view>
</picker>
<view :class="['picker-field', !formData.confirmBy ? 'is-placeholder' : '', readonlyMeta ? 'is-disabled' : '']" @click="openUserSelect('confirmBy')">
<text>{{ confirmByLabel }}</text>
<uni-icons v-if="!readonlyMeta" type="right" size="16" color="#9ca3af"></uni-icons>
</view>
</view>
<view class="form-item inline-radio">
@ -219,7 +221,7 @@
<script setup>
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getBrandList } from '@/api/mes/mold'
@ -240,6 +242,7 @@ const repairId = ref('')
const users = ref([])
const moldOptions = ref([])
const loading = ref(false)
const today = getTodayDate()
const formData = reactive({
id: undefined,
@ -249,9 +252,9 @@ const formData = reactive({
moldId: undefined,
moldCode: '',
moldName: '',
requireDate: '',
finishDate: '',
confirmDate: '',
requireDate: today,
finishDate: today,
confirmDate: today,
repairStatus: '0',
repairResult: '0',
acceptedBy: '',
@ -291,7 +294,6 @@ const statusLabel = computed(() => {
if (normalized === '2') return t('moldRepair.statusRejected')
return t('moldRepair.statusPending')
})
const userLabels = computed(() => users.value.map((item) => item.nickname || item.name || String(item.id || '')))
const moldLabels = computed(() => moldOptions.value.map((item) => item.label))
const faultLevelOptions = computed(() => {
const dicts = dictStore.getDict(DICT_TYPE.FAILURE_LEVEL) || []
@ -302,8 +304,6 @@ const faultLevelOptions = computed(() => {
value: String(item.value)
}))
})
const acceptedByIndex = computed(() => findUserIndex(formData.acceptedBy))
const confirmByIndex = computed(() => findUserIndex(formData.confirmBy))
const acceptedByLabel = computed(() => resolveUserLabel(formData.acceptedBy, t('moldRepair.placeholderAcceptedBy')))
const confirmByLabel = computed(() => resolveUserLabel(formData.confirmBy, t('moldRepair.placeholderConfirmBy')))
const moldIndex = computed(() => {
@ -327,6 +327,10 @@ onLoad(async (options) => {
}
})
onShow(() => {
applySelectedUser()
})
async function fetchUsers() {
try {
const res = await getSimpleUserList()
@ -373,9 +377,9 @@ async function fetchDetail(id) {
formData.moldId = detail?.moldId ?? detail?.machineryId
formData.moldCode = inputValue(detail?.moldCode ?? detail?.machineryCode)
formData.moldName = inputValue(detail?.moldName ?? detail?.machineryName)
formData.requireDate = formatPickerDate(detail?.requireDate)
formData.finishDate = formatPickerDate(detail?.finishDate)
formData.confirmDate = formatPickerDate(detail?.confirmDate)
formData.requireDate = defaultDateValue(detail?.requireDate)
formData.finishDate = defaultDateValue(detail?.finishDate)
formData.confirmDate = defaultDateValue(detail?.confirmDate)
formData.repairStatus = normalizedRepairStatus
formData.repairResult = normalizedRepairStatus
formData.acceptedBy = normalizeUserId(detail?.acceptedBy)
@ -423,12 +427,6 @@ function onDateChange(field, event) {
formData[field] = String(event?.detail?.value || '')
}
function onUserChange(field, event) {
const index = Number(event?.detail?.value || 0)
const current = users.value[index]
formData[field] = current ? String(current.id) : ''
}
function onMoldChange(event) {
const index = Number(event?.detail?.value || 0)
const current = moldOptions.value[index]
@ -501,12 +499,6 @@ function onRepairStatusChange(event) {
formData.repairResult = value
}
function findUserIndex(value) {
if (value === undefined || value === null || value === '') return 0
const index = users.value.findIndex((item) => String(item.id) === String(value) || item.nickname === String(value))
return index >= 0 ? index : 0
}
function resolveUserLabel(value, placeholder) {
if (value === undefined || value === null || value === '') return placeholder
const current = users.value.find((item) => String(item.id) === String(value) || item.nickname === String(value))
@ -529,6 +521,36 @@ function datePickerValue(value) {
return value || formatPickerDate(Date.now())
}
function openUserSelect(field) {
if (readonlyMeta.value) return
const selectedId = field === 'acceptedBy' ? formData.acceptedBy : formData.confirmBy
uni.navigateTo({
url: `/pages_function/pages/moldRepair/userSelect?field=${encodeURIComponent(field)}&selectedId=${encodeURIComponent(String(selectedId || ''))}`
})
}
function applySelectedUser() {
const result = getApp().globalData._moldRepairUserSelectResult
if (!result?.field || !result?.user) return
getApp().globalData._moldRepairUserSelectResult = null
const field = result.field === 'confirmBy' ? 'confirmBy' : 'acceptedBy'
const user = result.user
formData[field] = String(user.id || '')
if (user.id !== undefined && user.id !== null && !users.value.some((item) => String(item.id) === String(user.id))) {
users.value = [user, ...users.value]
}
}
function getTodayDate() {
return formatPickerDate(Date.now())
}
function defaultDateValue(value) {
const formatted = formatPickerDate(value)
if (formatted) return formatted
return mode.value === 'detail' ? '' : today
}
function formatPickerDate(value) {
if (value === undefined || value === null || value === '') return ''
const date = parseDate(value)
@ -817,6 +839,8 @@ function goBack() {
padding: 0 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.form-textarea {
@ -828,6 +852,10 @@ function goBack() {
color: #9ca3af;
}
.picker-field.is-disabled {
color: #9ca3af;
}
.code-row {
display: flex;
align-items: center;

@ -24,9 +24,6 @@
</view>
</picker>
<view class="reset-filter-btn" @click="resetFilters">{{ t('functionCommon.reset') }}</view>
<view class="scan-btn" @click="handleScan">
<uni-icons type="scan" size="22" color="#111827"></uni-icons>
</view>
</view>
<scroll-view
@ -227,51 +224,6 @@ function onStatusFilterChange(event) {
fetchList(true)
}
async function handleScan() {
try {
const res = await uni.scanCode({ scanType: ['qrCode', 'barCode'] })
const result = String(res?.result || '').trim()
if (!result) {
uni.showToast({ title: t('moldRepair.scanUnrecognized'), icon: 'none' })
return
}
const scan = parseMoldScanResult(result)
if (!scan.id) {
uni.showToast({ title: t('moldRepair.scanMoldRequired'), icon: 'none' })
return
}
selectedMoldId.value = scan.id
searchKeyword.value = ''
await fetchList(true)
} catch (error) {
const message = String(error?.errMsg || '')
if (message.includes('cancel')) return
uni.showToast({ title: t('moldRepair.scanFailed'), icon: 'none' })
}
}
function parseMoldScanResult(result) {
const text = String(result || '').trim()
const directMatch = text.match(/^([A-Z_]+)-(\d+)$/i)
if (directMatch) {
return {
type: directMatch[1].toUpperCase(),
id: directMatch[1].toUpperCase() === 'MOLD' ? directMatch[2] : ''
}
}
try {
const parsed = JSON.parse(text)
const type = String(parsed?.type || parsed?.bizType || parsed?.codeType || '').toUpperCase()
const id = String(parsed?.id || parsed?.moldId || parsed?.deviceId || '').trim()
return {
type,
id: type === 'MOLD' && id ? id : ''
}
} catch {
return { type: '', id: '' }
}
}
function canEdit(item) {
return !isProcessedRepair(item?.repairStatus)
}
@ -406,7 +358,7 @@ function textValue(value) {
.filter-bar {
display: grid;
grid-template-columns: minmax(0, 1fr) 150rpx 96rpx 64rpx;
grid-template-columns: minmax(0, 1fr) 150rpx 96rpx;
align-items: center;
gap: 14rpx;
padding: 18rpx 4rpx 20rpx;
@ -414,8 +366,7 @@ function textValue(value) {
.keyword-box,
.status-box,
.reset-filter-btn,
.scan-btn {
.reset-filter-btn {
height: 66rpx;
background: #ffffff;
border: 1rpx solid #d9dde5;
@ -452,13 +403,6 @@ function textValue(value) {
color: #a8adb7;
}
.scan-btn {
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
}
.reset-filter-btn {
display: flex;
align-items: center;

@ -0,0 +1,257 @@
<template>
<view class="page-container">
<NavBar :title="pageTitle" />
<view class="search-bar">
<view class="search-input-wrap">
<text class="search-icon iconfont icon-search"></text>
<input
v-model="searchText"
class="search-input"
type="text"
:placeholder="t('moldRepair.placeholderUserSearch')"
placeholder-class="search-placeholder"
confirm-type="search"
@input="onSearchInput"
@confirm="loadUsers"
/>
<text v-if="searchText" class="search-clear" @click="clearSearch">x</text>
</view>
</view>
<scroll-view v-if="userList.length > 0" scroll-y class="user-list">
<view
v-for="user in userList"
:key="user.id"
class="user-card"
:class="{ active: String(selectedId) === String(user.id) }"
@click="selectedId = user.id"
>
<text class="user-name">{{ textValue(user.nickname) }}</text>
<view class="user-meta">
<text class="user-meta-text">ID: {{ textValue(user.id) }}</text>
<text v-if="user.deptName" class="user-meta-divider">|</text>
<text v-if="user.deptName" class="user-meta-text">{{ textValue(user.deptName) }}</text>
</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('moldRepair.noUserData') }}</text>
</view>
<view class="bottom-actions">
<view class="bottom-btn confirm-btn" @click="handleConfirm">
{{ t('functionCommon.confirm') }}
</view>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getSimpleUserList } from '@/api/system/user'
const { t } = useI18n()
const field = ref('acceptedBy')
const selectedId = ref('')
const searchText = ref('')
const userList = ref([])
const loading = ref(false)
let searchTimer = null
const pageTitle = computed(() => {
return field.value === 'confirmBy' ? t('moldRepair.confirmBy') : t('moldRepair.acceptedBy')
})
onLoad(async (options) => {
field.value = String(options?.field || 'acceptedBy')
selectedId.value = String(options?.selectedId || '')
await loadUsers()
})
function onSearchInput() {
if (searchTimer) {
clearTimeout(searchTimer)
}
searchTimer = setTimeout(() => {
loadUsers()
}, 300)
}
function clearSearch() {
searchText.value = ''
loadUsers()
}
async function loadUsers() {
if (searchTimer) {
clearTimeout(searchTimer)
searchTimer = null
}
loading.value = true
try {
const nickname = searchText.value.trim()
const res = await getSimpleUserList({
nickname: nickname || undefined
})
const root = res && res.data !== undefined ? res.data : res
const data = Array.isArray(root) ? root : (Array.isArray(root?.data) ? root.data : [])
userList.value = Array.isArray(data) ? data : []
} catch (error) {
userList.value = []
} finally {
loading.value = false
}
}
function handleConfirm() {
if (!selectedId.value) {
uni.showToast({ title: t('moldRepair.validatorUserRequired'), icon: 'none' })
return
}
const user = userList.value.find((item) => String(item.id) === String(selectedId.value))
if (!user) {
uni.showToast({ title: t('moldRepair.validatorUserRequired'), icon: 'none' })
return
}
getApp().globalData._moldRepairUserSelectResult = {
field: field.value === 'confirmBy' ? 'confirmBy' : 'acceptedBy',
user
}
uni.navigateBack()
}
function textValue(value) {
if (value === 0) return '0'
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
</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;
}
.user-list {
padding: 16rpx 24rpx;
}
.user-card {
background: #fff;
border-radius: 14rpx;
padding: 28rpx 24rpx;
margin-bottom: 16rpx;
border: 2rpx solid transparent;
&.active {
border-color: #2563eb;
background: #f0f5ff;
}
}
.user-name {
font-size: 30rpx;
font-weight: 700;
color: #1a1a1a;
}
.user-meta {
display: flex;
align-items: center;
gap: 10rpx;
margin-top: 10rpx;
}
.user-meta-text {
font-size: 24rpx;
color: #7a8494;
line-height: 1.3;
}
.user-meta-divider {
font-size: 22rpx;
color: #c3cad5;
}
.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: 18rpx 24rpx calc(18rpx + 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: 84rpx;
line-height: 84rpx;
text-align: center;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 600;
}
.confirm-btn {
background: #1f4b79;
color: #fff;
}
</style>

@ -69,6 +69,7 @@ import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getDeviceLedgerList } from '@/api/mes/moldoperate'
import { getDeviceLineTree } from '@/api/mes/deviceLine'
import { getMoldBrandPage } from '@/api/mes/mold'
const { t } = useI18n()
@ -105,65 +106,39 @@ function getStatusLabel(device) {
return map[status] || textValue(device.deviceStatus) || '-'
}
// - fallback
function getCurrentMold(device) {
const deviceId = device?.id
const deviceCode = device?.deviceCode
//
const deviceMoldMap = ref(new Map())
// 1. _mountedMoldInfoMap deviceId deviceCode fallback
async function loadDeviceMolds() {
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)
}
const res = await getMoldBrandPage({ pageSize: 100 })
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 : [])
const map = new Map()
for (const mold of list) {
const deviceName = mold.deviceName
if (deviceName) {
if (!map.has(deviceName)) map.set(deviceName, [])
map.get(deviceName).push(mold.name || '')
}
// 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 || '-'
}
deviceMoldMap.value = map
} catch (e) {
console.warn('[deviceSelect] read _mountedMoldInfoMap error', e)
console.error('loadDeviceMolds 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 || '-'
}
// -
function getCurrentMold(device) {
if (!device) return t('moldOperate.noMoldOnDevice')
const deviceName = device.deviceName
if (deviceName && deviceMoldMap.value.has(deviceName)) {
const names = deviceMoldMap.value.get(deviceName) || []
return names.join('')
}
// 3. fallback
const staticMold = textValue(device.currentMold || device.moldName || device.moldId)
return staticMold === '-' ? t('moldOperate.noMoldOnDevice') : staticMold
// fallback
return textValue(device.currentMold || device.moldName) || t('moldOperate.noMoldOnDevice')
}
function flattenLineTree(nodes, parentId) {
@ -269,7 +244,7 @@ function handleConfirm() {
}
onShow(async () => {
await Promise.allSettled([loadDevices(), loadLineTree()])
await Promise.allSettled([loadDevices(), loadLineTree(), loadDeviceMolds()])
})
</script>

@ -4,24 +4,24 @@
<NavBar :title="t('moldOperate.tabDown')">
<template #right>
<view class="nav-right-btn" @click="goToHistory">
<uni-icons type="clock" size="22" color="#1f7cff"></uni-icons>
<text class="nav-right-text">{{ t('moldPressureNet.history') }}</text>
<uni-icons type="calendar" size="22" color="#22486e"></uni-icons>
</view>
</template>
</NavBar>
<!-- 操作按钮区 -->
<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 class="scan-input-row">
<input
class="scan-input"
v-model="scanCodeInput"
placeholder="红外扫码或输入设备码"
confirm-type="done"
:focus="true"
@confirm="onScanInputConfirm"
/>
</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>
@ -80,16 +80,6 @@
<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>
@ -117,40 +107,15 @@
<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="select-dropdown" @click="toggleOperatorDropdown">
<view class="dropdown-input">
<text :class="['dropdown-value', { placeholder: !selectedOperator }]">
{{ selectedOperator ? selectedOperator.label : t('moldOperate.placeholderOperator') }}
</text>
<uni-icons type="bottom" size="14" color="#999" class="dropdown-arrow"></uni-icons>
</view>
<view v-if="showOperatorDropdown" class="dropdown-panel">
<scroll-view scroll-y class="dropdown-scroll">
<view
v-for="(item, idx) in operatorOptions"
:key="item.value"
class="dropdown-item"
:class="{ active: selectedOperator?.value === item.value }"
@click.stop="handleSelectOperator(item, idx)"
>
<text class="dropdown-item-text">{{ item.label }}</text>
<uni-icons
v-if="selectedOperator?.value === item.value"
type="checkmarkempty"
size="16"
color="#1f7cff"
></uni-icons>
</view>
<view v-if="!operatorOptions.length" class="dropdown-empty"></view>
</scroll-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>
<view class="form-row">
<view class="form-cell">
<text class="form-label">{{ t('moldOperate.remark') }}</text>
@ -202,11 +167,17 @@ 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'
import { getDeviceLedgerList, createMoldOperate } from '@/api/mes/moldoperate'
import { getMoldBrandPage } from '@/api/mes/mold'
import useUserStore from '@/store/modules/user'
import { getDeviceLineTree } from '@/api/mes/deviceLine'
import { getSimpleUserList } from '@/api/system/user'
const { t } = useI18n()
const userStore = useUserStore()
//
const currentUserName = computed(() => userStore.name || '未知用户')
const currentUserId = computed(() => userStore.userId)
const lineInfoMap = ref(new Map()) // deviceLine id { name, parentId, parentChain }
@ -282,10 +253,8 @@ const tempSelectedMoldId = ref(null)
// ---- ----
const remarkText = ref('')
const operatorOptions = ref([])
const selectedOperator = ref(null)
const operatorIndex = ref(-1)
const showOperatorDropdown = ref(false)
const scanCodeInput = ref('')
// ---- ----
function textValue(v) {
@ -295,12 +264,7 @@ function textValue(v) {
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 '-'
@ -374,45 +338,20 @@ function findArr(obj, d = 0) {
return []
}
async function loadLowerMolds(deviceId) {
async function loadLowerMolds(deviceName) {
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)
const res = await getMoldBrandPage({ deviceName, pageSize: 100 })
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 : [])
lowerMoldOptions.value = list.map(m => ({
id: m.id,
moldId: m.id,
moldName: m.name || '-',
moldCode: m.code || '-',
productName: m.productName || '-'
}))
} catch (e) {
console.error('loadLowerMolds error', e)
} finally {
@ -420,17 +359,39 @@ async function loadLowerMolds(deviceId) {
}
}
// ---- ----
// ---- / ----
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()
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)
)
scanCodeInput.value = code
const matched = matchDeviceByCode(code)
if (matched) {
selectDevice(matched.raw)
} else {
@ -445,13 +406,24 @@ function handleScan() {
})
}
function onScanInputConfirm() {
const code = scanCodeInput.value.trim()
if (!code) return
const matched = matchDeviceByCode(code)
if (matched) {
selectDevice(matched.raw)
} else {
uni.showToast({ title: t('moldOperate.deviceNotFound'), icon: 'none' })
}
}
// ---- ----
function selectDevice(device) {
selectedDevice.value = device || {}
moldsLoaded.value = false
selectedMold.value = {}
if (device?.id) {
loadLowerMolds(device.id).then(() => {
if (device?.deviceName) {
loadLowerMolds(device.deviceName).then(() => {
moldsLoaded.value = true
if (lowerMoldOptions.value.length > 0) {
const first = lowerMoldOptions.value[0]
@ -459,9 +431,7 @@ function selectDevice(device) {
id: first.id || first.moldId,
moldName: first.moldName || first.name,
moldCode: first.moldCode || first.code,
productName: first.productName || '-',
mountTime: first.mountTime || '-',
useCount: first.useCount ?? '-'
productName: first.productName || '-'
}
}
})
@ -475,12 +445,12 @@ function openDevicePicker() {
// ---- ----
function openLowerMoldPicker() {
if (!selectedDevice.value?.id) {
if (!selectedDevice.value?.deviceName) {
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(() => {
loadLowerMolds(selectedDevice.value.deviceName).then(() => {
lowerMoldPickerRef.value?.open()
})
}
@ -494,9 +464,7 @@ function confirmLowerMoldSelection() {
id: mold.id || mold.moldId,
moldName: mold.moldName || mold.name,
moldCode: mold.moldCode || mold.code,
productName: mold.productName || '-',
mountTime: mold.mountTime || '-',
useCount: mold.useCount ?? '-'
productName: mold.productName || '-'
}
}
lowerMoldPickerRef.value?.close()
@ -504,34 +472,16 @@ function confirmLowerMoldSelection() {
function closeLowerMoldPicker() { lowerMoldPickerRef.value?.close() }
// ---- ----
async function loadOperators() {
try {
const res = await getSimpleUserList()
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
operatorOptions.value = data.map((u) => ({
value: u.id || u.userId,
label: u.nickname || u.userName || u.name || String(u.id || '')
}))
} catch (e) {
console.error('loadOperators error', e)
// ---- ----
function autoSetOperator() {
if (currentUserId.value && currentUserName.value) {
selectedOperator.value = {
value: currentUserId.value,
label: currentUserName.value
}
}
}
function toggleOperatorDropdown() {
showOperatorDropdown.value = !showOperatorDropdown.value
}
function closeOperatorDropdown() {
showOperatorDropdown.value = false
}
function handleSelectOperator(item, idx) {
selectedOperator.value = item
operatorIndex.value = idx
closeOperatorDropdown()
}
function goToHistory() {
uni.navigateTo({ url: '/pages_function/pages/moldoperate/history?type=down' })
}
@ -578,21 +528,11 @@ async function handleConfirm() {
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 = []
remarkText.value = ''
selectedOperator.value = null
operatorIndex.value = -1
} catch (e) {
console.error('[下模] 保存失败 =', e)
const errMsg = e?.msg || (typeof e === 'string' ? e : e?.message) || '系统异常'
@ -608,7 +548,8 @@ function handleCancel() {
}
onShow(async () => {
await Promise.allSettled([loadDevices(), loadLineTree(), loadOperators()])
autoSetOperator()
await Promise.allSettled([loadDevices(), loadLineTree()])
// globalData
const device = getApp().globalData._deviceSelectResult
if (device) {
@ -621,21 +562,16 @@ onShow(async () => {
<style lang="scss" scoped>
/* ====== 导航栏右侧按钮 ====== */
.nav-right-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
gap: 6rpx;
padding: 8rpx 18rpx;
justify-content: center;
background: #ffffff;
border-radius: 999rpx;
border-radius: 50%;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.08);
}
.nav-right-text {
font-size: 24rpx;
color: #374151;
font-weight: 500;
}
.page-container {
min-height: 100vh;
background: #f5f6f8;
@ -645,39 +581,48 @@ onShow(async () => {
/* ====== 操作按钮区 ====== */
.action-row {
display: flex;
gap: 20rpx;
align-items: center;
gap: 16rpx;
padding: 20rpx 24rpx;
}
.action-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
height: 96rpx;
border-radius: 12rpx;
height: 72rpx;
padding: 0 24rpx;
border-radius: 10rpx;
background: #1f4b79;
color: #fff;
font-size: 28rpx;
font-size: 26rpx;
font-weight: 600;
white-space: nowrap;
.btn-icon-wrap {
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
.btn-text {
font-size: 26rpx;
line-height: 1;
}
}
.btn-icon {
font-size: 36rpx;
color: #fff;
}
.scan-input-row {
flex: 1;
display: flex;
align-items: center;
height: 72rpx;
border-radius: 10rpx;
overflow: hidden;
}
.btn-text {
font-size: 28rpx;
}
.scan-input {
flex: 1;
height: 72rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #333;
background: #fff;
border: 1rpx solid #d0d5dd;
border-radius: 10rpx;
}
/* ====== 卡片通用样式 ====== */
@ -854,6 +799,11 @@ onShow(async () => {
font-size: 27rpx;
color: #333;
background: #f9fafb;
&.readonly-input {
background: #f8fafc;
border-color: #f0f0f0;
}
}
.dropdown-value {

@ -4,8 +4,7 @@
<NavBar :title="t('moldOperate.tabUp')">
<template #right>
<view class="nav-right-btn" @click="goToHistory">
<uni-icons type="clock" size="22" color="#1f7cff"></uni-icons>
<text class="nav-right-text">{{ t('moldPressureNet.history') }}</text>
<uni-icons type="calendar" size="22" color="#22486e"></uni-icons>
</view>
</template>
</NavBar>
@ -13,16 +12,17 @@
<!-- ========== 上模 ========== -->
<!-- 操作按钮区 -->
<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 class="scan-input-row">
<input
class="scan-input"
v-model="scanCodeInput"
placeholder="红外扫码或输入设备码"
confirm-type="done"
:focus="true"
@confirm="onScanInputConfirm"
/>
</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>
@ -115,35 +115,8 @@
<view class="form-row">
<view class="form-cell">
<text class="form-label"><text class="required">*</text>{{ t('moldOperate.operator') }}</text>
<!-- 下拉选择器 -->
<view class="select-dropdown" @click="toggleOperatorDropdown">
<view class="dropdown-input">
<text :class="['dropdown-value', { placeholder: !selectedOperator }]">
{{ selectedOperator ? selectedOperator.label : t('moldOperate.placeholderOperator') }}
</text>
<uni-icons type="bottom" size="14" color="#999" class="dropdown-arrow"></uni-icons>
</view>
<!-- 下拉列表在输入框下方展开 -->
<view v-if="showOperatorDropdown" class="dropdown-panel">
<scroll-view scroll-y class="dropdown-scroll">
<view
v-for="(item, idx) in operatorOptions"
:key="item.value"
class="dropdown-item"
:class="{ active: selectedOperator?.value === item.value }"
@click.stop="handleSelectOperator(item, idx)"
>
<text class="dropdown-item-text">{{ item.label }}</text>
<uni-icons
v-if="selectedOperator?.value === item.value"
type="checkmarkempty"
size="16"
color="#1f7cff"
></uni-icons>
</view>
<view v-if="!operatorOptions.length" class="dropdown-empty"></view>
</scroll-view>
</view>
<view class="dropdown-input readonly-input">
<text class="dropdown-value">{{ currentUserName }}</text>
</view>
</view>
</view>
@ -176,9 +149,16 @@ import { 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 { getSimpleUserList } from '@/api/system/user'
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) {
@ -211,15 +191,78 @@ async function loadDevices() {
}
}
// 线 - 线
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 operatorOptions = ref([])
const selectedOperator = ref(null)
const operatorIndex = ref(-1)
const scanCodeInput = ref('') // /
// -
const deviceStatusClass = computed(() => {
@ -241,7 +284,7 @@ const deviceStatusLabel = computed(() => {
return map[status] || textValue(selectedDevice.value?.deviceStatus) || '-'
})
const MOLD_STATUS_MAP = { 0: '在机', 1: '待用', 2: '维修', 3: '报废' }
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'
@ -250,91 +293,51 @@ function getMoldStatusClass(s) {
return 'standby-tag'
}
// ---- ID----
const MOUNTED_MOLD_KEY = '_mountedMoldInfoMap'
function getMountedMoldMap() {
try {
const data = uni.getStorageSync(MOUNTED_MOLD_KEY) || {}
return data
} catch (e) {
console.warn('[上模] getMountedMoldMap 异常', e)
return {}
// - machineId = ID
const currentMoldList = ref([])
async function fetchCurrentMolds(deviceName) {
if (!deviceName) {
currentMoldList.value = []
return
}
}
function getMountedMoldByDevice(deviceId) {
if (!deviceId) return null
const map = getMountedMoldMap()
const key = String(deviceId)
return map[key] || null
}
function saveMountedMoldInfo(info) {
if (!info || !info.deviceId) return
try {
const map = getMountedMoldMap()
map[String(info.deviceId)] = info
uni.setStorageSync(MOUNTED_MOLD_KEY, map)
console.log('[上模] saveMountedMoldInfo 写入成功, deviceId=', info.deviceId, 'map keys=', Object.keys(map))
//
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.warn('[上模] saveMountedMoldInfo 异常', e)
}
}
function clearMountedMoldInfo(deviceId) {
if (!deviceId) {
try { uni.removeStorageSync(MOUNTED_MOLD_KEY) } catch {}
return
console.error('fetchCurrentMolds error', e)
currentMoldList.value = []
}
try {
const map = getMountedMoldMap()
delete map[String(deviceId)]
uni.setStorageSync(MOUNTED_MOLD_KEY, map)
} catch {}
}
// -
const currentMoldDisplay = computed(() => {
if (!selectedDevice.value?.id) return '-'
const saved = getMountedMoldByDevice(selectedDevice.value.id)
if (saved) {
console.log('[上模] currentMoldDisplay 命中持久化, deviceId=', selectedDevice.value.id, 'mold=', saved.mold?.name || saved.mold?.moldName)
return saved.mold?.name || saved.mold?.moldName || '-'
if (currentMoldList.value.length > 0) {
return currentMoldList.value.map(m => m.name || '').filter(Boolean).join('') || '-'
}
return textValue(selectedDevice.value.currentMold)
return t('moldOperate.noMoldOnDevice')
})
// ---- ----
async function loadOperators() {
try {
const res = await getSimpleUserList()
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
operatorOptions.value = data.map((u) => ({
value: u.id || u.userId,
label: u.nickname || u.userName || u.name || String(u.id || '')
}))
} catch (e) {
console.error('loadOperators error', e)
// ---- ----
function autoSetOperator() {
if (currentUserId.value && currentUserName.value) {
selectedOperator.value = {
value: currentUserId.value,
label: currentUserName.value
}
}
}
const showOperatorDropdown = ref(false)
function toggleOperatorDropdown() {
showOperatorDropdown.value = !showOperatorDropdown.value
}
function closeOperatorDropdown() {
showOperatorDropdown.value = false
}
function handleSelectOperator(item, idx) {
selectedOperator.value = item
operatorIndex.value = idx
closeOperatorDropdown()
}
function selectDevice(device) {
selectedDevice.value = device || {}
tempSelectedDeviceId.value = device ? device.id : null
selectedMountMolds.value = []
if (device?.deviceName) fetchCurrentMolds(device.deviceName)
}
function openDevicePicker() {
@ -394,22 +397,6 @@ async function handleConfirmMount() {
console.log('=== 上模返回 ===', JSON.stringify(res))
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
// /
if (selectedMountMolds.value.length > 0) {
const info = {
deviceId: selectedDevice.value.id,
deviceCode: selectedDevice.value.deviceCode,
deviceName: selectedDevice.value.deviceName,
mold: selectedMountMolds.value[0],
mountTime: new Date().toLocaleString()
}
saveMountedMoldInfo(info)
// globalData
getApp().globalData._mountedMoldInfo = info
getApp().globalData._mountedMoldInfoMap = getMountedMoldMap()
console.log('=== 已保存在机模具信息 ===', JSON.stringify(info))
}
selectedDevice.value = {}
selectedMountMolds.value = []
} catch (e) {
@ -421,17 +408,40 @@ async function handleConfirmMount() {
}
// ==================== : / / ====================
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()
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)
)
scanCodeInput.value = code //
const matched = matchDeviceByCode(code)
if (matched) {
setDeviceLineName(matched.raw)
selectDevice(matched.raw)
} else {
uni.showToast({ title: t('moldOperate.deviceNotFound'), icon: 'none' })
@ -445,12 +455,23 @@ function handleScan() {
})
}
//
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 = ''
selectedOperator.value = null
operatorIndex.value = -1
//
uni.navigateBack({
fail: () => uni.switchTab({ url: '/pages/work' })
@ -462,7 +483,8 @@ function goToHistory() {
}
onShow(async () => {
await Promise.allSettled([loadDevices(), loadOperators()])
autoSetOperator()
await Promise.allSettled([loadDevices(), loadLineTree()])
// globalData
const device = getApp().globalData._deviceSelectResult
if (device) {
@ -487,59 +509,77 @@ onShow(async () => {
/* ====== 导航栏右侧按钮 ====== */
.nav-right-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
gap: 6rpx;
padding: 8rpx 18rpx;
justify-content: center;
background: #ffffff;
border-radius: 999rpx;
border-radius: 50%;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.08);
}
.nav-right-text {
font-size: 24rpx;
color: #374151;
font-weight: 500;
}
/* ====== 操作按钮区 ====== */
.action-row {
display: flex;
gap: 20rpx;
align-items: center;
gap: 16rpx;
padding: 20rpx 24rpx;
}
.action-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
height: 96rpx;
border-radius: 12rpx;
gap: 10rpx;
height: 72rpx;
padding: 0 24rpx;
border-radius: 10rpx;
background: #1f4b79;
color: #fff;
font-size: 28rpx;
font-size: 26rpx;
font-weight: 600;
white-space: nowrap;
.btn-icon-wrap {
width: 44rpx;
height: 44rpx;
width: 36rpx;
height: 36rpx;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon {
font-size: 36rpx;
font-size: 30rpx;
color: #fff;
}
.btn-text {
font-size: 28rpx;
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;
@ -706,6 +746,11 @@ onShow(async () => {
font-size: 27rpx;
color: #333;
background: #f9fafb;
&.readonly-input {
background: #f8fafc;
border-color: #f0f0f0;
}
}
.dropdown-value {

@ -77,7 +77,7 @@ function textValue(v) {
return s || '-'
}
const STATUS_MAP = { 0: '在机', 1: '待用', 2: '维修', 3: '报废' }
const STATUS_MAP = { 0: '在机', 1: '待用', 2: '维修', 3: '报废', 4: '在库' }
function getStatusText(s) { return STATUS_MAP[s] || textValue(s) }
function getStatusClass(s) {
if (s === 0) return 'in-use-tag'
@ -87,9 +87,11 @@ function getStatusClass(s) {
}
const filteredList = computed(() => {
// status=0
const available = moldList.value.filter(m => Number(m.status) !== 0)
const keyword = searchText.value.trim().toLowerCase()
if (!keyword) return moldList.value
return moldList.value.filter((m) => {
if (!keyword) return available
return available.filter((m) => {
return (m.name || '').toLowerCase().includes(keyword) ||
(m.code || '').toLowerCase().includes(keyword) ||
(m.productName || '').toLowerCase().includes(keyword)

File diff suppressed because it is too large Load Diff

@ -0,0 +1,594 @@
<template>
<view class="page-container">
<NavBar :title="t('sparepartInbound.moduleName')" />
<!-- 搜索栏 -->
<view class="filter-bar">
<view class="keyword-box">
<input
v-model="searchKeyword"
class="keyword-input"
:placeholder="t('sparepartInbound.searchPlaceholder')"
confirm-type="search"
@input="handleKeywordInput"
@confirm="handleSearch"
/>
</view>
<picker mode="selector" :range="statusLabels" :value="statusIndex" @change="onStatusChange">
<view class="status-box">
<text class="status-box-text">{{ currentStatusLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
</picker>
<view class="reset-filter-btn" @click="resetFilters">{{ t('functionCommon.reset') }}</view>
</view>
<!-- 列表 -->
<scroll-view
scroll-y
class="list-scroll"
:scroll-top="scrollTop"
@scroll="onScroll"
@scrolltolower="loadMore"
:lower-threshold="80"
>
<view class="list-wrap">
<view
v-for="item in list"
:key="item.id"
class="task-card"
@click="openDetail(item)"
>
<view class="card-header">
<view class="header-main">
<view class="header-left">
<text class="task-no">{{ textValue(item.no) }}</text>
</view>
<view class="header-tags">
<text :class="['record-tag', statusClass(item.status)]">{{ statusText(item.status) }}</text>
</view>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('sparepartInbound.sparepartInfo') }}</text>
<text class="value">{{ textValue(item.productNames) }}</text>
</view>
<view class="row">
<text class="label">{{ t('sparepartInbound.inboundTime') }}</text>
<text class="value">{{ formatDateTime(item.inTime || item.createTime) }}</text>
</view>
<view class="row">
<text class="label">{{ t('sparepartInbound.creator') }}</text>
<text class="value">{{ textValue(item.creatorName || item.creator) }}</text>
</view>
<view class="row">
<text class="label">{{ t('sparepartInbound.quantity') }}</text>
<text class="value highlight">{{ textValue(item.totalCount) }}</text>
</view>
<view class="row">
<text class="label">{{ t('sparepartInbound.reviewer') }}</text>
<text class="value">{{ textValue(item.auditUserName) }}</text>
</view>
</view>
<!-- 操作按钮待审核状态显示 -->
<view v-if="Number(item.status) === 10" class="card-actions">
<view class="action-btn approve-btn" @click.stop="handleApprove(item)">{{ t('sparepartInbound.approve') }}</view>
<view class="action-btn reject-btn" @click.stop="handleReject(item)">{{ t('sparepartInbound.reject') }}</view>
</view>
</view>
<view v-if="loading && pageNo === 1" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!list.length" class="hint">{{ t('sparepartInbound.empty') }}</view>
<view v-else-if="loadingMore" class="hint">{{ t('functionCommon.loadingMore') }}</view>
<view v-else-if="finished" class="hint">{{ t('functionCommon.noMoreData') }}</view>
</view>
</scroll-view>
<!-- 回顶部 -->
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<uni-icons type="arrow-up" size="20" color="#1f4b79"></uni-icons>
</view>
<!-- 新增按钮 -->
<view class="add-btn" @click="goAdd">
<text class="add-icon">+</text>
</view>
</view>
</template>
<script setup>
import { computed, nextTick, ref } from 'vue'
import { onShow, onUnload } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getSparepartInboundPage, auditSparepartInbound } from '@/api/mes/sparepartInbound'
const { t } = useI18n()
const selectedStatus = ref('')
const searchKeyword = ref('')
const statusOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
{ label: t('sparepartInbound.tabPending'), value: '0' },
{ label: t('sparepartInbound.tabAuditing'), value: '10' },
{ label: t('sparepartInbound.approve'), value: '20' }
])
const statusLabels = computed(() => statusOptions.value.map((item) => item.label))
const statusIndex = computed(() => {
const index = statusOptions.value.findIndex((item) => item.value === selectedStatus.value)
return index >= 0 ? index : 0
})
const currentStatusLabel = computed(() => {
const current = statusOptions.value.find((item) => item.value === selectedStatus.value)
return current ? current.label : t('functionCommon.all')
})
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const finished = ref(false)
const pageNo = ref(1)
const pageSize = ref(10)
const scrollTop = ref(0)
const showGoTop = ref(false)
let searchTimer = null
function textValue(v) {
if (v === 0) return '0'
if (v == null) return '-'
const s = String(v).trim()
return s || '-'
}
function formatDateTime(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) {
const [year, month, day, hour = 0, minute = 0, second = 0] = value
const pad = (n) => String(n).padStart(2, '0')
return `${year}-${pad(month)}-${pad(day)} ${pad(hour)}:${pad(minute)}:${pad(second)}`
}
const date = new Date(Number(value))
if (Number.isNaN(date.getTime())) return String(value)
const pad = (n) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
}
const STATUS_MAP = { 0: '待入库', 10: '待审核', 20: '已通过', 1: '已驳回' }
function statusText(s) {
const num = Number(s)
return STATUS_MAP[num] || textValue(num)
}
function statusClass(s) {
const num = Number(s)
if (num === 0) return 'text-warning'
if (num === 10) return 'text-primary'
if (num === 20) return 'text-success'
if (num === 1) return 'text-danger'
return ''
}
function normalizePageData(res) {
const root = res && res.data !== undefined ? res.data : res
const candidateList = root?.list || root?.rows || root?.records || root?.data?.list || root?.data?.rows || []
const candidateTotal = root?.total ?? root?.data?.total ?? (Array.isArray(candidateList) ? candidateList.length : 0)
return {
list: Array.isArray(candidateList) ? candidateList : [],
total: Number(candidateTotal || 0)
}
}
async function fetchList(reset) {
if (reset) {
pageNo.value = 1
finished.value = false
}
if (pageNo.value === 1) {
loading.value = true
} else {
loadingMore.value = true
}
try {
const params = {
pageNo: pageNo.value,
pageSize: pageSize.value,
no: searchKeyword.value.trim() || undefined,
status: selectedStatus.value || undefined
}
const res = await getSparepartInboundPage(params)
const page = normalizePageData(res)
list.value = reset ? page.list : [...list.value, ...page.list]
finished.value = list.value.length >= page.total || page.list.length < pageSize.value
} catch (e) {
if (!reset) pageNo.value = Math.max(1, pageNo.value - 1)
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
} finally {
loading.value = false
loadingMore.value = false
}
}
function onStatusChange(event) {
const index = Number(event?.detail?.value || 0)
selectedStatus.value = statusOptions.value[index]?.value ?? ''
fetchList(true)
}
function handleSearch() {
clearSearchTimer()
uni.hideKeyboard()
fetchList(true)
}
function handleKeywordInput() {
clearSearchTimer()
searchTimer = setTimeout(() => {
fetchList(true)
}, 300)
}
function resetFilters() {
clearSearchTimer()
searchKeyword.value = ''
selectedStatus.value = ''
fetchList(true)
}
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
}
function openDetail(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
uni.navigateTo({
url: `/pages_function/pages/sparepartInbound/detail?id=${encodeURIComponent(String(item.id))}`
})
}
async function handleApprove(item) {
if (!item?.id) return
uni.showModal({
title: t('functionCommon.confirmTitle'),
content: t('sparepartInbound.confirmApprove'),
confirmColor: '#16a34a',
success: async (res) => {
if (res.confirm) {
try {
await auditSparepartInbound({ id: item.id, status: 20 })
uni.showToast({ title: t('sparepartInbound.approveSuccess'), icon: 'success' })
fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
}
}
}
})
}
async function handleReject(item) {
if (!item?.id) return
uni.showModal({
title: t('functionCommon.confirmTitle'),
content: t('sparepartInbound.confirmReject'),
confirmColor: '#dc2626',
success: async (res) => {
if (res.confirm) {
try {
await auditSparepartInbound({ id: item.id, status: 1 })
uni.showToast({ title: t('sparepartInbound.rejectSuccess'), icon: 'success' })
fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
}
}
}
})
}
function onScroll(event) {
showGoTop.value = (event?.detail?.scrollTop || 0) > 600
}
function goTop() {
scrollTop.value = 0
}
function goAdd() {
uni.navigateTo({
url: '/pages_function/pages/sparepartInbound/create'
})
}
function clearSearchTimer() {
if (searchTimer) {
clearTimeout(searchTimer)
searchTimer = null
}
}
onShow(() => {
fetchList(true)
})
onUnload(() => {
clearSearchTimer()
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: #f4f5f7;
}
/* ====== 搜索栏 ====== */
.filter-bar {
display: flex;
align-items: center;
gap: 12rpx;
padding: 18rpx 24rpx;
background: #fff;
}
.keyword-box {
flex: 1;
min-width: 0;
height: 66rpx;
padding: 0 28rpx;
background: #f4f5f7;
border: 1rpx solid #e5e7eb;
border-radius: 12rpx;
display: flex;
align-items: center;
}
.keyword-input {
width: 100%;
font-size: 24rpx;
color: #374151;
}
.status-box {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
height: 66rpx;
padding: 0 28rpx;
min-width: 160rpx;
background: #fff;
border: 1rpx solid #e5e7eb;
border-radius: 12rpx;
}
.status-box-text {
font-size: 24rpx;
color: #374151;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.reset-filter-btn {
height: 66rpx;
line-height: 66rpx;
padding: 0 28rpx;
font-size: 24rpx;
color: #4b5563;
background: #fff;
border: 1rpx solid #e5e7eb;
border-radius: 12rpx;
flex-shrink: 0;
}
/* ====== 列表 ====== */
.list-scroll {
height: calc(100vh - 194rpx);
}
.list-wrap {
padding: 0 24rpx 60rpx;
}
.task-card {
position: relative;
margin-top: 20rpx;
padding: 28rpx;
background: #fff;
border-radius: 22rpx;
box-shadow: 0 8rpx 28rpx rgba(15, 23, 42, 0.06);
}
.card-header {
margin-bottom: 18rpx;
}
.header-main {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.header-left {
min-width: 0;
flex: 1;
}
.task-no {
font-size: 32rpx;
font-weight: 700;
color: #0f172a;
}
.header-tags {
display: flex;
align-items: center;
gap: 12rpx;
flex-shrink: 0;
}
.record-tag {
padding: 8rpx 18rpx;
border-radius: 999rpx;
font-size: 22rpx;
line-height: 1;
background: #e2e8f0;
color: #64748b;
}
.card-body .row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 20rpx;
margin-top: 12rpx;
&:first-child {
margin-top: 0;
}
}
.label {
width: 140rpx;
font-size: 25rpx;
color: #94a3b8;
flex-shrink: 0;
}
.value {
flex: 1;
text-align: right;
font-size: 27rpx;
color: #334155;
line-height: 1.5;
&.highlight {
color: #1f4b79;
font-weight: 600;
}
}
/* ====== 操作按钮 ====== */
.card-actions {
display: flex;
gap: 16rpx;
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #f0f0f0;
}
.action-btn {
flex: 1;
height: 64rpx;
line-height: 64rpx;
text-align: center;
border-radius: 10rpx;
font-size: 26rpx;
font-weight: 500;
&.approve-btn {
background: #dcfce7;
color: #16a34a;
}
&.reject-btn {
background: #fee2e2;
color: #dc2626;
}
&:active {
opacity: 0.8;
}
}
/* ====== 状态标签颜色 ====== */
.text-success {
color: #16a34a;
}
.text-danger {
color: #dc2626;
}
.text-warning {
color: #d97706;
}
.text-primary {
color: #2563eb;
}
.record-tag.text-success {
color: #15803d;
background: #dcfce7;
}
.record-tag.text-danger {
color: #dc2626;
background: #fee2e2;
}
.record-tag.text-warning {
color: #d97706;
background: #fef3c7;
}
.record-tag.text-primary {
color: #1d4ed8;
background: #dbeafe;
}
/* ====== 提示 ====== */
.hint {
padding: 36rpx 0;
text-align: center;
color: #94a3b8;
font-size: 26rpx;
}
/* ====== 回顶部 ====== */
.go-top-btn {
position: fixed;
right: 28rpx;
bottom: calc(140rpx + env(safe-area-inset-bottom));
width: 92rpx;
height: 92rpx;
border-radius: 46rpx;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.12);
display: flex;
align-items: center;
justify-content: center;
}
/* ====== 新增按钮 ====== */
.add-btn {
position: fixed;
right: 28rpx;
bottom: calc(56rpx + env(safe-area-inset-bottom));
width: 92rpx;
height: 92rpx;
border-radius: 46rpx;
background: #1f4b79;
box-shadow: 0 14rpx 30rpx rgba(24, 63, 108, 0.24);
display: flex;
align-items: center;
justify-content: center;
}
.add-icon {
color: #ffffff;
font-size: 64rpx;
line-height: 1;
margin-top: -4rpx;
}
</style>

@ -0,0 +1,295 @@
<template>
<view class="page-container">
<NavBar :title="t('sparepartInbound.selectSparepart')" />
<!-- 搜索区 -->
<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('sparepartInbound.searchSparepartPlaceholder')" @input="onSearch" placeholder-class="search-placeholder" />
<text v-if="searchText" class="search-clear" @click="clearSearch"></text>
</view>
</view>
<!-- 备件列表 -->
<scroll-view scroll-y class="sparepart-list" v-if="filteredList.length > 0">
<view
v-for="item in filteredList"
:key="item.id"
class="sparepart-card"
:class="{ active: selectedId === item.id }"
@click="selectedId = item.id"
>
<view class="sparepart-card-header">
<text class="sparepart-name">{{ textValue(item.name) }}</text>
</view>
<view class="sparepart-card-body">
<view class="sparepart-info-row">
<text class="info-label">{{ t('sparepartInbound.sparepartCode') }}</text>
<text class="info-value">{{ textValue(item.barCode) }}</text>
</view>
<view class="sparepart-info-row">
<text class="info-label">{{ t('sparepartInbound.spec') }}</text>
<text class="info-value">{{ textValue(item.standard || item.deviceSpec) }}</text>
</view>
<view class="sparepart-info-row">
<text class="info-label">{{ t('sparepartInbound.category') }}</text>
<text class="info-value">{{ textValue(item.categoryName) }}</text>
</view>
<view class="sparepart-info-row">
<text class="info-label">{{ t('sparepartInbound.unit') }}</text>
<text class="info-value">{{ textValue(item.unitName) }}</text>
</view>
<view class="sparepart-info-row">
<text class="info-label">{{ t('sparepartInbound.purchaseUnit') }}</text>
<text class="info-value">{{ textValue(item.purchaseUnitName) }}</text>
</view>
<view class="sparepart-info-row">
<text class="info-label">{{ t('sparepartInbound.convertRatio') }}</text>
<text class="info-value">1{{ textValue(item.purchaseUnitName) }}={{ textValue(item.purchaseUnitConvertQuantity) }}{{ textValue(item.unitName) }}</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('sparepartInbound.noSparepartData') }}</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 { getSparepartSimpleList } from '@/api/mes/sparepart'
const { t } = useI18n()
const sparepartList = 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 || '-'
}
const filteredList = computed(() => {
let list = sparepartList.value
const keyword = searchText.value.trim().toLowerCase()
if (keyword) {
list = list.filter(item => {
return (item.name || '').toLowerCase().includes(keyword) ||
(item.barCode || '').toLowerCase().includes(keyword) ||
(item.standard || '').toLowerCase().includes(keyword)
})
}
return list
})
function onSearch() {}
function clearSearch() {
searchText.value = ''
}
async function loadSpareparts() {
loading.value = true
try {
const raw = []
let page = 1
const maxPages = 5
while (page <= maxPages) {
const res = await getSparepartSimpleList(page)
let root = res && res.data !== undefined ? res.data : res
let pageList = Array.isArray(root)
? root
: Array.isArray(root?.list) ? root.list
: Array.isArray(root?.rows) ? root.rows
: []
if (!pageList.length) break
raw.push(...pageList)
if (pageList.length < 100) break
page++
}
// "33"
sparepartList.value = raw.filter(item => item.categoryName === '33')
console.log('[sparepartSelect] 加载全部物料:', raw.length)
} catch (e) {
console.error('loadSpareparts error', e)
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
} finally {
loading.value = false
}
}
function handleConfirm() {
if (!selectedId.value) {
uni.showToast({ title: t('sparepartInbound.validatorSparepartRequired'), icon: 'none' })
return
}
const item = sparepartList.value.find((d) => d.id === selectedId.value)
// globalData onShow
getApp().globalData._sparepartSelectResult = item || null
uni.navigateBack()
}
onShow(async () => {
await loadSpareparts()
})
</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;
}
/* 备件列表 */
.sparepart-list {
padding: 16rpx 24rpx;
}
.sparepart-card {
background: #fff;
border-radius: 14rpx;
padding: 24rpx;
margin-bottom: 16rpx;
border: 2rpx solid transparent;
&.active {
border-color: #2563eb;
background: #f0f5ff;
}
}
.sparepart-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.sparepart-name {
font-size: 30rpx;
font-weight: 700;
color: #1a1a1a;
}
.sparepart-card-body {
border-top: 1rpx solid #f0f0f0;
padding-top: 16rpx;
}
.sparepart-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: 18rpx 24rpx calc(18rpx + 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: 84rpx;
line-height: 84rpx;
text-align: center;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 600;
}
.confirm-btn {
background: #1f4b79;
color: #fff;
}
</style>

@ -0,0 +1,711 @@
<template>
<view class="page-container">
<NavBar :title="t('sparepartOutbound.createTitle')" />
<!-- 操作按钮区 -->
<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">扫备件码</text>
</view>
<view class="action-btn select-btn" @click="handleSelectSparepart">
<view class="btn-icon-wrap">
<text class="iconfont icon-device btn-icon"></text>
</view>
<text class="btn-text">选择备件</text>
</view>
</view>
<!-- 已选备件 -->
<view v-if="selectedSparepart.id" class="sparepart-section">
<view class="section-title-bar">
<view class="section-bar-line"></view>
<text class="section-title">已选备件</text>
</view>
<view class="sparepart-card">
<view class="card-top">
<view class="card-image-wrap">
<image
v-if="sparepartImage"
:src="sparepartImage"
class="card-image"
mode="aspectFill"
/>
<view v-else class="card-image-empty">
<text class="empty-img-icon">📦</text>
</view>
</view>
<view class="card-info-right">
<view class="info-item">
<text class="info-label">备件名称</text>
<text class="info-name">{{ textValue(selectedSparepart.name) }}</text>
</view>
<view class="info-item">
<text class="info-label">规格</text>
<text class="info-value">{{ textValue(selectedSparepart.standard || selectedSparepart.deviceSpec) }}</text>
</view>
<view class="info-item">
<text class="info-label">当前库存</text>
<text class="info-value stock-highlight">{{ textValue(selectedSparepart.stock) }}{{ textUnit(selectedSparepart.minStockUnitName) }}</text>
</view>
</view>
</view>
<view class="card-bottom">
<view class="detail-row two-col">
<view class="detail-col">
<text class="detail-label">采购单位</text>
<text class="detail-value">{{ textValue(selectedSparepart.purchaseUnitName) }}</text>
</view>
<view class="detail-col">
<text class="detail-label">库存单位</text>
<text class="detail-value">{{ textValue(selectedSparepart.unitName || selectedSparepart.minStockUnitName) }}</text>
</view>
</view>
<view class="detail-row">
<text class="detail-label">换算关系</text>
<text class="detail-value">1{{ textValue(selectedSparepart.purchaseUnitName) }}={{ textValue(selectedSparepart.purchaseUnitConvertQuantity) }}{{ stockUnitLabel(selectedSparepart) }}</text>
</view>
</view>
</view>
<!-- 出库用途 -->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view>
<text class="section-title">出库用途</text>
</view>
<view class="purpose-card">
<view
class="purpose-item"
:class="{ active: selectedPurpose === 'repair' }"
@click="setPurpose('repair')"
>
<text class="iconfont icon-repair purpose-icon"></text>
<text class="purpose-text">维修领用</text>
</view>
<view
class="purpose-item"
:class="{ active: selectedPurpose === 'maintain' }"
@click="setPurpose('maintain')"
>
<text class="iconfont icon-shield purpose-icon"></text>
<text class="purpose-text">保养领用</text>
</view>
</view>
<!-- 关联信息维修领用/保养领用 -->
<view v-if="selectedPurpose === 'repair' || selectedPurpose === 'maintain'" class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view>
<text class="section-title">关联信息</text>
</view>
<view v-if="selectedPurpose === 'repair' || selectedPurpose === 'maintain'" class="select-row-card warehouse-area-card">
<view class="warehouse-area-rows">
<view class="warehouse-area-row">
<text class="warehouse-area-label">关联设备</text>
<view class="warehouse-area-dropdown" @click="toggleDeviceDropdown">
<view class="dropdown-input">
<text :class="['dropdown-value', { placeholder: !selectedDevice }]">{{ selectedDevice ? selectedDevice.label : '请选择' }}</text>
<text class="dropdown-arrow"></text>
</view>
<view v-if="showDeviceDropdown" class="dropdown-panel">
<scroll-view scroll-y class="dropdown-scroll">
<view v-for="item in deviceOptions" :key="item.value" class="dropdown-item" :class="{ active: selectedDevice?.value === item.value }" @click.stop="handleSelectDevice(item)">
<text class="dropdown-item-text">{{ item.label }}</text>
<text v-if="selectedDevice?.value === item.value" class="dropdown-check"></text>
</view>
<view v-if="!deviceOptions.length" class="dropdown-empty"></view>
</scroll-view>
</view>
</view>
</view>
<view class="warehouse-area-row">
<text class="warehouse-area-label">{{ selectedPurpose === 'repair' ? '维修单号' : '保养单号' }}</text>
<view class="warehouse-area-dropdown" @click="toggleOrderDropdown">
<view class="dropdown-input">
<text :class="['dropdown-value', { placeholder: !currentOrder }]">{{ currentOrder ? currentOrder.label : '请选择' }}</text>
<text class="dropdown-arrow"></text>
</view>
<view v-if="showOrderDropdown" class="dropdown-panel">
<scroll-view scroll-y class="dropdown-scroll">
<view v-for="item in currentOrderOptions" :key="item.value" class="dropdown-item" :class="{ active: currentOrder?.value === item.value }" @click.stop="handleSelectOrder(item)">
<text class="dropdown-item-text">{{ item.label }}</text>
<text v-if="currentOrder?.value === item.value" class="dropdown-check"></text>
</view>
<view v-if="!currentOrderOptions.length" class="dropdown-empty">{{ selectedPurpose === 'repair' ? '' : '' }}</view>
</scroll-view>
</view>
</view>
</view>
</view>
</view>
<!-- 出库数量 -->
<view class="qty-section">
<view class="section-title-bar">
<view class="section-bar-line"></view>
<text class="section-title">出库数量</text>
</view>
<view class="qty-input-card">
<view class="form-field">
<text class="form-label">出库数量</text>
<input
v-model="outboundQty"
class="form-input"
placeholder="请输入"
confirm-type="done"
/>
<text class="form-suffix-text">单位{{ textValue(selectedSparepart.purchaseUnitName) }}</text>
</view>
<view class="convert-row">
<text class="convert-label-inline">折算后库存数量</text>
<text class="convert-value-inline">{{ calculatedStock }}</text>
<text class="convert-unit-inline">{{ stockUnitLabel(selectedSparepart) }}</text>
</view>
<view class="convert-formula-inline">
{{ outboundQty || 0 }}{{ textValue(selectedSparepart.purchaseUnitName) }} × {{ textValue(selectedSparepart.purchaseUnitConvertQuantity) }}{{ stockUnitLabel(selectedSparepart) }} = {{ calculatedStock }}{{ stockUnitLabel(selectedSparepart) }}
</view>
</view>
<!-- 经办人 -->
<view class="section-title-bar" style="padding-top: 24rpx;">
<view class="section-bar-line"></view>
<text class="section-title">经办人</text>
</view>
<view class="select-row-card" @click="toggleOperatorDropdown">
<view class="full-dropdown">
<text :class="{ placeholder: !selectedOperator }">
{{ selectedOperator ? selectedOperator.label : '请选择经办人' }}
</text>
<text class="dropdown-arrow"></text>
</view>
<view v-if="showOperatorDropdown" class="dropdown-panel">
<scroll-view scroll-y class="dropdown-scroll">
<view
v-for="item in operatorOptions"
:key="item.value"
class="dropdown-item"
:class="{ active: selectedOperator?.value === item.value }"
@click.stop="handleSelectOperator(item)"
>
<text class="dropdown-item-text">{{ item.label }}</text>
<text v-if="selectedOperator?.value === item.value" class="dropdown-check"></text>
</view>
<view v-if="!operatorOptions.length" class="dropdown-empty"></view>
</scroll-view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-else class="empty-wrap">
<text class="empty-text">请扫码或选择备件</text>
</view>
<!-- 底部操作栏 -->
<view class="bottom-actions">
<view class="bottom-btn cancel-btn" @click="handleCancel"></view>
<view class="bottom-btn confirm-btn" @click="handleSubmit"></view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onShow, onHide } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getSimpleUserList } from '@/api/mes/moldget'
import { getDeviceLedgerList } from '@/api/mes/moldoperate'
import { getEquipmentRepairListByDeviceId, getEquipmentMaintenanceByDeviceId } from '@/api/mes/equipment'
import { getDvRepairPage } from '@/api/mes/dvrepair'
import { getSparepartDetail } from '@/api/mes/sparepart'
const { t } = useI18n()
const selectedSparepart = ref({})
const outboundQty = ref(null)
const selectedPurpose = ref('repair')
function setPurpose(value) {
selectedPurpose.value = value
//
selectedDevice.value = null
selectedRepairOrder.value = null
selectedMaintainOrder.value = null
repairOrderOptions.value = []
maintainOrderOptions.value = []
}
// -
const deviceOptions = ref([])
const selectedDevice = ref(null)
const showDeviceDropdown = ref(false)
//
const repairOrderOptions = ref([])
const selectedRepairOrder = ref(null)
//
const maintainOrderOptions = ref([])
const selectedMaintainOrder = ref(null)
//
const showOrderDropdown = ref(false)
const currentOrder = computed(() =>
selectedPurpose.value === 'repair' ? selectedRepairOrder.value : selectedMaintainOrder.value
)
const currentOrderOptions = computed(() =>
selectedPurpose.value === 'repair' ? repairOrderOptions.value : maintainOrderOptions.value
)
//
const operatorOptions = ref([])
const selectedOperator = ref(null)
const showOperatorDropdown = ref(false)
// images URL
const sparepartImage = computed(() => {
const images = selectedSparepart.value.images
if (!images) return ''
if (Array.isArray(images)) return String(images[0] || '')
return String(images).split(',')[0]?.trim() || ''
})
//
const calculatedStock = computed(() => {
const qty = Number(outboundQty.value) || 0
const ratio = Number(selectedSparepart.value.purchaseUnitConvertQuantity) || 0
return qty * ratio
})
function textValue(v) {
if (v === 0) return '0'
if (v == null) return '-'
const s = String(v).trim()
return s || '-'
}
function textUnit(v) {
if (v === 0) return '0'
if (v == null) return ''
return String(v).trim()
}
function stockUnitLabel(item) {
return item.unitName || item.minStockUnitName || '个'
}
function handleScan() {
uni.scanCode({
onlyFromCamera: true,
scanType: ['barCode', 'qrCode'],
success: (res) => {
console.log('扫码结果:', res.result)
uni.showToast({ title: '扫码成功: ' + res.result, icon: 'none' })
},
fail: () => {
uni.showToast({ title: '扫码失败', icon: 'none' })
}
})
}
function handleCancel() {
uni.navigateBack()
}
function handleSelectSparepart() {
uni.navigateTo({
url: '/pages_function/pages/sparepartInbound/sparepartSelect'
})
}
//
function toggleDeviceDropdown() {
showDeviceDropdown.value = !showDeviceDropdown.value
}
async function handleSelectDevice(item) {
selectedDevice.value = item
showDeviceDropdown.value = false
//
selectedRepairOrder.value = null
selectedMaintainOrder.value = null
repairOrderOptions.value = []
maintainOrderOptions.value = []
//
if (selectedPurpose.value === 'repair') {
loadRepairOrders(item.deviceCode)
} else {
loadMaintainOrders(item.deviceCode)
}
}
async function loadDevices() {
try {
const res = await getDeviceLedgerList({ pageNo: 1, pageSize: 100 })
const data = res && res.data !== undefined ? res.data : res
let list = []
if (Array.isArray(data)) { list = data }
else if (data && Array.isArray(data.data)) { list = data.data }
else if (data && data.data && Array.isArray(data.data.list)) { list = data.data.list }
else if (data && data.data && Array.isArray(data.data.rows)) { list = data.data.rows }
else if (data && data.data && Array.isArray(data.data.records)) { list = data.data.records }
else if (data && Array.isArray(data.list)) { list = data.list }
else if (data && Array.isArray(data.rows)) { list = data.rows }
else if (data && Array.isArray(data.records)) { list = data.records }
deviceOptions.value = list.map((d) => ({
value: d.id,
label: d.name || d.deviceName || d.deviceCode || String(d.id || ''),
deviceCode: d.deviceCode || d.code || '', //
deviceName: d.name || d.deviceName || ''
}))
} catch (e) {
console.error('loadDevices error', e)
}
}
// /
function toggleOrderDropdown() {
if (!selectedDevice.value) return
showOrderDropdown.value = !showOrderDropdown.value
}
function handleSelectOrder(item) {
if (selectedPurpose.value === 'repair') {
selectedRepairOrder.value = item
} else {
selectedMaintainOrder.value = item
}
showOrderDropdown.value = false
}
//
async function loadMaintainOrders(deviceCode) {
if (!deviceCode) {
const deviceId = selectedDevice.value?.value
if (deviceId) await loadMaintainOrdersById(deviceId)
return
}
try {
// ID
const deviceId = selectedDevice.value?.value
if (deviceId) await loadMaintainOrdersById(deviceId)
} catch (e) {
console.error('loadMaintainOrders error', e)
}
}
async function loadMaintainOrdersById(deviceId) {
if (!deviceId) return
try {
const res = await getEquipmentMaintenanceByDeviceId(deviceId)
const data = res && res.data !== undefined ? res.data : res
let list = []
if (Array.isArray(data)) { list = data }
else if (data && Array.isArray(data.data)) { list = data.data }
else if (data && data.data && Array.isArray(data.data.list)) { list = data.data.list }
else if (data && data.data && Array.isArray(data.data.rows)) { list = data.data.rows }
else if (data && data.data && Array.isArray(data.data.records)) { list = data.data.records }
else if (data && Array.isArray(data.list)) { list = data.list }
else if (data && Array.isArray(data.rows)) { list = data.rows }
else if (data && Array.isArray(data.records)) { list = data.records }
maintainOrderOptions.value = list.map((m) => ({
value: m.id,
label: m.maintenanceCode || m.code || m.maintenanceNo || m.no || String(m.id || '')
}))
console.log('保养单列表:', JSON.stringify(maintainOrderOptions.value))
} catch (e) {
console.error('loadMaintainOrdersById error', e)
}
}
//
async function loadRepairOrders(deviceCode) {
if (!deviceCode) {
// ID
const deviceId = selectedDevice.value?.value
if (deviceId) await loadRepairOrdersById(deviceId)
return
}
try {
const res = await getDvRepairPage({ machineryCode: deviceCode, pageNo: 1, pageSize: 100 })
const root = res && res.data !== undefined ? res.data : res
let list = []
if (Array.isArray(root)) { list = root }
else if (root && Array.isArray(root.data)) { list = root.data }
else if (root && root.data && Array.isArray(root.data.list)) { list = root.data.list }
else if (root && root.data && Array.isArray(root.data.rows)) { list = root.data.rows }
else if (root && root.data && Array.isArray(root.data.records)) { list = root.data.records }
else if (root && Array.isArray(root.list)) { list = root.list }
else if (root && Array.isArray(root.rows)) { list = root.rows }
repairOrderOptions.value = list.map((r) => ({
value: r.id,
label: r.repairCode || r.repairName || r.subjectName || String(r.id || '')
}))
console.log('维修单列表(分页):', JSON.stringify(repairOrderOptions.value))
} catch (e) {
console.error('loadRepairOrdersByDeviceCode error', e)
}
}
// ID
async function loadRepairOrdersById(deviceId) {
if (!deviceId) return
try {
const res = await getEquipmentRepairListByDeviceId(deviceId)
const data = res && res.data !== undefined ? res.data : res
let list = []
if (Array.isArray(data)) { list = data }
else if (data && Array.isArray(data.data)) { list = data.data }
else if (data && data.data && Array.isArray(data.data.list)) { list = data.data.list }
else if (data && data.data && Array.isArray(data.data.rows)) { list = data.data.rows }
else if (data && data.data && Array.isArray(data.data.records)) { list = data.data.records }
else if (data && Array.isArray(data.list)) { list = data.list }
else if (data && Array.isArray(data.rows)) { list = data.rows }
else if (data && Array.isArray(data.records)) { list = data.records }
repairOrderOptions.value = list.map((r) => ({
value: r.id,
label: r.repairName || r.repairCode || r.subjectName || String(r.id || '')
}))
console.log('维修单列表(设备ID):', JSON.stringify(repairOrderOptions.value))
} catch (e) {
console.error('loadRepairOrdersById error', e)
}
}
function toggleOperatorDropdown() {
showOperatorDropdown.value = !showOperatorDropdown.value
}
function handleSelectOperator(item) {
selectedOperator.value = item
showOperatorDropdown.value = false
}
async function loadOperators() {
try {
const res = await getSimpleUserList()
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
operatorOptions.value = data.map((u) => ({
value: u.id || u.userId,
label: u.nickname || u.userName || u.name || String(u.id || '')
}))
} catch (e) {
console.error('loadOperators error', e)
}
}
async function handleSubmit() {
if (!selectedSparepart.value.id) {
uni.showToast({ title: '请先选择备件', icon: 'none' })
return
}
if (!outboundQty.value || Number(outboundQty.value) <= 0) {
uni.showToast({ title: '请输入出库数量', icon: 'none' })
return
}
if (!selectedOperator.value) {
uni.showToast({ title: '请选择经办人', icon: 'none' })
return
}
if (!selectedPurpose.value) {
uni.showToast({ title: '请选择出库用途', icon: 'none' })
return
}
// TODO:
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
}
onShow(async () => {
const selectResult = getApp().globalData?._sparepartSelectResult
if (selectResult) {
selectedSparepart.value = selectResult
//
if (selectResult.id) {
try {
const res = await getSparepartDetail(selectResult.id)
const detail = res?.data || res
if (detail && detail.suppliers) {
selectedSparepart.value = { ...selectedSparepart.value, ...detail }
}
} catch (e) {
console.error('获取备件详情失败:', e)
}
}
outboundQty.value = null
getApp().globalData._sparepartSelectResult = null
}
loadOperators()
loadDevices()
})
onHide(() => {
showOperatorDropdown.value = false
showDeviceDropdown.value = false
showOrderDropdown.value = false
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: #f5f6f8;
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
}
.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: #1f4b79;
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; font-weight: 600; color: #fff; }
}
.sparepart-section { padding: 0; }
.section-title-bar { display: flex; align-items: center; gap: 10rpx; padding: 24rpx 24rpx 18rpx; }
.section-bar-line { width: 6rpx; height: 32rpx; border-radius: 3rpx; background: #2563eb; flex-shrink: 0; }
.section-title { font-size: 30rpx; font-weight: 700; color: #1a1a1a; }
.sparepart-card {
background: #fff; border-radius: 16rpx; padding: 24rpx; margin: 0 24rpx 20rpx;
box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.04);
}
.card-top { display: flex; position: relative; }
.card-image-wrap {
width: 160rpx; height: 160rpx; border-radius: 12rpx; overflow: hidden;
background: #f8fafc; border: 1rpx solid #f0f0f0; flex-shrink: 0; margin-right: 20rpx;
}
.card-image { width: 100%; height: 100%; }
.card-image-empty { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f8fafc; }
.empty-img-icon { font-size: 56rpx; opacity: 0.3; }
.card-info-right { flex: 1; min-width: 0; }
.info-item { display: flex; align-items: center; margin-bottom: 8rpx; }
.info-label { font-size: 26rpx; color: #6b7280; flex-shrink: 0; }
.info-name { font-size: 30rpx; font-weight: 700; color: #0f172a; }
.info-value { font-size: 26rpx; color: #374151; &.stock-highlight { color: #2563eb; font-weight: 500; } }
.card-bottom { margin-top: 20rpx; padding-top: 20rpx; border-top: 1rpx solid #f0f0f0; }
.detail-row { display: flex; align-items: center; margin-bottom: 14rpx;
&:last-child { margin-bottom: 0; }
&.two-col { justify-content: space-between; }
}
.detail-col { display: flex; align-items: center; flex: 1; }
.detail-label { font-size: 24rpx; color: #9ca3af; flex-shrink: 0; }
.detail-value { font-size: 24rpx; color: #4b5563; }
/* ====== 出库用途 ====== */
.purpose-card {
display: flex;
gap: 20rpx;
margin: 0 24rpx 20rpx;
}
.purpose-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
height: 92rpx;
background: #fff;
border: 2rpx solid #e5e7eb;
border-radius: 14rpx;
font-size: 28rpx;
color: #6b7280;
transition: all 0.2s;
&.active {
border-color: #2563eb;
background: #eff6ff;
color: #2563eb;
font-weight: 600;
}
}
.purpose-icon {
font-size: 32rpx;
}
.qty-section { margin-top: 4rpx; }
.qty-input-card {
background: #fff; border-radius: 16rpx; padding: 24rpx; margin: 0 24rpx;
box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.04);
}
.form-field { display: flex; flex-direction: column; gap: 12rpx; }
.form-label { font-size: 26rpx; color: #4b5563; font-weight: 500; }
.form-input { width: 100%; height: 88rpx; padding: 0 24rpx; font-size: 28rpx; color: #374151; background: #f8fafc; border-radius: 14rpx; box-sizing: border-box; }
.form-suffix-text { display: block; margin-top: 10rpx; font-size: 24rpx; color: #9ca3af; }
.convert-row { margin-top: 20rpx; padding-top: 20rpx; border-top: 1rpx solid #f0f0f0; display: flex; align-items: center; }
.convert-label-inline { font-size: 26rpx; color: #6b7280; }
.convert-value-inline { font-size: 36rpx; font-weight: 700; color: #1f2937; margin-left: auto; }
.convert-unit-inline { font-size: 24rpx; color: #64748b; margin-left: 8rpx; }
.convert-formula-inline { margin-top: 10rpx; font-size: 22rpx; color: #9ca3af; }
.select-row-card {
position: relative; display: flex; align-items: center; justify-content: space-between;
background: #fff; border-radius: 16rpx; padding: 24rpx; margin: 0 24rpx 20rpx;
box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.04);
}
.warehouse-area-rows { width: 100%; display: flex; flex-direction: column; gap: 16rpx; }
.warehouse-area-row { display: flex; align-items: center; }
.warehouse-area-label { width: 120rpx; font-size: 26rpx; color: #6b7280; flex-shrink: 0; }
.warehouse-area-dropdown { flex: 1; min-width: 0; position: relative;
.dropdown-panel { position: absolute; top: 100%; left: 0; right: 0; z-index: 200; margin-top: 4rpx; background: #fff; border: 1rpx solid #e0e0e0; border-radius: 12rpx; box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.15); overflow: hidden; }
}
.warehouse-area-card { align-items: flex-start; }
.full-dropdown {
display: flex; align-items: center; justify-content: space-between;
width: 100%; font-size: 28rpx; color: #374151;
.placeholder { color: #9ca3af; }
}
.dropdown-input { display: flex; align-items: center; height: 64rpx; padding: 0 20rpx; border: 1rpx solid #e0e0e0; border-radius: 10rpx; background: #f9fafb; }
.dropdown-value { flex: 1; font-size: 27rpx; color: #333; &.placeholder { color: #bbb; } }
.dropdown-arrow { font-size: 20rpx; color: #999; flex-shrink: 0; }
.dropdown-panel { position: absolute; top: 68rpx; left: 0; right: 0; z-index: 99; background: #fff; border: 1rpx solid #e0e0e0; border-radius: 12rpx; box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.1); overflow: hidden; }
.dropdown-scroll { max-height: 360rpx; }
.dropdown-item { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; border-bottom: 1rpx solid #f0f0f0;
&:last-child { border-bottom: 0; }
&.active { background: #f0f5ff; }
}
.dropdown-item-text { font-size: 27rpx; color: #333; }
.dropdown-check { font-size: 28rpx; color: #2563eb; font-weight: 700; }
.dropdown-empty { padding: 32rpx; text-align: center; color: #999; font-size: 26rpx; }
.empty-wrap { padding: 160rpx 24rpx; text-align: center; }
.empty-text { font-size: 28rpx; color: #94a3b8; }
.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;
}
.bottom-btn { flex: 1; height: 84rpx; line-height: 84rpx; text-align: center; border-radius: 16rpx; font-size: 30rpx; font-weight: 600;
&:active { opacity: 0.85; }
}
.cancel-btn { background: #eef2f7; color: #475569; }
.confirm-btn { background: #1f4b79; color: #ffffff; }
</style>

@ -0,0 +1,594 @@
<template>
<view class="page-container">
<NavBar :title="t('sparepartOutbound.moduleName')" />
<!-- 搜索栏 -->
<view class="filter-bar">
<view class="keyword-box">
<input
v-model="searchKeyword"
class="keyword-input"
:placeholder="t('sparepartOutbound.searchPlaceholder')"
confirm-type="search"
@input="handleKeywordInput"
@confirm="handleSearch"
/>
</view>
<picker mode="selector" :range="statusLabels" :value="statusIndex" @change="onStatusChange">
<view class="status-box">
<text class="status-box-text">{{ currentStatusLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
</picker>
<view class="reset-filter-btn" @click="resetFilters">{{ t('functionCommon.reset') }}</view>
</view>
<!-- 列表 -->
<scroll-view
scroll-y
class="list-scroll"
:scroll-top="scrollTop"
@scroll="onScroll"
@scrolltolower="loadMore"
:lower-threshold="80"
>
<view class="list-wrap">
<view
v-for="item in list"
:key="item.id"
class="task-card"
@click="openDetail(item)"
>
<view class="card-header">
<view class="header-main">
<view class="header-left">
<text class="task-no">{{ textValue(item.no) }}</text>
</view>
<view class="header-tags">
<text :class="['record-tag', statusClass(item.status)]">{{ statusText(item.status) }}</text>
</view>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('sparepartOutbound.sparepartInfo') }}</text>
<text class="value">{{ textValue(item.productNames) }}</text>
</view>
<view class="row">
<text class="label">{{ t('sparepartOutbound.outboundTime') }}</text>
<text class="value">{{ formatDateTime(item.outTime || item.createTime) }}</text>
</view>
<view class="row">
<text class="label">{{ t('sparepartOutbound.creator') }}</text>
<text class="value">{{ textValue(item.creatorName || item.creator) }}</text>
</view>
<view class="row">
<text class="label">{{ t('sparepartOutbound.quantity') }}</text>
<text class="value highlight">{{ textValue(item.totalCount) }}</text>
</view>
<view class="row">
<text class="label">{{ t('sparepartOutbound.reviewer') }}</text>
<text class="value">{{ textValue(item.auditUserName) }}</text>
</view>
</view>
<!-- 操作按钮待审核状态显示 -->
<view v-if="Number(item.status) === 10" class="card-actions">
<view class="action-btn approve-btn" @click.stop="handleApprove(item)">{{ t('sparepartOutbound.approve') }}</view>
<view class="action-btn reject-btn" @click.stop="handleReject(item)">{{ t('sparepartOutbound.reject') }}</view>
</view>
</view>
<view v-if="loading && pageNo === 1" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!list.length" class="hint">{{ t('sparepartOutbound.empty') }}</view>
<view v-else-if="loadingMore" class="hint">{{ t('functionCommon.loadingMore') }}</view>
<view v-else-if="finished" class="hint">{{ t('functionCommon.noMoreData') }}</view>
</view>
</scroll-view>
<!-- 回顶部 -->
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<uni-icons type="arrow-up" size="20" color="#1f4b79"></uni-icons>
</view>
<!-- 新增按钮 -->
<view class="add-btn" @click="goAdd">
<text class="add-icon">+</text>
</view>
</view>
</template>
<script setup>
import { computed, nextTick, ref } from 'vue'
import { onShow, onUnload } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getSparepartOutboundPage, auditSparepartOutbound } from '@/api/mes/sparepartOutbound'
const { t } = useI18n()
const selectedStatus = ref('')
const searchKeyword = ref('')
const statusOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
{ label: t('sparepartOutbound.tabPending'), value: '0' },
{ label: t('sparepartOutbound.tabAuditing'), value: '10' },
{ label: t('sparepartOutbound.approve'), value: '20' }
])
const statusLabels = computed(() => statusOptions.value.map((item) => item.label))
const statusIndex = computed(() => {
const index = statusOptions.value.findIndex((item) => item.value === selectedStatus.value)
return index >= 0 ? index : 0
})
const currentStatusLabel = computed(() => {
const current = statusOptions.value.find((item) => item.value === selectedStatus.value)
return current ? current.label : t('functionCommon.all')
})
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const finished = ref(false)
const pageNo = ref(1)
const pageSize = ref(10)
const scrollTop = ref(0)
const showGoTop = ref(false)
let searchTimer = null
function textValue(v) {
if (v === 0) return '0'
if (v == null) return '-'
const s = String(v).trim()
return s || '-'
}
function formatDateTime(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) {
const [year, month, day, hour = 0, minute = 0, second = 0] = value
const pad = (n) => String(n).padStart(2, '0')
return `${year}-${pad(month)}-${pad(day)} ${pad(hour)}:${pad(minute)}:${pad(second)}`
}
const date = new Date(Number(value))
if (Number.isNaN(date.getTime())) return String(value)
const pad = (n) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
}
const STATUS_MAP = { 0: '待出库', 10: '待审核', 20: '已通过', 1: '已驳回' }
function statusText(s) {
const num = Number(s)
return STATUS_MAP[num] || textValue(num)
}
function statusClass(s) {
const num = Number(s)
if (num === 0) return 'text-warning'
if (num === 10) return 'text-primary'
if (num === 20) return 'text-success'
if (num === 1) return 'text-danger'
return ''
}
function normalizePageData(res) {
const root = res && res.data !== undefined ? res.data : res
const candidateList = root?.list || root?.rows || root?.records || root?.data?.list || root?.data?.rows || []
const candidateTotal = root?.total ?? root?.data?.total ?? (Array.isArray(candidateList) ? candidateList.length : 0)
return {
list: Array.isArray(candidateList) ? candidateList : [],
total: Number(candidateTotal || 0)
}
}
async function fetchList(reset) {
if (reset) {
pageNo.value = 1
finished.value = false
}
if (pageNo.value === 1) {
loading.value = true
} else {
loadingMore.value = true
}
try {
const params = {
pageNo: pageNo.value,
pageSize: pageSize.value,
no: searchKeyword.value.trim() || undefined,
status: selectedStatus.value || undefined
}
const res = await getSparepartOutboundPage(params)
const page = normalizePageData(res)
list.value = reset ? page.list : [...list.value, ...page.list]
finished.value = list.value.length >= page.total || page.list.length < pageSize.value
} catch (e) {
if (!reset) pageNo.value = Math.max(1, pageNo.value - 1)
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
} finally {
loading.value = false
loadingMore.value = false
}
}
function onStatusChange(event) {
const index = Number(event?.detail?.value || 0)
selectedStatus.value = statusOptions.value[index]?.value ?? ''
fetchList(true)
}
function handleSearch() {
clearSearchTimer()
uni.hideKeyboard()
fetchList(true)
}
function handleKeywordInput() {
clearSearchTimer()
searchTimer = setTimeout(() => {
fetchList(true)
}, 300)
}
function resetFilters() {
clearSearchTimer()
searchKeyword.value = ''
selectedStatus.value = ''
fetchList(true)
}
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
}
function openDetail(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
uni.navigateTo({
url: `/pages_function/pages/sparepartOutbound/detail?id=${encodeURIComponent(String(item.id))}`
})
}
async function handleApprove(item) {
if (!item?.id) return
uni.showModal({
title: t('functionCommon.confirmTitle'),
content: t('sparepartOutbound.confirmApprove'),
confirmColor: '#16a34a',
success: async (res) => {
if (res.confirm) {
try {
await auditSparepartOutbound({ id: item.id, status: 20 })
uni.showToast({ title: t('sparepartOutbound.approveSuccess'), icon: 'success' })
fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
}
}
}
})
}
async function handleReject(item) {
if (!item?.id) return
uni.showModal({
title: t('functionCommon.confirmTitle'),
content: t('sparepartOutbound.confirmReject'),
confirmColor: '#dc2626',
success: async (res) => {
if (res.confirm) {
try {
await auditSparepartOutbound({ id: item.id, status: 1 })
uni.showToast({ title: t('sparepartOutbound.rejectSuccess'), icon: 'success' })
fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
}
}
}
})
}
function onScroll(event) {
showGoTop.value = (event?.detail?.scrollTop || 0) > 600
}
function goTop() {
scrollTop.value = 0
}
function goAdd() {
uni.navigateTo({
url: '/pages_function/pages/sparepartOutbound/create'
})
}
function clearSearchTimer() {
if (searchTimer) {
clearTimeout(searchTimer)
searchTimer = null
}
}
onShow(() => {
fetchList(true)
})
onUnload(() => {
clearSearchTimer()
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: #f4f5f7;
}
/* ====== 搜索栏 ====== */
.filter-bar {
display: flex;
align-items: center;
gap: 12rpx;
padding: 18rpx 24rpx;
background: #fff;
}
.keyword-box {
flex: 1;
min-width: 0;
height: 66rpx;
padding: 0 28rpx;
background: #f4f5f7;
border: 1rpx solid #e5e7eb;
border-radius: 12rpx;
display: flex;
align-items: center;
}
.keyword-input {
width: 100%;
font-size: 24rpx;
color: #374151;
}
.status-box {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
height: 66rpx;
padding: 0 28rpx;
min-width: 160rpx;
background: #fff;
border: 1rpx solid #e5e7eb;
border-radius: 12rpx;
}
.status-box-text {
font-size: 24rpx;
color: #374151;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.reset-filter-btn {
height: 66rpx;
line-height: 66rpx;
padding: 0 28rpx;
font-size: 24rpx;
color: #4b5563;
background: #fff;
border: 1rpx solid #e5e7eb;
border-radius: 12rpx;
flex-shrink: 0;
}
/* ====== 列表 ====== */
.list-scroll {
height: calc(100vh - 194rpx);
}
.list-wrap {
padding: 0 24rpx 60rpx;
}
.task-card {
position: relative;
margin-top: 20rpx;
padding: 28rpx;
background: #fff;
border-radius: 22rpx;
box-shadow: 0 8rpx 28rpx rgba(15, 23, 42, 0.06);
}
.card-header {
margin-bottom: 18rpx;
}
.header-main {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.header-left {
min-width: 0;
flex: 1;
}
.task-no {
font-size: 32rpx;
font-weight: 700;
color: #0f172a;
}
.header-tags {
display: flex;
align-items: center;
gap: 12rpx;
flex-shrink: 0;
}
.record-tag {
padding: 8rpx 18rpx;
border-radius: 999rpx;
font-size: 22rpx;
line-height: 1;
background: #e2e8f0;
color: #64748b;
}
.card-body .row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 20rpx;
margin-top: 12rpx;
&:first-child {
margin-top: 0;
}
}
.label {
width: 140rpx;
font-size: 25rpx;
color: #94a3b8;
flex-shrink: 0;
}
.value {
flex: 1;
text-align: right;
font-size: 27rpx;
color: #334155;
line-height: 1.5;
&.highlight {
color: #1f4b79;
font-weight: 600;
}
}
/* ====== 操作按钮 ====== */
.card-actions {
display: flex;
gap: 16rpx;
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #f0f0f0;
}
.action-btn {
flex: 1;
height: 64rpx;
line-height: 64rpx;
text-align: center;
border-radius: 10rpx;
font-size: 26rpx;
font-weight: 500;
&.approve-btn {
background: #dcfce7;
color: #16a34a;
}
&.reject-btn {
background: #fee2e2;
color: #dc2626;
}
&:active {
opacity: 0.8;
}
}
/* ====== 状态标签颜色 ====== */
.text-success {
color: #16a34a;
}
.text-danger {
color: #dc2626;
}
.text-warning {
color: #d97706;
}
.text-primary {
color: #2563eb;
}
.record-tag.text-success {
color: #15803d;
background: #dcfce7;
}
.record-tag.text-danger {
color: #dc2626;
background: #fee2e2;
}
.record-tag.text-warning {
color: #d97706;
background: #fef3c7;
}
.record-tag.text-primary {
color: #1d4ed8;
background: #dbeafe;
}
/* ====== 提示 ====== */
.hint {
padding: 36rpx 0;
text-align: center;
color: #94a3b8;
font-size: 26rpx;
}
/* ====== 回顶部 ====== */
.go-top-btn {
position: fixed;
right: 28rpx;
bottom: calc(140rpx + env(safe-area-inset-bottom));
width: 92rpx;
height: 92rpx;
border-radius: 46rpx;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.12);
display: flex;
align-items: center;
justify-content: center;
}
/* ====== 新增按钮 ====== */
.add-btn {
position: fixed;
right: 28rpx;
bottom: calc(56rpx + env(safe-area-inset-bottom));
width: 92rpx;
height: 92rpx;
border-radius: 46rpx;
background: #1f4b79;
box-shadow: 0 14rpx 30rpx rgba(24, 63, 108, 0.24);
display: flex;
align-items: center;
justify-content: center;
}
.add-icon {
color: #ffffff;
font-size: 64rpx;
line-height: 1;
margin-top: -4rpx;
}
</style>

@ -178,7 +178,7 @@ function dialogClose() {
.server-address-text {
max-width: 400rpx;
text-align: right;
// text-align: right;
word-break: break-all;
}
</style>

@ -1,3 +1,4 @@
import { getCurrentLocale } from '@/locales'
const DIRECT_ROUTE_PREFIXES = ['pages/', 'page_', 'pages_']
@ -87,6 +88,12 @@ const MENU_ROUTE_MAP = {
mold: '/pages_function/pages/mold/index',
equipment: '/pages_function/pages/equipment/index',
spare: '/pages_function/pages/spare/index',
sparepartInbound: '/pages_function/pages/sparepartInbound/index',
sparepartoutbound: '/pages_function/pages/sparepartOutbound/index',
'备件出库': '/pages_function/pages/sparepartOutbound/index',
sparepartinbound: '/pages_function/pages/sparepartInbound/index',
sparepartIn: '/pages_function/pages/sparepartInbound/index',
'备件入库': '/pages_function/pages/sparepartInbound/index',
keypart: '/pages_function/pages/keypart/index',
product: '/pages_function/pages/product/index'
}
@ -185,7 +192,7 @@ export function getConfigurableNavMenus(menus) {
export function buildNavMenuViewModels(menus) {
return getConfigurableNavMenus(menus).map((menu, index) => {
const displayName = String(menu.name || menu.enName || '').trim() || `菜单${index + 1}`
const displayName = getLocalizedMenuName(menu, `菜单${index + 1}`)
return {
...menu,
displayName,
@ -305,7 +312,8 @@ export function resolveMenuUrl(menu) {
return directRoute
}
const keys = [menu?.component, menu?.path, menu?.enName, menu?.name]
// 优先用 menu.name中文最不容易冲突再用 component/path
const keys = [menu?.name, menu?.enName, menu?.component, menu?.path]
for (const key of keys) {
const normalizedKey = normalizeMenuKey(key)
if (normalizedKey && MENU_ROUTE_MAP[normalizedKey]) {
@ -328,13 +336,22 @@ export function getMenuSymbol(name, index) {
return MENU_SYMBOLS[index % MENU_SYMBOLS.length]
}
export function getLocalizedMenuName(menu, fallback = '') {
const name = String(menu?.name || '').trim()
const enName = String(menu?.enName || '').trim()
if (getCurrentLocale() === 'en-US') {
return enName || name || fallback
}
return name || enName || fallback
}
export function syncTabBarMenus(menus, options = {}) {
const reportMenu = findTabMenuByPage(menus, 'pages/report')
const workMenu = findTabMenuByPage(menus, 'pages/work')
return [
options.homeText || '首页',
reportMenu?.name || options.reportFallback || '报表',
workMenu?.name || reportMenu?.name || options.workFallback || '管理',
getLocalizedMenuName(reportMenu, options.reportFallback || '报表'),
getLocalizedMenuName(workMenu, getLocalizedMenuName(reportMenu, options.workFallback || '管理')),
options.mineText || '我的'
]
}

Loading…
Cancel
Save