Compare commits

...

77 Commits
main ... newUI

Author SHA1 Message Date
黄伟杰 c33d37ea23 style:修改菜单页面样式 2 months ago
黄伟杰 b2cb9aeee0 style:我的-删除没用菜单 2 months ago
黄伟杰 d0d4430b56 style:功能导航-入口icon颜色修改 2 months ago
黄伟杰 8392b5d63a style:首页-功能导航-更多-底部弹框添加过度动画 2 months ago
黄伟杰 b5f7848ede config:修改打包后端地址 2 months ago
黄伟杰 c2151b7c2f feat:添加empty组件 2 months ago
黄伟杰 55796a6a41 style:底部导航栏及滚动优化 2 months ago
黄伟杰 1846e783a3 refactor:更换uview-plus tabbar组件 2 months ago
黄伟杰 bdd15e5104 refactor:重构navbar,改用uview-plus版本 2 months ago
黄伟杰 4bc2589ced style:功能导航模块交互优化 2 months ago
黄伟杰 af78c2ffa3 refactor:重构功能导航模块 2 months ago
黄伟杰 b849d644ee config:添加打包环境配置 2 months ago
黄伟杰 b0004e30c7 refactor:重构navbar 2 months ago
黄伟杰 e632a6dd8f fix:修复setNavigationBarTitle调用后导致背景颜色被重置问题 2 months ago
黄伟杰 a97d9223b6 feat:报表/管理菜单动态渲染 2 months ago
黄伟杰 d16f397336 fix:修复h5端与app编译报错问题 2 months ago
黄伟杰 15912db748 style:登录页样式重构 2 months ago
黄伟杰 bcd9a4e734 style:修改config地址 2 months ago
黄伟杰 298f8743da style:产品合格率排行-label换行展示 2 months ago
黄伟杰 3a3ac5de5a style:首页-质量/生产切换按钮样式调整 2 months ago
黄伟杰 1caaeb935b style:产品合格率图表优化 2 months ago
黄伟杰 ab45f7708c style:首页-生产/质量切换按钮优化 2 months ago
黄伟杰 24b91e495c feat:首页添加质量模块 2 months ago
黄伟杰 6c8eef9771 style:修改打包后端地址 3 months ago
黄伟杰 1df0eda266 feat:生产概括-查看更多添加筛选条件 3 months ago
黄伟杰 7741a50eac style:生产概括-任务-添加两个卡片展示、横向滑动 3 months ago
黄伟杰 f36f51b2f7 style:产品物料模块-手动输入编码方式传参改用code 3 months ago
黄伟杰 a7ac7d8ad7 style:备件模块-手动输入编码方式传参改用code 3 months ago
黄伟杰 220489ef43 style:关键件模块-手动输入编码方式传参改用code 3 months ago
黄伟杰 2faec8ef4c style:设备模块-手动输入编码方式传参改用code 3 months ago
黄伟杰 bf62c5df31 style:模具模块-手动输入编码方式传参改用code 3 months ago
黄伟杰 a3c8c7e412 style:模具台账-编码添加复制按钮 3 months ago
黄伟杰 38ab4c77b2 style:模具类型-字段调整、添加复制按钮 3 months ago
黄伟杰 7b5b45d8f8 style:管理页面样式修改 3 months ago
黄伟杰 9c47239a9f style:生产整体概况-字段调整 3 months ago
黄伟杰 61e06971f1 feat:新增设备分类、设备台账、设备关键件 3 months ago
黄伟杰 3f496dea47 feat:新增产品管理模块 3 months ago
黄伟杰 493fd0125f feat:新增模具管理-点检记录 3 months ago
黄伟杰 929dea162b feat:新增模具管理-点检任务 3 months ago
黄伟杰 b4b7c1359c feat:新增模具管理-点检模板 3 months ago
黄伟杰 c89ce1b06b style:修改字典枚举 3 months ago
黄伟杰 d3565c0b44 style:模具管理模块图标更改 3 months ago
黄伟杰 6dbe599eda style:首页-设备模块样式调整 3 months ago
黄伟杰 a5291165db style:修改打包接口地址 3 months ago
黄伟杰 99bf9b3b4d style:图表样式修改 3 months ago
黄伟杰 a9fa9b4f02 Merge branch 'master' of https://git.ngsk.tech/linweidong/besure_app 3 months ago
黄伟杰 6ff98266bc feat:下拉框组件更换,用uniapp官方的 3 months ago
HuangHuiKang 65fff2a1c8 chore:添加dockerfile和nginx 3 months ago
黄伟杰 853472a7da style:更换报表logo 3 months ago
黄伟杰 6cce45cb0e feat:设备模块添加图表 3 months ago
黄伟杰 5244e54162 style:首页loading优化 3 months ago
黄伟杰 6bf11ff41c feat:设备概括模块 3 months ago
黄伟杰 e41283295b perf:首页-组件化 3 months ago
黄伟杰 b1e6d3af72 feat:任务/产品查看更多页面 3 months ago
黄伟杰 d3dfc45074 feat:首页添加任务、产品展示模块 3 months ago
黄伟杰 22179e1eac feat:模具管理-添加点检项库 4 months ago
黄伟杰 d537a23660 style:修改模具出库、模具入库、上下模卡片样式 4 months ago
黄伟杰 5aab6cad57 feat:添加上下模模块 4 months ago
黄伟杰 f45f518c2a feat:添加模具入库模块 4 months ago
黄伟杰 e3dec8568e feat:添加模具出库模块 4 months ago
黄伟杰 3f17d6317f style:添加中英文切换 4 months ago
黄伟杰 0ea15894bd feat:模具管理-添加模具类型/模具台账模块 4 months ago
黄伟杰 1514cdf7b4 style:功能导航-扫一扫添加二维码类型判断 4 months ago
黄伟杰 f8082bd587 style:修改设备、关键件、备件、产品物料传参 4 months ago
黄伟杰 207ff2ce3b style:顶部导航按钮样式调整 4 months ago
黄伟杰 808f0838ab style:首页-生产计划-展开收起列表按钮 4 months ago
黄伟杰 6c4e9d5e8c feat:封装header及bottombtn组件 4 months ago
黄伟杰 d5710f1e87 feat:添加产品物料模块,扫一扫、详情 4 months ago
黄伟杰 41fd0348bf feat:备件模块添加扫一扫、对接接口 4 months ago
黄伟杰 878304b2c9 feat:设备关键件对接接口 4 months ago
黄伟杰 1fd1cf21f8 feat:设备模块添加扫一扫、对接接口 4 months ago
黄伟杰 ec7962d050 feat:模具扫码/详情对接接口 4 months ago
黄伟杰 f181fa9986 style:app端代码兼容性修改 4 months ago
黄伟杰 0d173acd51 feat:模具查询-扫一扫功能 4 months ago
黄伟杰 93da020626 feat:首页模块对接接口 4 months ago
黄伟杰 b943caa4f9 style:管理-页面返回顶部按钮逻辑修改 4 months ago
kkk-ops 15025df2f5 commit 4 months ago

@ -0,0 +1 @@
VITE_APP_BASE_URL=http://192.168.5.106:48081

@ -0,0 +1 @@
VITE_APP_BASE_URL=

@ -0,0 +1,10 @@
FROM nginx:alpine
RUN rm -f /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY dist/build/h5 /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

@ -0,0 +1,70 @@
server {
listen 80;
server_name localhost;
client_max_body_size 100m;
location /api/ {
proxy_pass http://besure_server:48081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /admin-api/ {
proxy_pass http://besure_server:48081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /jmreport/ {
proxy_pass http://besure_server:48081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /v3/api-docs/ {
proxy_pass http://besure_server:48081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /doc.html {
proxy_pass http://besure_server:48081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /swagger-ui/ {
proxy_pass http://besure_server:48081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /webjars/ {
proxy_pass http://besure_server:48081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location ~ /v3/api-docs/.*\.json$ {
proxy_pass http://besure_server:48081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}

21455
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -66,7 +66,7 @@
"mqtt": "4.1.0",
"pinia": "2.0.17",
"tslib": "^2.6.2",
"uview-plus": "^3.1.45",
"uview-plus": "^3.7.36",
"vue": "3.4.23",
"vue-i18n": "9.10.2"
},
@ -76,13 +76,21 @@
"@dcloudio/uni-cli-shared": "3.0.0-4020320240708001",
"@dcloudio/uni-stacktracey": "3.0.0-4020320240708001",
"@dcloudio/vite-plugin-uni": "3.0.0-4020320240708001",
"@types/node": "18.19.130",
"@vue/runtime-core": "^3.4.23",
"@vue/tsconfig": "^0.1.3",
"less": "^4.2.0",
"sass": "^1.77.7",
"sass": "1.56.1",
"sass-loader": "^10.1.1",
"typescript": "^4.9.5",
"vite": "5.2.8",
"vue-tsc": "^1.8.8"
},
"pnpm": {
"overrides": {
"@intlify/core-base": "9.10.2",
"@intlify/message-compiler": "9.10.2",
"@intlify/shared": "9.10.2"
}
}
}

File diff suppressed because it is too large Load Diff

@ -1,14 +1,63 @@
<script>
import { getToken } from '@/utils/auth'
import { initializeLocale, translateLiteral } from '@/locales'
import useUserStore from '@/store/modules/user'
let wrapped = false
function wrapUniTextApi() {
if (wrapped) return
wrapped = true
const rawShowToast = uni.showToast
const rawShowModal = uni.showModal
const rawShowLoading = uni.showLoading
uni.showToast = (options = {}) => {
const next = { ...options }
if (typeof next.title === 'string') {
next.title = translateLiteral(next.title)
}
return rawShowToast(next)
}
uni.showLoading = (options = {}) => {
const next = { ...options }
if (typeof next.title === 'string') {
next.title = translateLiteral(next.title)
}
return rawShowLoading(next)
}
uni.showModal = (options = {}) => {
const next = { ...options }
if (typeof next.title === 'string') {
next.title = translateLiteral(next.title)
}
if (typeof next.content === 'string') {
next.content = translateLiteral(next.content)
}
if (typeof next.confirmText === 'string') {
next.confirmText = translateLiteral(next.confirmText)
}
if (typeof next.cancelText === 'string') {
next.cancelText = translateLiteral(next.cancelText)
}
return rawShowModal(next)
}
}
export default {
onLaunch: function () {
console.log('App Launch')
initializeLocale()
wrapUniTextApi()
if (getToken()) {
useUserStore().getInfo().catch(() => {})
}
},
onShow: function () {
console.log('App Show')
initializeLocale()
wrapUniTextApi()
},
onHide: function () {
console.log('App Hide')
}
}
</script>

@ -0,0 +1,48 @@
import request from '@/utils/request'
export function getProductCategoryList(params) {
return request({
url: '/admin-api/erp/product-category/list',
method: 'get',
params
})
}
export function getProductCategorySimpleList() {
return request({
url: '/admin-api/erp/product-category/simple-list',
method: 'get'
})
}
export function getProductCategory(id) {
return request({
url: '/admin-api/erp/product-category/get',
method: 'get',
params: { id }
})
}
export function createProductCategory(data) {
return request({
url: '/admin-api/erp/product-category/create',
method: 'post',
data
})
}
export function updateProductCategory(data) {
return request({
url: '/admin-api/erp/product-category/update',
method: 'put',
data
})
}
export function deleteProductCategory(id) {
return request({
url: '/admin-api/erp/product-category/delete',
method: 'delete',
params: { id }
})
}

@ -0,0 +1,70 @@
import request from '@/utils/request'
export function getProductPage(params) {
return request({
url: '/admin-api/erp/product/page',
method: 'get',
params
})
}
export function getProductSimpleList(params) {
return request({
url: '/admin-api/erp/product/simple-list-all',
method: 'get',
params
})
}
export function getMesProductSimpleList() {
return request({
url: '/admin-api/erp/product/simple-list-product',
method: 'get'
})
}
export function getItemSimpleList() {
return request({
url: '/admin-api/erp/product/simple-list-item',
method: 'get'
})
}
export function getProduct(id) {
return request({
url: '/admin-api/erp/product/get',
method: 'get',
params: { id }
})
}
export function createProduct(data) {
return request({
url: '/admin-api/erp/product/create',
method: 'post',
data
})
}
export function updateProduct(data) {
return request({
url: '/admin-api/erp/product/update',
method: 'put',
data
})
}
export function deleteProduct(id) {
return request({
url: '/admin-api/erp/product/delete',
method: 'delete',
params: { id }
})
}
export function getProductUnitSimpleList() {
return request({
url: '/admin-api/erp/product-unit/simple-list',
method: 'get'
})
}

@ -33,14 +33,27 @@ export function register(data) {
})
}
// 获取用户详细信息
export function getInfo() {
function getPermissionInfo(params = {}) {
return request({
url: '/admin-api/system/auth/get-permission-info',
method: 'get'
method: 'get',
params: {
clientType: 2,
...params
}
})
}
// 获取用户详细信息
export function getInfo() {
return getPermissionInfo()
}
// 获取功能导航菜单
export function getNavPermissionInfo() {
return getPermissionInfo()
}
// 退出方法
export function logout() {
return request({

@ -0,0 +1,48 @@
import request from '@/utils/request'
export function getCriticalComponentPage(params) {
return request({
url: '/admin-api/mes/critical-component/page',
method: 'get',
params
})
}
export function getCriticalComponent(id) {
return request({
url: '/admin-api/mes/critical-component/get',
method: 'get',
params: { id }
})
}
export function createCriticalComponent(data) {
return request({
url: '/admin-api/mes/critical-component/create',
method: 'post',
data
})
}
export function updateCriticalComponent(data) {
return request({
url: '/admin-api/mes/critical-component/update',
method: 'put',
data
})
}
export function deleteCriticalComponent(ids) {
return request({
url: '/admin-api/mes/critical-component/delete',
method: 'delete',
params: { ids }
})
}
export function getCriticalComponentList() {
return request({
url: '/admin-api/mes/critical-component/list',
method: 'get'
})
}

@ -0,0 +1,48 @@
import request from '@/utils/request'
export function getDeviceLedgerPage(params) {
return request({
url: '/admin-api/mes/device-ledger/page',
method: 'get',
params
})
}
export function getDeviceLedger(id) {
return request({
url: '/admin-api/mes/device-ledger/get',
method: 'get',
params: { id }
})
}
export function createDeviceLedger(data) {
return request({
url: '/admin-api/mes/device-ledger/create',
method: 'post',
data
})
}
export function updateDeviceLedger(data) {
return request({
url: '/admin-api/mes/device-ledger/update',
method: 'put',
data
})
}
export function deleteDeviceLedger(ids) {
return request({
url: '/admin-api/mes/device-ledger/delete',
method: 'delete',
params: { ids }
})
}
export function getDeviceLedgerList() {
return request({
url: '/admin-api/mes/device-ledger/list',
method: 'get'
})
}

@ -0,0 +1,49 @@
import request from '@/utils/request'
export function getDeviceTypeTree(params) {
return request({
url: '/admin-api/mes/device-type/tree',
method: 'get',
params
})
}
export function getDeviceTypeList(params) {
return request({
url: '/admin-api/mes/device-type/page',
method: 'get',
params
})
}
export function getDeviceType(id) {
return request({
url: '/admin-api/mes/device-type/get',
method: 'get',
params: { id }
})
}
export function createDeviceType(data) {
return request({
url: '/admin-api/mes/device-type/create',
method: 'post',
data
})
}
export function updateDeviceType(data) {
return request({
url: '/admin-api/mes/device-type/update',
method: 'put',
data
})
}
export function deleteDeviceType(id) {
return request({
url: '/admin-api/mes/device-type/delete',
method: 'delete',
params: { id }
})
}

@ -0,0 +1,33 @@
import request from '@/utils/request'
export function getEquipmentDetail(id, params = {}) {
return request({
url: '/admin-api/mes/device-ledger/get',
method: 'get',
params: { id, ...params }
})
}
export function getEquipmentInspectionByDeviceId(deviceId, params = {}) {
return request({
url: '/admin-api/mes/ticket-management/getInspectionByDeviceId',
method: 'get',
params: { deviceId, ...params }
})
}
export function getEquipmentMaintenanceByDeviceId(deviceId, params = {}) {
return request({
url: '/admin-api/mes/ticket-management/getMaintenanceByDeviceId',
method: 'get',
params: { deviceId, ...params }
})
}
export function getEquipmentRepairListByDeviceId(deviceId, params = {}) {
return request({
url: '/admin-api/mes/dv-repair/getRepairListByDeviceId',
method: 'get',
params: { deviceId, ...params }
})
}

@ -0,0 +1,137 @@
import upload from '@/utils/upload'
import request from '@/utils/request'
export function getMoldDetail(id, params = {}) {
return request({
url: '/admin-api/erp/mold-brand/mold/get',
method: 'get',
params: { id, ...params }
})
}
export function getMoldInspectionByMoldId(moldId, params = {}) {
return request({
url: '/admin-api/mes/mold-ticket-management/getInspectionByMoldId',
method: 'get',
params: { moldId, ...params }
})
}
export function getMoldMaintenanceByMoldId(moldId, params = {}) {
return request({
url: '/admin-api/mes/mold-ticket-management/getMaintenanceByMoldId',
method: 'get',
params: { moldId, ...params }
})
}
export function getMoldRepairListByMoldId(moldId, params = {}) {
return request({
url: '/admin-api/mes/mold-repair/getRepairListByMoldId',
method: 'get',
params: { moldId, ...params }
})
}
export function getMoldBrandPage(params = {}) {
return request({
url: '/admin-api/erp/mold-brand/page',
method: 'get',
params
})
}
export function getMoldBrandDetail(id) {
return request({
url: '/admin-api/erp/mold-brand/get',
method: 'get',
params: { id }
})
}
export function getMoldBrandTree() {
return request({
url: '/admin-api/erp/mold-brand/tree',
method: 'get'
})
}
export function getMoldPage(params = {}) {
return request({
url: '/admin-api/erp/mold-brand/mold/page',
method: 'get',
params
})
}
export function getMoldList(params = {}) {
return request({
url: '/admin-api/erp/mold-brand/mold/list',
method: 'get',
params
})
}
export function getInTransitMoldAllList() {
return request({
url: '/admin-api/erp/mold/getInTransitMoldAllList',
method: 'get'
})
}
export function createMoldBrand(data) {
return request({
url: '/admin-api/erp/mold-brand/create',
method: 'post',
data
})
}
export function updateMoldBrand(data) {
return request({
url: '/admin-api/erp/mold-brand/update',
method: 'put',
data
})
}
export function deleteMoldBrand(id) {
return request({
url: '/admin-api/erp/mold-brand/delete',
method: 'delete',
params: { id }
})
}
export function createMold(data) {
return request({
url: '/admin-api/erp/mold-brand/mold/create',
method: 'post',
data
})
}
export function updateMold(data) {
return request({
url: '/admin-api/erp/mold-brand/mold/update',
method: 'put',
data
})
}
export function deleteMold(id) {
return request({
url: '/admin-api/erp/mold-brand/mold/delete',
method: 'delete',
params: { id }
})
}
export function uploadMoldImage(filePath, name = 'file') {
return upload({
url: '/admin-api/infra/file/upload',
name,
filePath,
showLoading: false
})
}

@ -0,0 +1,41 @@
import request from '@/utils/request'
export function getMoldInspectionItemPage(params = {}) {
return request({
url: '/admin-api/mes/mold-subject/page',
method: 'get',
params
})
}
export function getMoldInspectionItemDetail(id) {
return request({
url: '/admin-api/mes/mold-subject/get',
method: 'get',
params: { id }
})
}
export function createMoldInspectionItem(data) {
return request({
url: '/admin-api/mes/mold-subject/create',
method: 'post',
data
})
}
export function updateMoldInspectionItem(data) {
return request({
url: '/admin-api/mes/mold-subject/update',
method: 'put',
data
})
}
export function deleteMoldInspectionItem(ids = []) {
return request({
url: '/admin-api/mes/mold-subject/delete',
method: 'delete',
params: { ids: Array.isArray(ids) ? ids.join(',') : String(ids) }
})
}

@ -0,0 +1,48 @@
import request from '@/utils/request'
export function getMoldInspectionPlanPage(params = {}) {
return request({
url: '/admin-api/mes/mold-plan-maintenance/page',
method: 'get',
params
})
}
export function getMoldInspectionPlanDetail(id) {
return request({
url: '/admin-api/mes/mold-plan-maintenance/getSubjectList',
method: 'get',
params: { id }
})
}
export function createMoldInspectionPlan(data) {
return request({
url: '/admin-api/mes/mold-plan-maintenance/create',
method: 'post',
data
})
}
export function updateMoldInspectionPlan(data) {
return request({
url: '/admin-api/mes/mold-plan-maintenance/update',
method: 'put',
data
})
}
export function deleteMoldInspectionPlan(ids = []) {
return request({
url: '/admin-api/mes/mold-plan-maintenance/delete',
method: 'delete',
params: { ids: Array.isArray(ids) ? ids.join(',') : String(ids) }
})
}
export function getMoldSubjectAllList() {
return request({
url: '/admin-api/mes/mold-subject/getAllList',
method: 'get'
})
}

@ -0,0 +1,41 @@
import request from '@/utils/request'
export function getTaskManagementPage(params = {}) {
return request({
url: '/admin-api/mes/mold-task-management/page',
method: 'get',
params
})
}
export function createTaskManagement(data) {
return request({
url: '/admin-api/mes/mold-task-management/create',
method: 'post',
data
})
}
export function updateTaskManagement(data) {
return request({
url: '/admin-api/mes/mold-task-management/update',
method: 'put',
data
})
}
export function deleteTaskManagement(ids = []) {
return request({
url: '/admin-api/mes/mold-task-management/delete',
method: 'delete',
params: { ids: Array.isArray(ids) ? ids.join(',') : String(ids) }
})
}
export function createTaskManagementTicket(id) {
return request({
url: '/admin-api/mes/mold-task-management/createMoldTicket',
method: 'post',
params: { id }
})
}

@ -0,0 +1,33 @@
import request from '@/utils/request'
export function getTicketManagementPage(params = {}) {
return request({
url: '/admin-api/mes/mold-ticket-management/page',
method: 'get',
params
})
}
export function batchUpdateTicketStatus(data) {
return request({
url: '/admin-api/mes/mold-ticket-management/batchUpdateStatus',
method: 'put',
data
})
}
export function getTicketResultsPage(params = {}) {
return request({
url: '/admin-api/mes/mold-ticket-results/page',
method: 'get',
params
})
}
export function batchUpdateTicketResults(data) {
return request({
url: '/admin-api/mes/mold-ticket-results/batchUpdate',
method: 'put',
data
})
}

@ -0,0 +1,63 @@
import request from '@/utils/request'
export function getMoldGetPage(params = {}) {
return request({
url: '/admin-api/erp/stock-out/page',
method: 'get',
params
})
}
export function getMoldGetDetail(id) {
return request({
url: '/admin-api/erp/stock-out/get',
method: 'get',
params: { id }
})
}
export function createMoldGet(data) {
return request({
url: '/admin-api/erp/stock-out/create',
method: 'post',
data
})
}
export function updateMoldGet(data) {
return request({
url: '/admin-api/erp/stock-out/update',
method: 'put',
data
})
}
export function updateMoldGetStatus(id, status) {
return request({
url: '/admin-api/erp/stock-out/update-status',
method: 'put',
params: { id, status }
})
}
export function deleteMoldGet(ids = []) {
return request({
url: '/admin-api/erp/stock-out/delete',
method: 'delete',
params: { ids: ids.join(',') }
})
}
export function getWarehouseSimpleList() {
return request({
url: '/admin-api/erp/warehouse/simple-list',
method: 'get'
})
}
export function getSimpleUserList() {
return request({
url: '/admin-api/system/user/simple-list',
method: 'get'
})
}

@ -0,0 +1,57 @@
import request from '@/utils/request'
export function getMoldOperatePage(params = {}) {
return request({
url: '/admin-api/mes/mold-operate/page',
method: 'get',
params
})
}
export function getMoldOperateDetail(id) {
return request({
url: '/admin-api/mes/mold-operate/get',
method: 'get',
params: { id }
})
}
export function createMoldOperate(data) {
return request({
url: '/admin-api/mes/mold-operate/create',
method: 'post',
data
})
}
export function updateMoldOperate(data) {
return request({
url: '/admin-api/mes/mold-operate/update',
method: 'put',
data
})
}
export function deleteMoldOperate(id) {
return request({
url: '/admin-api/mes/mold-operate/delete',
method: 'delete',
params: { id }
})
}
export function getLowerMoldList(id) {
return request({
url: '/admin-api/mes/mold-operate/getLowerMoldList',
method: 'get',
params: { id }
})
}
export function getDeviceLedgerList(params = {}) {
return request({
url: '/admin-api/mes/device-ledger/list',
method: 'get',
params
})
}

@ -0,0 +1,49 @@
import request from '@/utils/request'
export function getMoldReturnPage(params = {}) {
return request({
url: '/admin-api/erp/stock-in/page',
method: 'get',
params
})
}
export function getMoldReturnDetail(id) {
return request({
url: '/admin-api/erp/stock-in/get',
method: 'get',
params: { id }
})
}
export function createMoldReturn(data) {
return request({
url: '/admin-api/erp/stock-in/create',
method: 'post',
data
})
}
export function updateMoldReturn(data) {
return request({
url: '/admin-api/erp/stock-in/update',
method: 'put',
data
})
}
export function updateMoldReturnStatus(id, status) {
return request({
url: '/admin-api/erp/stock-in/update-status',
method: 'put',
params: { id, status }
})
}
export function deleteMoldReturn(ids = []) {
return request({
url: '/admin-api/erp/stock-in/delete',
method: 'delete',
params: { ids: ids.join(',') }
})
}

@ -9,6 +9,13 @@ export function getUnitList() {
method: 'get'
})
}
export function getProductUnitSimpleList() {
return request({
url: '/admin-api/erp/product-unit/simple-list',
method: 'get'
})
}
// 原料列表
export function getItemList() {
return request({

@ -0,0 +1,49 @@
import request from '@/utils/request'
export function getBomPage(params) {
return request({
url: '/admin-api/mes/bom/page',
method: 'get',
params
})
}
export function getBom(id) {
return request({
url: '/admin-api/mes/bom/get',
method: 'get',
params: { id }
})
}
export function createBom(data) {
return request({
url: '/admin-api/mes/bom/create',
method: 'post',
data
})
}
export function updateBom(data) {
return request({
url: '/admin-api/mes/bom/update',
method: 'put',
data
})
}
export function deleteBom(id) {
return request({
url: '/admin-api/mes/bom/delete',
method: 'delete',
params: { id }
})
}
export function getBomDetailListByBomId(bomId) {
return request({
url: '/admin-api/mes/bom/bom-detail/list-by-bom-id',
method: 'get',
params: { bomId }
})
}

@ -50,6 +50,13 @@ export function delData(dictCode) {
method: 'delete'
})
}
export function getSimpleDictList() {
return request({
url: '/admin-api/system/dict-data/simple-list',
method: 'get'
})
}
//
export const processTypes = [
{text: '制浆', value: 'zhijiang'}, {text: '成型', value: 'chengxing'},

@ -0,0 +1,16 @@
import request from '@/utils/request'
export function getUserNavMenuList() {
return request({
url: '/admin-api/system/user-nav-menu/list',
method: 'get'
})
}
export function updateUserNavMenuList(data) {
return request({
url: '/admin-api/system/user-nav-menu/update-list',
method: 'put',
data
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

@ -0,0 +1,97 @@
<template>
<view class="app-empty-state">
<view class="empty-icon-wrap">
<view class="empty-icon-bg empty-icon-bg-left"></view>
<view class="empty-icon-bg empty-icon-bg-right"></view>
<view class="empty-icon-core">
<uni-icons :type="icon" size="38" color="#2d5a87" />
</view>
</view>
<text class="empty-title">{{ title }}</text>
<text v-if="desc" class="empty-desc">{{ desc }}</text>
</view>
</template>
<script setup>
defineProps({
icon: {
type: String,
default: 'locked-filled'
},
title: {
type: String,
required: true
},
desc: {
type: String,
default: ''
}
})
</script>
<style lang="scss" scoped>
.app-empty-state {
background: #ffffff;
border-radius: 28rpx;
padding: 72rpx 40rpx;
text-align: center;
box-shadow: 0 12rpx 36rpx rgba(26, 58, 92, 0.08);
}
.empty-icon-wrap {
position: relative;
width: 176rpx;
height: 176rpx;
margin: 0 auto 28rpx;
}
.empty-icon-bg {
position: absolute;
top: 50%;
width: 78rpx;
height: 78rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, rgba(45, 90, 135, 0.08) 0%, rgba(45, 90, 135, 0.18) 100%);
}
.empty-icon-bg-left {
left: 10rpx;
transform: translateY(-50%) rotate(-18deg);
}
.empty-icon-bg-right {
right: 10rpx;
transform: translateY(-50%) rotate(18deg);
}
.empty-icon-core {
position: absolute;
top: 50%;
left: 50%;
width: 132rpx;
height: 132rpx;
border-radius: 36rpx;
background: linear-gradient(180deg, #f7fbff 0%, #eaf3fb 100%);
border: 2rpx solid rgba(45, 90, 135, 0.1);
display: flex;
align-items: center;
justify-content: center;
transform: translate(-50%, -50%);
box-shadow: inset 0 2rpx 0 rgba(255, 255, 255, 0.8);
}
.empty-title {
display: block;
font-size: 36rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 16rpx;
}
.empty-desc {
display: block;
font-size: 28rpx;
color: #7a8795;
line-height: 1.5;
}
</style>

@ -0,0 +1,92 @@
<template>
<view class="bottom-action-bar">
<view class="action-row">
<view
v-for="(btn, index) in normalizedButtons"
:key="index"
class="action-btn"
:class="btn.type === 'primary' ? 'btn-primary' : 'btn-default'"
@click="handleClick(btn, index)"
>
<text class="btn-text" :class="btn.type === 'primary' ? 'text-primary' : 'text-default'">{{ btn.text }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
buttons: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['action'])
const normalizedButtons = computed(() => {
if (!Array.isArray(props.buttons) || props.buttons.length === 0) {
return [{ text: '返回', type: 'default', key: 'back' }]
}
return props.buttons.map((b) => ({
text: b && b.text ? b.text : '按钮',
type: b && b.type === 'primary' ? 'primary' : 'default',
key: b && b.key ? b.key : ''
}))
})
function handleClick(btn, index) {
emit('action', { ...btn, index })
}
</script>
<style lang="scss" scoped>
.bottom-action-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #ffffff;
box-shadow: 0 -6rpx 24rpx rgba(0, 0, 0, 0.08);
padding: 20rpx 24rpx calc(20rpx + env(safe-area-inset-bottom));
z-index: 90;
}
.action-row {
display: flex;
gap: 20rpx;
}
.action-btn {
flex: 1;
height: 88rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
.btn-default {
background: #ffffff;
border: 2rpx solid #dcdfe6;
}
.btn-primary {
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
}
.btn-text {
font-size: 30rpx;
font-weight: 600;
}
.text-default {
color: #303133;
}
.text-primary {
color: #ffffff;
}
</style>

@ -0,0 +1,109 @@
<template>
<view class="copy-btn" :class="sizeClass" @click.stop="handleCopy">
<text :class="iconClass">📋</text>
</view>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
content: {
type: [String, Number],
required: true,
default: ''
},
size: {
type: String,
default: 'small',
validator: (value) => ['small', 'medium', 'large'].includes(value)
},
successMessage: {
type: String,
default: '复制成功'
},
failMessage: {
type: String,
default: '复制失败'
},
emptyMessage: {
type: String,
default: '没有可复制的内容'
}
})
const emit = defineEmits(['copy', 'error'])
const sizeClass = computed(() => {
return `copy-btn-${props.size}`
})
const iconClass = computed(() => {
return `copy-icon-${props.size}`
})
function handleCopy() {
const content = String(props.content || '').trim()
if (!content) {
uni.showToast({ title: props.emptyMessage, icon: 'none' })
emit('error', { type: 'empty', content: props.content })
return
}
uni.setClipboardData({
data: content,
success: () => {
uni.showToast({ title: props.successMessage, icon: 'success' })
emit('copy', { content })
},
fail: (err) => {
uni.showToast({ title: props.failMessage, icon: 'none' })
emit('error', { type: 'fail', error: err, content: props.content })
}
})
}
</script>
<style lang="scss" scoped>
.copy-btn {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border-radius: 8rpx;
background: rgba(26, 58, 92, 0.08);
transition: background-color 0.2s;
&:active {
background: rgba(26, 58, 92, 0.15);
}
}
.copy-btn-small {
width: 48rpx;
height: 48rpx;
}
.copy-btn-medium {
width: 56rpx;
height: 56rpx;
}
.copy-btn-large {
width: 64rpx;
height: 64rpx;
}
.copy-icon-small {
font-size: 24rpx;
}
.copy-icon-medium {
font-size: 28rpx;
}
.copy-icon-large {
font-size: 32rpx;
}
</style>

@ -0,0 +1,119 @@
<template>
<view>
<up-navbar
:title="translatedTitle"
:bgColor="navbarBgColor"
:titleStyle="titleStyleObj"
:leftIcon="showBackBtn ? 'arrow-left' : ''"
:leftIconColor="navTextColor"
:leftIconSize="20"
:autoBack="false"
:placeholder="true"
:safeAreaInsetTop="true"
@leftClick="handleBack"
>
<template #right>
<slot name="right"></slot>
</template>
</up-navbar>
<view v-if="subTitle" class="navbar-subtitle-wrap">
<text class="navbar-subtitle">{{ translatedSubTitle }}</text>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import { translateLiteral } from '@/locales'
const { t } = useI18n()
const props = defineProps({
title: {
type: String,
default: ''
},
subTitle: {
type: String,
default: ''
},
backgroundColor: {
type: String,
default: '#22486e'
},
textColor: {
type: String,
default: '#ffffff'
},
showBack: {
type: Boolean,
default: undefined
}
})
const currentPagesLength = ref(1)
const isLoginPage = computed(() => {
const pages = getCurrentPages()
if (pages && pages.length > 0) {
return pages[pages.length - 1].route === 'pages/login'
}
return false
})
const showBackBtn = computed(() => {
if (props.showBack !== undefined) return props.showBack
return currentPagesLength.value > 1
})
const navbarBgColor = computed(() => {
return isLoginPage.value ? '#ffffff' : props.backgroundColor
})
const navTextColor = computed(() => {
return isLoginPage.value ? '#000000' : props.textColor
})
const translatedTitle = computed(() => translateLiteral(props.title))
const translatedSubTitle = computed(() => translateLiteral(props.subTitle))
const titleStyleObj = computed(() => {
return {
color: navTextColor.value,
fontWeight: '700',
fontSize: '34rpx'
}
})
onShow(() => {
const pages = getCurrentPages()
currentPagesLength.value = pages ? pages.length : 1
})
function handleBack() {
if (!showBackBtn.value) return
uni.navigateBack({
fail: () => {
uni.reLaunch({
url: '/pages/index'
})
}
})
}
</script>
<style lang="scss" scoped>
.navbar-subtitle-wrap {
padding: 14rpx 24rpx 20rpx;
background: #ffffff;
}
.navbar-subtitle {
font-size: 34rpx;
font-weight: 700;
color: #1a3a5c;
}
</style>

@ -0,0 +1,405 @@
<template>
<view class="page-container">
<view class="header-section">
<text class="header-title">{{ title }}</text>
<text class="header-subtitle">{{ subtitle }}</text>
</view>
<view v-if="searchable" class="search-section">
<view class="search-wrapper">
<view class="search-icon">
<text class="iconfont icon-search"></text>
</view>
<input
v-model="menuSearchKeyword"
class="search-input"
type="text"
placeholder="搜索菜单"
placeholder-class="input-placeholder"
/>
<view v-if="menuSearchKeyword" class="clear-btn" @click="clearMenuSearch">
<text class="clear-icon">×</text>
</view>
</view>
</view>
<scroll-view scroll-y class="content-scroll" :scroll-top="scrollTop" scroll-with-animation @scroll="onScroll">
<view class="content-inner" :class="{ 'safe-bottom': safeBottom }">
<AppEmptyState
v-if="filteredModules.length === 0"
:icon="emptyStateIcon"
:title="emptyStateTitle"
:desc="emptyStateDesc"
/>
<view v-for="(module, moduleIndex) in filteredModules" :key="module.id || `${pagePath}-${moduleIndex}`" class="module-section">
<view class="module-header">
<view class="module-icon" :style="{ background: getModuleColor(moduleIndex) }">
<text class="icon-text">{{ getMenuSymbol(module.name, moduleIndex) }}</text>
</view>
<text class="module-title">{{ module.name }}</text>
</view>
<view class="function-grid">
<view
v-for="(entry, entryIndex) in module.children"
:key="entry.id || `${moduleIndex}-${entryIndex}`"
class="function-item"
@click="handleClick(entry)"
>
<view class="function-icon" :style="{ background: `${hexToRgba(getModuleColor(moduleIndex), 0.1)}` }">
<uni-icons
v-if="isUniIcon(entry.icon)"
:type="getUniIconName(entry.icon)"
size="24"
:color="getModuleColor(moduleIndex)"
/>
<u-icon
v-else-if="isUviewIcon(entry.icon)"
:name="getUviewIconName(entry.icon)"
size="24"
:color="getModuleColor(moduleIndex)"
></u-icon>
<text v-else class="icon-inner">{{ getMenuSymbol(entry.name, entryIndex) }}</text>
</view>
<text class="function-name">{{ entry.name }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
<view v-if="showGoTop && showGoTopButton" class="go-top-btn" @click="goTop">
<text class="go-top-icon"></text>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import AppEmptyState from '@/components/common/AppEmptyState.vue'
import useUserStore from '@/store/modules/user'
import { buildPageModules, findTabMenuByPage, getMenuSymbol, getModuleColor, resolveMenuUrl } from '@/utils/permissionMenu'
const props = defineProps({
pagePath: {
type: String,
required: true
},
title: {
type: String,
required: true
},
subtitle: {
type: String,
default: ''
},
searchable: {
type: Boolean,
default: false
},
showGoTop: {
type: Boolean,
default: false
},
safeBottom: {
type: Boolean,
default: false
}
})
const userStore = useUserStore()
const menuSearchKeyword = ref('')
const scrollTop = ref(0)
const currentScrollTop = ref(0)
const showGoTopButton = ref(false)
const modules = computed(() => {
const tabMenu = findTabMenuByPage(userStore.menus, props.pagePath)
return buildPageModules(tabMenu)
})
const filteredModules = computed(() => {
const keyword = menuSearchKeyword.value.trim().toLowerCase()
if (!keyword) {
return modules.value
}
return modules.value
.map((module) => ({
...module,
children: (module.children || []).filter((entry) => {
const target = `${module.name}|${entry.name}`.toLowerCase()
return target.includes(keyword)
})
}))
.filter((module) => (module.children || []).length > 0 || String(module.name || '').toLowerCase().includes(keyword))
})
const hasMenuPermission = computed(() => modules.value.length > 0)
const isSearchEmpty = computed(() => {
return Boolean(menuSearchKeyword.value.trim()) && hasMenuPermission.value && filteredModules.value.length === 0
})
const emptyStateTitle = computed(() => {
return isSearchEmpty.value ? '未找到匹配菜单' : '请先配置菜单权限'
})
const emptyStateDesc = computed(() => {
return isSearchEmpty.value
? `未搜索到与"${menuSearchKeyword.value.trim()}"相关的菜单项,请调整关键词后重试`
: `当前账号还未配置${props.title}相关菜单权限,请联系管理员完成配置`
})
const emptyStateIcon = computed(() => {
return isSearchEmpty.value ? 'search' : 'locked-filled'
})
function clearMenuSearch() {
menuSearchKeyword.value = ''
}
function onScroll(event) {
const top = Number(event?.detail?.scrollTop || 0)
currentScrollTop.value = top
showGoTopButton.value = top > 600
}
function goTop() {
scrollTop.value = currentScrollTop.value + 1
setTimeout(() => {
scrollTop.value = 0
}, 0)
}
function hexToRgba(hex, alpha) {
const value = String(hex || '').replace('#', '')
if (value.length !== 6) {
return `rgba(45, 90, 135, ${alpha})`
}
const red = parseInt(value.slice(0, 2), 16)
const green = parseInt(value.slice(2, 4), 16)
const blue = parseInt(value.slice(4, 6), 16)
return `rgba(${red}, ${green}, ${blue}, ${alpha})`
}
function isUniIcon(icon) {
return String(icon || '').startsWith('uni-icons:')
}
function isUviewIcon(icon) {
return String(icon || '').startsWith('uview-plus:')
}
function getUniIconName(icon) {
return String(icon || '').replace(/^uni-icons:/, '').trim()
}
function getUviewIconName(icon) {
return String(icon || '').replace(/^uview-plus:/, '').trim()
}
function handleClick(menu) {
const url = resolveMenuUrl(menu)
if (url) {
uni.navigateTo({ url })
return
}
uni.showToast({
title: `暂未配置${menu?.name || '该菜单'}页面`,
icon: 'none'
})
}
</script>
<style lang="scss" scoped>
.page-container {
display: flex;
flex-direction: column;
height: 100%;
background-color: #f0f2f5;
}
.header-section {
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
padding: 60rpx 40rpx;
padding-top: 80rpx;
.header-title {
display: block;
font-size: 44rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 12rpx;
}
.header-subtitle {
display: block;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
}
.search-section {
padding: 24rpx;
background-color: #ffffff;
}
.search-wrapper {
display: flex;
align-items: center;
background: #f3f5f8;
border-radius: 999rpx;
padding: 0 28rpx;
height: 88rpx;
}
.search-icon {
color: #a0a7b4;
font-size: 30rpx;
margin-right: 16rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #1f2d3d;
}
.input-placeholder {
color: #a0a7b4;
}
.clear-btn {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: #d9dee8;
display: flex;
align-items: center;
justify-content: center;
}
.clear-icon {
color: #ffffff;
font-size: 28rpx;
line-height: 1;
}
.content-scroll {
flex: 1;
}
.content-inner {
padding: 24rpx;
&.safe-bottom {
padding-bottom: calc(24rpx + 120rpx + env(safe-area-inset-bottom));
}
}
.module-section {
background: #ffffff;
border-radius: 28rpx;
margin-bottom: 24rpx;
overflow: hidden;
box-shadow: 0 10rpx 30rpx rgba(26, 58, 92, 0.06);
}
.module-header {
display: flex;
align-items: center;
padding: 32rpx;
border-bottom: 1rpx solid #eef1f5;
}
.module-icon {
width: 68rpx;
height: 68rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 18rpx;
}
.icon-text {
color: #ffffff;
font-size: 30rpx;
font-weight: 600;
}
.module-title {
font-size: 36rpx;
font-weight: 700;
color: #1a3a5c;
}
.function-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14rpx;
padding: 24rpx;
}
.function-item {
background: #f7f9fc;
border-radius: 18rpx;
padding: 22rpx 10rpx 18rpx;
display: flex;
flex-direction: column;
align-items: center;
min-height: 148rpx;
&:active {
transform: scale(0.98);
}
}
.function-icon {
width: 72rpx;
height: 72rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 14rpx;
}
.icon-inner {
font-size: 28rpx;
color: #1a3a5c;
font-weight: 700;
}
.function-name {
font-size: 24rpx;
color: #233242;
text-align: center;
line-height: 1.35;
word-break: break-all;
}
.go-top-btn {
position: fixed;
bottom: 140rpx;
right: 30rpx;
width: 88rpx;
height: 88rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(26, 58, 92, 0.3);
z-index: 50;
}
.go-top-icon {
font-size: 44rpx;
color: #ffffff;
font-weight: bold;
}
</style>

@ -0,0 +1,161 @@
<template>
<view class="tabbar-shell" :class="{ 'is-hidden': !tabbarVisible }">
<up-tabbar :value="activeIndex" :activeColor="activeColor" :inactiveColor="inactiveColor" :safeAreaInsetBottom="true"
:fixed="true" :placeholder="true" :border="false" @change="handleChange" zIndex="1000">
<up-tabbar-item v-for="(item, index) in tabList" :key="index" :text="item.text" :name="index">
<template #active-icon>
<image :src="item.selectedIcon" class="tabbar-icon" mode="widthFix" />
</template>
<template #inactive-icon>
<image :src="item.icon" class="tabbar-icon" mode="widthFix" />
</template>
</up-tabbar-item>
</up-tabbar>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import useUserStore from '@/store/modules/user'
import { storeToRefs } from 'pinia'
import { getDynamicTabMenus } from '@/utils/permissionMenu'
import { useI18n } from 'vue-i18n'
const TABBAR_VISIBILITY_EVENT = 'tabbar-visibility-change'
const { t } = useI18n()
const userStore = useUserStore()
const { menus } = storeToRefs(userStore)
const inactiveColor = '#666666'
const activeColor = '#1a3a5c'
const activeIndex = ref(0)
const tabbarVisible = ref(true)
const homeIcon = '/static/images/tabbar/home.png'
const homeSelectedIcon = '/static/images/tabbar/home_.png'
const reportIcon = '/static/images/tabbar/report.png'
const reportSelectedIcon = '/static/images/tabbar/report_.png'
const workIcon = '/static/images/tabbar/work.png'
const workSelectedIcon = '/static/images/tabbar/work_.png'
const mineIcon = '/static/images/tabbar/mine.png'
const mineSelectedIcon = '/static/images/tabbar/mine_.png'
const routeMap = {
'pages/index': 0,
'pages/report': 1,
'pages/work': 2,
'pages/mine': 3
}
function getCurrentActiveIndex() {
const pages = getCurrentPages()
if (pages && pages.length > 0) {
const route = pages[pages.length - 1].route
return routeMap[route] !== undefined ? routeMap[route] : 0
}
return 0
}
const tabList = computed(() => {
const dynamicMenus = getDynamicTabMenus(menus.value)
return [
{
text: t('nav.home'),
icon: homeIcon,
selectedIcon: homeSelectedIcon,
path: '/pages/index'
},
{
text: dynamicMenus[0]?.name || t('tab.report'),
icon: reportIcon,
selectedIcon: reportSelectedIcon,
path: '/pages/report'
},
{
text: dynamicMenus[1]?.name || dynamicMenus[0]?.name || t('tab.work'),
icon: workIcon,
selectedIcon: workSelectedIcon,
path: '/pages/work'
},
{
text: t('nav.mine'),
icon: mineIcon,
selectedIcon: mineSelectedIcon,
path: '/pages/mine'
}
]
})
onMounted(() => {
activeIndex.value = getCurrentActiveIndex()
uni.$on(TABBAR_VISIBILITY_EVENT, handleTabbarVisibility)
})
onUnmounted(() => {
uni.$off(TABBAR_VISIBILITY_EVENT, handleTabbarVisibility)
})
function handleTabbarVisibility(visible = true) {
tabbarVisible.value = visible !== false
}
function handleChange(index) {
if (activeIndex.value === index) return
activeIndex.value = index
const item = tabList.value[index]
if (item) {
uni.reLaunch({
url: item.path
})
}
}
watch(() => useUserStore().menus, () => {
activeIndex.value = getCurrentActiveIndex()
})
</script>
<style lang="scss" scoped>
.tabbar-shell {
position: relative;
:deep(.u-tabbar) {
position: fixed !important;
left: 0;
right: 0;
bottom: 0 !important;
width: 100%;
z-index: 1000 !important;
transform: translateY(0);
opacity: 1;
will-change: transform, opacity;
transition: transform 0.36s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.24s ease;
}
:deep(.u-tabbar__content) {
z-index: 1000 !important;
}
&.is-hidden {
:deep(.u-tabbar) {
transform: translateY(calc(100% + 24rpx + env(safe-area-inset-bottom)));
opacity: 0;
pointer-events: none;
}
}
}
.tabbar-icon {
width: 48rpx;
height: 48rpx;
}
</style>
<style>
.u-tabbar {
flex: none !important;
}
</style>

@ -0,0 +1,324 @@
<template>
<view class="banner-section">
<view class="banner-bg">
<view class="banner-content">
<text class="banner-title">{{ t('dashboard.welcome') }}</text>
<text class="banner-subtitle">{{ t('dashboard.subtitle') }}</text>
</view>
<view class="banner-decoration">
<view class="deco-line"></view>
<view class="deco-dot"></view>
</view>
</view>
<view class="bell-wrapper" @click="openTodo">
<view class="bell-icon">
<view class="bell-badge" v-show="badgeVisible">
<text class="bell-badge-text">{{ badgeText }}</text>
</view>
<image src="/static/logo/bell.png" mode="aspectFit" style="width: 48rpx; height: 48rpx;" />
</view>
</view>
<uni-popup ref="todoPopup" type="right" background-color="#fff">
<view class="todo-popup">
<view class="todo-header">
<view class="todo-back" @click="closeTodo">
<text class="back-icon"></text>
<text class="back-text">{{ t('dashboard.back') }}</text>
</view>
<text class="todo-title-text">{{ t('dashboard.todoTitle') }}</text>
</view>
<scroll-view scroll-y class="todo-scroll">
<view v-if="todoList.length === 0" class="todo-empty">
<text class="empty-text">{{ t('dashboard.noTodo') }}</text>
</view>
<view v-else>
<view v-for="(item, index) in todoList" :key="index" class="todo-item">
<view class="todo-dot"></view>
<view class="todo-content">
<view class="todo-item-title">{{ item.name }}</view>
<view class="todo-sub">{{ t('dashboard.taskCode', { value: item.code }) }}</view>
<view class="todo-sub">{{ t('dashboard.taskType', { value: item.type }) }}</view>
<view class="todo-sub">{{ t('dashboard.taskTarget', { value: item.deviceName }) }}</view>
<view class="todo-sub">{{ t('dashboard.createTime', { value: formatDate(item.createTime) }) }}</view>
</view>
</view>
</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import request from '@/utils/request'
const { t } = useI18n()
const todoPopup = ref(null)
const todoCount = ref(0)
const todoList = reactive([])
const badgeVisible = computed(() => Number(todoCount.value) > 0)
const badgeText = computed(() => {
const count = Number(todoCount.value)
if (!Number.isFinite(count) || count <= 0) return ''
if (count > 99) return '99+'
return String(count)
})
function formatDate(ms) {
if (!ms) return '-'
const date = new Date(ms)
if (Number.isNaN(date.getTime())) return '-'
const pad2 = (n) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`
}
function openTodo() {
todoPopup.value?.open()
}
function closeTodo() {
todoPopup.value?.close()
}
async function loadTodoList() {
const res = await request({ url: '/admin-api/mes/dashboard/getTodoList', method: 'get' })
const data = res?.data || []
todoList.splice(0, todoList.length, ...data)
todoCount.value = data.length
}
onMounted(() => {
loadTodoList()
})
defineExpose({ loadTodoList })
</script>
<style lang="scss" scoped>
.banner-section {
position: relative;
height: 320rpx;
}
.banner-bg {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 50%, #3d7ab5 100%);
position: relative;
overflow: visible;
&::before {
content: '';
position: absolute;
top: -50%;
right: -20%;
width: 300rpx;
height: 300rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
}
&::after {
content: '';
position: absolute;
bottom: -30%;
left: 10%;
width: 200rpx;
height: 200rpx;
background: rgba(255, 255, 255, 0.05);
border-radius: 50%;
}
}
.banner-content {
position: relative;
z-index: 2;
padding: 60rpx 40rpx;
.banner-title {
display: block;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 16rpx;
}
.banner-subtitle {
display: block;
font-size: 40rpx;
font-weight: bold;
color: #ffffff;
line-height: 1.4;
}
}
.banner-decoration {
position: absolute;
bottom: 40rpx;
left: 40rpx;
display: flex;
align-items: center;
.deco-line {
width: 60rpx;
height: 4rpx;
background: #ff8c00;
border-radius: 2rpx;
}
.deco-dot {
width: 12rpx;
height: 12rpx;
background: #ff8c00;
border-radius: 50%;
margin-left: 16rpx;
}
}
.bell-wrapper {
position: absolute;
top: 30rpx;
right: 30rpx;
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.bell-icon {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.bell-badge {
position: absolute;
top: -10rpx;
right: -12rpx;
z-index: 2;
min-width: 34rpx;
height: 34rpx;
padding: 0 8rpx;
border-radius: 18rpx;
background: #ff4d4f;
border: 2rpx solid #ffffff;
display: flex;
align-items: center;
justify-content: center;
}
.bell-badge-text {
color: #ffffff;
font-size: 20rpx;
line-height: 1;
font-weight: 600;
}
.todo-popup {
width: 600rpx;
height: 100vh;
background: #ffffff;
}
.todo-header {
display: flex;
align-items: center;
padding: 40rpx 30rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
}
.todo-back {
display: flex;
align-items: center;
&:active {
opacity: 0.7;
}
.back-icon {
font-size: 48rpx;
color: #ffffff;
margin-right: 8rpx;
}
.back-text {
font-size: 28rpx;
color: #ffffff;
}
}
.todo-title-text {
flex: 1;
text-align: center;
font-size: 34rpx;
font-weight: 600;
color: #ffffff;
margin-right: 80rpx;
}
.todo-scroll {
height: calc(100vh - 140rpx);
background: #f5f7fa;
}
.todo-empty {
display: flex;
align-items: center;
justify-content: center;
height: 400rpx;
.empty-text {
font-size: 28rpx;
color: #999999;
}
}
.todo-item {
display: flex;
align-items: center;
background: #ffffff;
padding: 28rpx 30rpx;
margin-bottom: 2rpx;
&:active {
background: #f5f7fa;
}
}
.todo-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: #1a3a5c;
margin-right: 20rpx;
}
.todo-content {
flex: 1;
.todo-item-title {
display: block;
font-size: 28rpx;
color: #333333;
font-weight: bold;
margin-bottom: 12rpx;
}
.todo-sub {
display: block;
font-size: 24rpx;
color: #666666;
margin-bottom: 6rpx;
line-height: 1.4;
}
}
</style>

@ -0,0 +1,557 @@
<template>
<view class="device-section">
<view class="section-title">{{ t('deviceOverview.title') }}</view>
<!-- 设备状态统计 -->
<view class="trend-stats">
<view class="trend-stat-card">
<text class="trend-stat-value">{{ deviceData.totalDevices }}</text>
<text class="trend-stat-label">{{ t('deviceOverview.totalDevices') }}</text>
</view>
<view class="trend-stat-card">
<text class="trend-stat-value running">{{ deviceData.runningCount }}</text>
<text class="trend-stat-label">{{ t('deviceOverview.runningCount') }}</text>
</view>
<view class="trend-stat-card">
<text class="trend-stat-value standby">{{ deviceData.standbyCount }}</text>
<text class="trend-stat-label">{{ t('deviceOverview.standbyCount') }}</text>
</view>
<view class="trend-stat-card">
<text class="trend-stat-value fault">{{ deviceData.faultCount }}</text>
<text class="trend-stat-label">{{ t('deviceOverview.faultCount') }}</text>
</view>
</view>
<!-- 设备比率统计 -->
<view class="trend-stats">
<view class="trend-stat-card">
<text class="trend-stat-value offline">{{ deviceData.offlineCount }}</text>
<text class="trend-stat-label">{{ t('deviceOverview.offlineCount') }}</text>
</view>
<view class="trend-stat-card">
<text class="trend-stat-value">{{ deviceData.utilizationRate }}</text>
<text class="trend-stat-label">{{ t('deviceOverview.utilizationRate') }}</text>
</view>
<view class="trend-stat-card">
<text class="trend-stat-value running">{{ deviceData.bootRate }}</text>
<text class="trend-stat-label">{{ t('deviceOverview.bootRate') }}</text>
</view>
<view class="trend-stat-card">
<text class="trend-stat-value fault">{{ deviceData.faultRate }}</text>
<text class="trend-stat-label">{{ t('deviceOverview.faultRate') }}</text>
</view>
</view>
<!-- 稼动率/开机率趋势图 -->
<view class="rate-trend-section">
<view class="rate-trend-header">
<text class="rate-trend-title">{{ t('deviceOverview.rateTrend') }}</text>
<picker mode="selector" :range="periodRange" range-key="text" :value="periodIndex" @change="onPeriodChange">
<view class="filter-select">
<text class="filter-text">{{ currentPeriodLabel }}</text>
<text class="filter-arrow"></text>
</view>
</picker>
</view>
<view class="switch-bar">
<view class="switch-item">
<text class="switch-label">{{ t('deviceOverview.onlyScheduled') }}</text>
<up-switch v-model="onlyScheduled" size="20" activeColor="#1a3a5c" @change="onSwitchChange" />
</view>
<view class="switch-item">
<text class="switch-label">{{ t('deviceOverview.skipHoliday') }}</text>
<up-switch v-model="skipHoliday" size="20" activeColor="#1a3a5c" @change="onSwitchChange" />
</view>
</view>
<view class="chart-box">
<qiun-data-charts type="line" :chartData="rateChartData" :canvas2d="false" :opts="rateChartOpts" />
</view>
</view>
<!-- 近7日平均稼动率排名 -->
<view class="rate-trend-section">
<view class="rate-trend-header">
<text class="rate-trend-title">{{ rankingTitle }}</text>
</view>
<scroll-view class="ranking-scroll" scroll-y :style="{ height: rankingScrollHeight }">
<view :style="{ height: rankingChartHeight }">
<qiun-data-charts type="bar" :chartData="rankingChartData" :canvas2d="false" :opts="rankingChartOpts" />
</view>
</scroll-view>
</view>
<!-- 单设备近7日稼动率/开机率趋势 -->
<view class="rate-trend-section">
<view class="rate-trend-header">
<text class="rate-trend-title">{{ deviceTrendTitle }}</text>
<picker mode="selector" :range="deviceRange" range-key="text" :value="deviceIndex" @change="onDeviceChange">
<view class="filter-select">
<text class="filter-text">{{ selectedDeviceName || t('deviceOverview.selectDevice') }}</text>
<text class="filter-arrow"></text>
</view>
</picker>
</view>
<view class="device-trend-chart-box" v-if="selectedDeviceId">
<qiun-data-charts type="line" :chartData="deviceTrendChartData" :canvas2d="false" :opts="deviceTrendChartOpts" />
</view>
<view class="empty-hint" v-else>
<text class="empty-hint-text">{{ t('deviceOverview.selectDeviceHint') }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import request from '@/utils/request'
const { t } = useI18n()
const deviceData = reactive({
totalDevices: 0,
runningCount: 0,
standbyCount: 0,
faultCount: 0,
offlineCount: 0,
utilizationRate: '-',
bootRate: '-',
faultRate: '-'
})
const currentPeriod = ref('LAST_7_DAYS')
const onlyScheduled = ref(true)
const skipHoliday = ref(false)
const periodRange = computed(() => [
{ text: t('deviceOverview.periodLastWeek'), value: 'LAST_WEEK' },
{ text: t('deviceOverview.periodThisWeek'), value: 'THIS_WEEK' },
{ text: t('deviceOverview.periodLast7Days'), value: 'LAST_7_DAYS' },
{ text: t('deviceOverview.periodLastMonth'), value: 'LAST_MONTH' },
{ text: t('deviceOverview.periodThisMonth'), value: 'THIS_MONTH' }
])
const periodIndex = computed(() => {
const idx = periodRange.value.findIndex(item => item.value === currentPeriod.value)
return idx >= 0 ? idx : 0
})
const periodLabelMap = computed(() => ({
LAST_WEEK: t('deviceOverview.periodLastWeek'),
THIS_WEEK: t('deviceOverview.periodThisWeek'),
LAST_7_DAYS: t('deviceOverview.periodLast7Days'),
LAST_MONTH: t('deviceOverview.periodLastMonth'),
THIS_MONTH: t('deviceOverview.periodThisMonth')
}))
const currentPeriodLabel = computed(() => {
return periodLabelMap.value[currentPeriod.value] || t('deviceOverview.periodLast7Days')
})
const rankingTitle = computed(() => {
const period = currentPeriodLabel.value
return t('deviceOverview.utilizationRanking').replace('近7日', period)
})
const deviceTrendTitle = computed(() => {
const period = currentPeriodLabel.value
return t('deviceOverview.deviceRateTrend').replace('近7日', period)
})
const rateChartOpts = {
color: ['#1a3a5c', '#18bc37'],
dataLabel: false,
dataPointShape: false,
legend: { show: true, position: 'bottom' },
xAxis: { disableGrid: true, labelCount: 5 },
yAxis: { gridType: 'dash', dashLength: 2, data: [{ min: 0, max: 100 }] },
extra: { line: { type: 'straight', width: 1, activeType: 'hollow' } }
}
const rateChartData = reactive({
categories: [],
series: [
{ name: '', data: [] },
{ name: '', data: [] }
]
})
const rankingChartOpts = {
color: ['#1a3a5c'],
dataLabel: true,
legend: { show: false },
xAxis: { disableGrid: true, max: 100, axisLabel: { padding: [0, 0, 0, 10] } },
yAxis: { disableGrid: true, axisLabel: { padding: [0, 10, 0, 0], formatter: function(value) {
if (value.length > 10) {
return value.substring(0, 10) + '...';
}
return value;
} } },
extra: {
bar: {
type: 'group',
width: 20,
seriesGap: 4,
categoryGap: 4,
barBorderRadius: [4, 4, 0, 0],
linearType: 'custom',
linearOpacity: 0.6,
activeBgColor: '#1a3a5c',
activeBgOpacity: 0.08
}
}
}
const rankingChartData = reactive({
categories: [],
series: [{ name: '', data: [] }]
})
const ITEM_HEIGHT_PX = 30
const MAX_VISIBLE = 6
const rankingScrollHeight = computed(() => {
const count = rankingChartData.categories.length || 1
const visible = Math.min(count, MAX_VISIBLE)
return `${visible * ITEM_HEIGHT_PX + 30}px`
})
const rankingChartHeight = computed(() => {
const count = rankingChartData.categories.length || 1
return `${count * ITEM_HEIGHT_PX + 30}px`
})
const deviceList = ref([])
const selectedDeviceId = ref(null)
const selectedDeviceName = ref('')
const isInitialLoad = ref(true)
const deviceRange = computed(() =>
deviceList.value.map((d) => ({ text: d.deviceName || d.name || '', value: d.id }))
)
const deviceIndex = computed(() => {
const idx = deviceRange.value.findIndex(item => item.value === selectedDeviceId.value)
return idx >= 0 ? idx : 0
})
const deviceTrendChartOpts = {
color: ['#1a3a5c', '#18bc37'],
dataLabel: false,
dataPointShape: false,
legend: { show: true, position: 'bottom' },
xAxis: { disableGrid: true, labelCount: 5 },
yAxis: { gridType: 'dash', dashLength: 2, data: [{ min: 0, max: 100 }] },
extra: { line: { type: 'straight', width: 1, activeType: 'hollow' } }
}
const deviceTrendChartData = reactive({
categories: [],
series: [
{ name: '', data: [] },
{ name: '', data: [] }
]
})
async function loadDeviceOverview() {
const res = await request({ url: '/admin-api/iot/device/getDeviceOverview', method: 'get', showLoading: !isInitialLoad.value })
const data = res?.data || {}
deviceData.totalDevices = data.totalDevices ?? 0
deviceData.runningCount = data.runningCount ?? 0
deviceData.standbyCount = data.standbyCount ?? 0
deviceData.faultCount = data.faultCount ?? 0
deviceData.offlineCount = data.offlineCount ?? 0
deviceData.utilizationRate = data.utilizationRate ?? '-'
deviceData.bootRate = data.bootRate ?? '-'
deviceData.faultRate = data.faultRate ?? '-'
}
async function loadRateTrend() {
const params = {
period: currentPeriod.value,
onlyScheduled: onlyScheduled.value,
skipHoliday: skipHoliday.value
}
const res = await request({ url: '/admin-api/iot/device/deviceRateTrend', method: 'get', params, showLoading: !isInitialLoad.value })
const list = res?.data || []
const categories = list.map((item) => (item.day || '').substring(5))
const utilizationData = list.map((item) => {
const v = parseFloat(item.utilizationRate)
return isNaN(v) ? 0 : Math.round(v * 100) / 100
})
const powerOnData = list.map((item) => {
const v = parseFloat(item.powerOnRate)
return isNaN(v) ? 0 : Math.round(v * 100) / 100
})
rateChartData.categories = categories
rateChartData.series = [
{ name: t('deviceOverview.utilizationRateTrend'), data: utilizationData },
{ name: t('deviceOverview.bootRate'), data: powerOnData }
]
}
async function loadUtilizationRanking() {
const params = { period: currentPeriod.value }
const res = await request({ url: '/admin-api/iot/device-operation-record/deviceOperationPageList', method: 'get', params, showLoading: !isInitialLoad.value })
const list = res?.data || []
const sorted = [...list].sort((a, b) => {
const va = parseFloat(a.utilizationRate) || 0
const vb = parseFloat(b.utilizationRate) || 0
return vb - va
})
const categories = sorted.map((item) => item.deviceName || '')
const data = sorted.map((item) => {
const v = parseFloat(item.utilizationRate)
return isNaN(v) ? 0 : Math.round(v * 100) / 100
})
const total = sorted.length
const colors = sorted.map((_, index) => {
const ratio = total > 1 ? index / (total - 1) : 0
const r = Math.round(26 + ratio * (74 - 26))
const g = Math.round(58 + ratio * (144 - 58))
const b = Math.round(92 + ratio * (194 - 92))
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
})
rankingChartData.categories = categories
rankingChartData.series = [{
name: t('deviceOverview.utilizationRateTrend'),
data,
linearColor: colors.map((c) => ['#e8f0f8', c])
}]
}
async function loadDeviceList() {
const res = await request({ url: '/admin-api/iot/device/deviceList', method: 'get', showLoading: !isInitialLoad.value })
deviceList.value = res?.data || []
if (deviceList.value.length > 0 && !selectedDeviceId.value) {
const first = deviceList.value[0]
selectedDeviceId.value = first.id
selectedDeviceName.value = first.deviceName || first.name || ''
loadDeviceRateTrend()
}
}
async function loadDeviceRateTrend() {
if (!selectedDeviceId.value) return
const params = { deviceId: selectedDeviceId.value, period: currentPeriod.value }
const res = await request({
url: '/admin-api/iot/device-operation-record/deviceRateTrendByDeviceId',
method: 'get',
params,
showLoading: !isInitialLoad.value
})
const list = res?.data || []
const categories = list.map((item) => (item.day || '').substring(5))
const utilizationData = list.map((item) => {
const v = parseFloat(item.utilizationRate)
return isNaN(v) ? 0 : Math.round(v * 100) / 100
})
const powerOnData = list.map((item) => {
const v = parseFloat(item.powerOnRate)
return isNaN(v) ? 0 : Math.round(v * 100) / 100
})
deviceTrendChartData.categories = categories
deviceTrendChartData.series = [
{ name: t('deviceOverview.utilizationRateTrend'), data: utilizationData },
{ name: t('deviceOverview.bootRate'), data: powerOnData }
]
}
function onDeviceChange(e) {
const idx = e.detail.value
const item = deviceRange.value[idx]
if (!item) return
selectedDeviceId.value = item.value
selectedDeviceName.value = item.text
loadDeviceRateTrend()
}
function onPeriodChange(e) {
const idx = e.detail.value
const val = periodRange.value[idx]?.value
if (!val) return
currentPeriod.value = val
loadRateTrend()
loadUtilizationRanking()
loadDeviceRateTrend()
}
function onSwitchChange() {
loadRateTrend()
}
onMounted(async () => {
await Promise.all([
loadDeviceOverview(),
loadRateTrend(),
loadUtilizationRanking(),
loadDeviceList()
])
isInitialLoad.value = false
})
defineExpose({ loadDeviceOverview, loadRateTrend, loadUtilizationRanking, loadDeviceRateTrend })
</script>
<style lang="scss" scoped>
.device-section {
margin-top: 20rpx;
background: #ffffff;
border-radius: 20rpx;
padding: 28rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 24rpx;
}
.trend-stats {
display: flex;
justify-content: space-between;
margin-bottom: 24rpx;
&:last-child {
margin-bottom: 0;
}
}
.trend-stat-card {
flex: 1;
background: #f8fafc;
border-radius: 12rpx;
padding: 20rpx 12rpx;
margin: 0 6rpx;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
.trend-stat-value {
display: block;
font-size: 36rpx;
font-weight: bold;
color: #1a3a5c;
margin-bottom: 6rpx;
&.running {
color: #18bc37;
}
&.standby {
color: #4a90c2;
}
&.fault {
color: #ff4d4f;
}
&.offline {
color: #999999;
}
}
.trend-stat-label {
display: block;
font-size: 22rpx;
color: #999999;
}
.rate-trend-section {
margin-top: 28rpx;
padding-top: 24rpx;
border-top: 2rpx solid #f0f2f5;
}
.rate-trend-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.rate-trend-title {
font-size: 28rpx;
font-weight: 500;
color: #1a3a5c;
}
.filter-select {
display: flex;
align-items: center;
padding: 10rpx 20rpx;
background: #f0f2f5;
border-radius: 12rpx;
&:active {
background: #e8ecf0;
}
}
.filter-text {
font-size: 24rpx;
color: #1a3a5c;
font-weight: 500;
margin-right: 8rpx;
}
.filter-arrow {
font-size: 18rpx;
color: #999999;
}
.switch-bar {
display: flex;
align-items: center;
margin-bottom: 20rpx;
gap: 32rpx;
}
.switch-item {
display: flex;
align-items: center;
gap: 8rpx;
}
.switch-label {
font-size: 24rpx;
color: #666666;
}
.chart-box {
width: 100%;
height: 450rpx;
min-width: 100%;
}
.ranking-scroll {
width: 100%;
min-width: 100%;
}
.device-trend-chart-box {
width: 100%;
min-width: 100%;
}
.empty-hint {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx 0;
}
.empty-hint-text {
font-size: 26rpx;
color: #999999;
}
</style>

@ -0,0 +1,164 @@
<template>
<view>
<view class="mode-switch-btn" @click="openPopup">
<uni-icons type="settings" size="28" color="#1a3a5c" />
</view>
<view v-if="showModePopup" class="popup-overlay" @click="showModePopup = false">
<view
class="popup-content"
:class="{ 'popup-enter': showModePopup }"
:style="{ top: popupTop + 'px', right: popupRight + 'px' }"
@click.stop
>
<view
class="popup-item"
:class="{ active: currentMode === 'production' }"
@click="switchMode('production')"
>
<text class="popup-icon">📋</text>
<text class="popup-text">{{ t('dashboard.production') }}</text>
</view>
<view
class="popup-item"
:class="{ active: currentMode === 'quality' }"
@click="switchMode('quality')"
>
<text class="popup-icon">🛡</text>
<text class="popup-text">{{ t('dashboard.quality') }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { getCurrentInstance } from 'vue'
const { t } = useI18n()
const instance = getCurrentInstance()
const props = defineProps({
currentMode: {
type: String,
default: 'production'
}
})
const emit = defineEmits(['modeChange'])
const showModePopup = ref(false)
const popupTop = ref(0)
const popupRight = ref(0)
function openPopup() {
const query = uni.createSelectorQuery().in(instance.proxy)
query.select('.mode-switch-btn').boundingClientRect((rect) => {
if (rect) {
const { windowWidth } = uni.getSystemInfoSync()
popupTop.value = rect.bottom + 8
popupRight.value = windowWidth - rect.right
}
showModePopup.value = true
}).exec()
}
function switchMode(mode) {
showModePopup.value = false
emit('modeChange', mode)
}
</script>
<style lang="scss" scoped>
.mode-switch-btn {
width: 64rpx;
height: 64rpx;
// background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
// box-shadow: 0 4rpx 12rpx rgba(26, 58, 92, 0.3);
&:active {
transform: scale(0.95);
}
}
.popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
animation: overlayFadeIn 0.25s ease-out;
}
.popup-content {
position: fixed;
width: 200rpx;
background: linear-gradient(180deg, #1a3a5c 0%, #2d5a87 100%);
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 8rpx 32rpx rgba(26, 58, 92, 0.4);
transform-origin: top right;
&.popup-enter {
animation: popupSlideIn 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
}
@keyframes overlayFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes popupSlideIn {
from {
opacity: 0;
transform: scale(0.6) translateY(-20rpx);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.popup-item {
display: flex;
align-items: center;
padding: 24rpx 20rpx;
justify-content: center;
gap: 12rpx;
&:active {
background: rgba(255, 255, 255, 0.1);
}
&.active {
background: rgba(255, 255, 255, 0.2);
}
&:not(:last-child) {
border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
}
}
.popup-icon {
font-size: 36rpx;
}
.popup-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
</style>

@ -0,0 +1,376 @@
<template>
<view v-if="renderVisible" class="nav-menu-editor-mask" :class="{ 'mask-hidden': !animVisible }" @click="handleClose">
<view class="nav-menu-editor" :class="{ 'panel-hidden': !animVisible }" @click.stop>
<view class="editor-header">
<text class="editor-title">{{ t('dashboard.editNavMenu') }}</text>
<view class="editor-close" @click="handleClose">
<uni-icons type="close" size="24" color="#999"></uni-icons>
</view>
</view>
<view class="editor-content">
<view class="nav-section">
<text class="nav-section-title">{{ t('dashboard.configuredNav') }}</text>
<view class="nav-grid">
<view v-for="item in configuredMenuList" :key="item.id" class="nav-item is-selected">
<view class="nav-icon" :style="{ background: '#e1e6eb' }">
<uni-icons v-if="isUniIcon(item.icon)" :type="getUniIconName(item.icon)" size="28"
color="#1a3a5c" />
<u-icon v-else-if="isUviewIcon(item.icon)" :name="getUviewIconName(item.icon)" size="28"
color="#1a3a5c"></u-icon>
<text v-else class="nav-icon-text">{{ item.symbol }}</text>
</view>
<text class="nav-text">{{ item.displayName }}</text>
<view class="check-badge" @click.stop="handleConfiguredClick(item)">
<u-icon name="minus-circle-fill" size="18" color="#de3730"></u-icon>
</view>
</view>
</view>
</view>
<view class="divider-line"></view>
<view class="nav-section">
<text class="nav-section-title">{{ t('dashboard.unconfiguredNav') }}</text>
<view class="nav-grid">
<view v-for="item in unconfiguredMenuList" :key="item.id" class="nav-item"
@click="handleUnconfiguredClick(item)">
<view class="nav-icon nav-icon-disabled" :style="{ background: '#e1e6eb' }">
<uni-icons v-if="isUniIcon(item.icon)" :type="getUniIconName(item.icon)" size="28"
color="#1a3a5c" />
<u-icon v-else-if="isUviewIcon(item.icon)" :name="getUviewIconName(item.icon)" size="28"
color="#1a3a5c"></u-icon>
<text v-else class="nav-icon-text">{{ item.symbol }}</text>
</view>
<text class="nav-text nav-text-disabled">{{ item.displayName }}</text>
</view>
</view>
</view>
<text class="click-hint">{{ t('dashboard.clickHint') }}</text>
</view>
<view class="editor-footer">
<view class="btn-reset" @click="handleReset">
<text>{{ t('common.reset') }}</text>
</view>
<view class="btn-confirm" @click="handleConfirm">
<text>{{ t('common.complete') }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import useUserStore from '@/store/modules/user'
import { getUserNavMenuList, updateUserNavMenuList } from '@/api/system/userNavMenu'
import { getNavPermissionInfo } from '@/api/login'
import { buildNavMenuViewModels } from '@/utils/permissionMenu'
const { t } = useI18n()
const userStore = useUserStore()
function isUniIcon(icon) {
return String(icon || '').startsWith('uni-icons:')
}
function isUviewIcon(icon) {
return String(icon || '').startsWith('uview-plus:')
}
function getUniIconName(icon) {
return String(icon || '').replace(/^uni-icons:/, '').trim()
}
function getUviewIconName(icon) {
return String(icon || '').replace(/^uview-plus:/, '').trim()
}
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['close', 'update'])
const renderVisible = ref(false)
const animVisible = ref(false)
watch(() => props.visible, (val) => {
if (val) {
renderVisible.value = true
nextTick(() => {
setTimeout(() => { animVisible.value = true }, 30)
})
} else {
animVisible.value = false
setTimeout(() => { renderVisible.value = false }, 300)
}
}, { immediate: true })
const userMenuList = ref([])
const configuredRecords = ref([])
const originConfIds = ref([])
const configuredIds = ref([])
const menuMap = computed(() => {
const map = {}
userMenuList.value.forEach(item => {
map[item.id] = item
})
return map
})
const configuredMenuList = computed(() => {
return configuredIds.value
.map(id => menuMap.value[id])
.filter(Boolean)
})
const unconfiguredMenuList = computed(() => {
return userMenuList.value
.filter(item => !configuredIds.value.includes(item.id))
})
async function loadData() {
try {
const menuRes = await getNavPermissionInfo()
userMenuList.value = buildNavMenuViewModels(menuRes?.data?.menus)
const navRes = await getUserNavMenuList()
configuredRecords.value = Array.isArray(navRes?.data) ? [...navRes.data] : []
configuredRecords.value.sort((left, right) => Number(left?.sort || 0) - Number(right?.sort || 0))
configuredIds.value = configuredRecords.value
.map(item => item.menuId)
.filter(menuId => !!menuMap.value[menuId])
originConfIds.value = [...configuredIds.value]
} catch (error) {
console.error('加载菜单数据失败:', error)
}
}
function handleConfiguredClick(item) {
configuredIds.value = configuredIds.value.filter(id => id !== item.id)
}
function handleUnconfiguredClick(item) {
if (!configuredIds.value.includes(item.id)) {
configuredIds.value.push(item.id)
}
}
function handleReset() {
configuredIds.value = [...originConfIds.value]
}
async function handleConfirm() {
try {
const userId = userStore.userId
const data = configuredIds.value.map((menuId, index) => ({
menuId,
userId,
sort: index,
status: 1
}))
await updateUserNavMenuList(data)
uni.showToast({ title: t('common.updateSuccess'), icon: 'success' })
emit('update')
handleClose()
} catch (error) {
console.error('保存失败:', error)
uni.showToast({ title: t('common.saveFailed'), icon: 'none' })
}
}
function handleClose() {
emit('close')
}
watch(() => props.visible, async (val) => {
if (val) {
await loadData()
}
})
</script>
<style lang="scss" scoped>
.nav-menu-editor-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1100;
display: flex;
align-items: flex-end;
box-sizing: border-box;
transition: opacity 0.3s ease;
&.mask-hidden {
opacity: 0;
}
}
.nav-menu-editor {
width: 100%;
max-height: 75vh;
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
&.panel-hidden {
transform: translateY(100%);
}
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.editor-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.editor-close {
padding: 16rpx;
}
.editor-content {
flex: 1;
overflow-y: auto;
padding: 24rpx;
}
.divider-line {
height: 1rpx;
background: #f0f0f0;
margin: 24rpx 0;
}
.nav-section {
margin-bottom: 8rpx;
}
.nav-section-title {
font-size: 28rpx;
color: #999;
margin-bottom: 20rpx;
display: block;
}
.nav-grid {
display: flex;
flex-wrap: wrap;
min-height: 120rpx;
border-radius: 24rpx;
}
.nav-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 24rpx;
padding: 16rpx;
border-radius: 16rpx;
position: relative;
transition: all 0.15s;
&:active {
opacity: 0.7;
}
}
.nav-icon {
width: 96rpx;
height: 96rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
.nav-icon-text {
font-size: 32rpx;
color: #1a3a5c;
font-weight: 700;
}
}
.nav-text {
font-size: 24rpx;
color: #333;
text-align: center;
&.nav-text-disabled {
color: #999;
}
}
.check-badge {
position: absolute;
top: 8rpx;
right: 26rpx;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.click-hint {
font-size: 24rpx;
color: #999;
text-align: center;
display: block;
margin-top: 16rpx;
padding-bottom: 24rpx;
}
.editor-footer {
display: flex;
gap: 24rpx;
padding: 24rpx 32rpx;
border-top: 1rpx solid #f0f0f0;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
}
.btn-reset,
.btn-confirm {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
}
.btn-reset {
background: #f5f5f5;
color: #666;
}
.btn-confirm {
background: #22486e;
color: #ffffff;
}
</style>

@ -0,0 +1,249 @@
<template>
<view v-if="renderVisible" class="nav-menu-more-mask" :class="{ 'mask-hidden': !animVisible }" @click="handleClose">
<view class="nav-menu-more" :class="{ 'panel-hidden': !animVisible }" @click.stop>
<view class="more-header">
<text class="more-title">{{ t('dashboard.allNavMenu') }}</text>
<view class="more-actions">
<view class="action-btn edit-btn" @click="handleEdit">
<uni-icons type="compose" size="22" color="#22486e"></uni-icons>
</view>
</view>
</view>
<scroll-view scroll-y class="more-content">
<view v-if="displayMenuList.length === 0" class="empty-state">
<text class="empty-text">{{ t('dashboard.clickHint') }}</text>
</view>
<view v-else class="nav-grid">
<view
v-for="item in displayMenuList"
:key="item.id"
class="nav-item"
@click="handleNavClick(item)"
>
<view class="nav-icon" :style="{ background: '#e1e6eb' }">
<uni-icons v-if="isUniIcon(item.icon)" :type="getUniIconName(item.icon)" size="28" color="#1a3a5c" />
<u-icon v-else-if="isUviewIcon(item.icon)" :name="getUviewIconName(item.icon)" size="28" color="#1a3a5c"></u-icon>
<text v-else class="nav-icon-text">{{ item.symbol }}</text>
</view>
<text class="nav-text">{{ item.displayName }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { computed, ref, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
function hexToRgba(hex, alpha) {
const value = String(hex || '').replace('#', '')
if (value.length !== 6) {
return `rgba(45, 90, 135, ${alpha})`
}
const red = parseInt(value.slice(0, 2), 16)
const green = parseInt(value.slice(2, 4), 16)
const blue = parseInt(value.slice(4, 6), 16)
return `rgba(${red}, ${green}, ${blue}, ${alpha})`
}
function isUniIcon(icon) {
return String(icon || '').startsWith('uni-icons:')
}
function isUviewIcon(icon) {
return String(icon || '').startsWith('uview-plus:')
}
function getUniIconName(icon) {
return String(icon || '').replace(/^uni-icons:/, '').trim()
}
function getUviewIconName(icon) {
return String(icon || '').replace(/^uview-plus:/, '').trim()
}
const props = defineProps({
visible: {
type: Boolean,
default: false
},
menuList: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['close', 'edit'])
const renderVisible = ref(false)
const animVisible = ref(false)
watch(() => props.visible, (val) => {
if (val) {
renderVisible.value = true
nextTick(() => {
setTimeout(() => { animVisible.value = true }, 30)
})
} else {
animVisible.value = false
setTimeout(() => { renderVisible.value = false }, 300)
}
}, { immediate: true })
const displayMenuList = computed(() => Array.isArray(props.menuList) ? props.menuList : [])
function handleNavClick(item) {
if (item.route) {
uni.navigateTo({ url: item.route })
} else {
uni.showToast({ title: t('common.moduleBuilding'), icon: 'none' })
}
}
function handleEdit() {
emit('edit')
}
function handleClose() {
emit('close')
}
</script>
<style lang="scss" scoped>
.nav-menu-more-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1100;
display: flex;
align-items: flex-end;
box-sizing: border-box;
transition: opacity 0.3s ease;
&.mask-hidden {
opacity: 0;
}
}
.nav-menu-more {
width: 100%;
max-height: 66.6vh;
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
&.panel-hidden {
transform: translateY(100%);
}
}
.more-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.more-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.more-actions {
display: flex;
align-items: center;
gap: 24rpx;
}
.edit-btn {
justify-content: center;
}
.action-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 12rpx 20rpx;
&.edit-btn {
background: rgba(34, 72, 110, 0.1);
border-radius: 20rpx;
font-size: 26rpx;
color: #22486e;
}
&.close-btn {
padding: 12rpx;
}
}
.more-content {
flex: 1;
padding: 24rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));
}
.empty-state {
min-height: 220rpx;
display: flex;
align-items: center;
justify-content: center;
}
.empty-text {
font-size: 26rpx;
color: #999;
text-align: center;
}
.nav-grid {
display: flex;
flex-wrap: wrap;
}
.nav-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 32rpx;
padding: 16rpx;
&:active {
opacity: 0.7;
}
}
.nav-icon {
width: 100rpx;
height: 100rpx;
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
.nav-icon-text {
font-size: 32rpx;
color: #1a3a5c;
font-weight: 700;
}
}
.nav-text {
font-size: 26rpx;
color: #333;
text-align: center;
}
</style>

@ -0,0 +1,265 @@
<template>
<view class="nav-section">
<view class="section-header">
<text class="section-title">{{ t('dashboard.functionNav') }}</text>
<view class="more-btn" @click="handleMoreClick">
<text class="more-text">{{ t('common.more') }}</text>
</view>
</view>
<view class="nav-grid">
<view v-for="(item, index) in displayNavList" :key="item.id || index" class="nav-item" @click="handleNavClick(item)">
<view class="nav-icon" :style="{ background: '#e1e6eb' }">
<uni-icons v-if="isUniIcon(item.icon)" :type="getUniIconName(item.icon)" size="28" :color="item.accentColor" />
<u-icon v-else-if="isUviewIcon(item.icon)" :name="getUviewIconName(item.icon)" size="28" :color="item.accentColor"></u-icon>
<text v-else class="nav-icon-text">{{ item.symbol }}</text>
</view>
<text class="nav-text">{{ item.displayName }}</text>
</view>
</view>
<NavMenuMore
:visible="showMoreModal"
:menu-list="moreNavList"
@close="showMoreModal = false"
@edit="handleEdit"
/>
<NavMenuEditor
:visible="showEditModal"
@close="showEditModal = false"
@update="handleUpdate"
/>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { getUserNavMenuList } from '@/api/system/userNavMenu'
import { getNavPermissionInfo } from '@/api/login'
import { buildNavMenuViewModels } from '@/utils/permissionMenu'
import NavMenuMore from './NavMenuMore.vue'
import NavMenuEditor from './NavMenuEditor.vue'
const { t } = useI18n()
const TABBAR_VISIBILITY_EVENT = 'tabbar-visibility-change'
function hexToRgba(hex, alpha) {
const value = String(hex || '').replace('#', '')
if (value.length !== 6) {
return `rgba(45, 90, 135, ${alpha})`
}
const red = parseInt(value.slice(0, 2), 16)
const green = parseInt(value.slice(2, 4), 16)
const blue = parseInt(value.slice(4, 6), 16)
return `rgba(${red}, ${green}, ${blue}, ${alpha})`
}
function isUniIcon(icon) {
return String(icon || '').startsWith('uni-icons:')
}
function isUviewIcon(icon) {
return String(icon || '').startsWith('uview-plus:')
}
function getUniIconName(icon) {
return String(icon || '').replace(/^uni-icons:/, '').trim()
}
function getUviewIconName(icon) {
return String(icon || '').replace(/^uview-plus:/, '').trim()
}
const showMoreModal = ref(false)
const showEditModal = ref(false)
const configuredMenuList = ref([])
const allMenuList = ref([])
const loaded = ref(false)
const defaultNavList = [
{ id: 'default-mold', displayName: '模具', symbol: '🔧', accentColor: '#1a3a5c', route: '/pages_function/pages/mold/index' },
{ id: 'default-equipment', displayName: '设备', symbol: '⚙️', accentColor: '#2d5a87', route: '/pages_function/pages/equipment/index' },
{ id: 'default-keypart', displayName: '配件', symbol: '🔩', accentColor: '#3d7ab5', route: '/pages_function/pages/keypart/index' },
{ id: 'default-spare', displayName: '备件', symbol: '📦', accentColor: '#4a90c2', route: '/pages_function/pages/spare/index' },
{ id: 'default-product', displayName: '产品', symbol: '🧾', accentColor: '#5aa0d2', route: '/pages_function/pages/product/index' }
]
const displayNavList = computed(() => {
if (!loaded.value) {
return []
}
if (configuredMenuList.value.length === 0) {
return defaultNavList
}
return configuredMenuList.value.slice(0, 5)
})
const moreNavList = computed(() => {
if (!loaded.value) {
return []
}
if (configuredMenuList.value.length === 0) {
return defaultNavList
}
return configuredMenuList.value
})
const modalVisible = computed(() => showMoreModal.value || showEditModal.value)
let tabbarRestoreTimer = null
async function loadConfiguredMenus() {
try {
const menuRes = await getNavPermissionInfo()
allMenuList.value = buildNavMenuViewModels(menuRes?.data?.menus)
const navRes = await getUserNavMenuList()
const configuredRecords = Array.isArray(navRes?.data) ? [...navRes.data] : []
configuredRecords.sort((left, right) => Number(left?.sort || 0) - Number(right?.sort || 0))
const configuredIds = configuredRecords.map(item => item.menuId)
if (configuredIds.length === 0) {
configuredMenuList.value = []
loaded.value = true
return
}
const configuredMenus = allMenuList.value
.filter(item => configuredIds.includes(item.id))
.sort((a, b) => {
const indexA = configuredIds.indexOf(a.id)
const indexB = configuredIds.indexOf(b.id)
return indexA - indexB
})
configuredMenuList.value = configuredMenus
loaded.value = true
} catch (error) {
console.error('加载配置菜单失败:', error)
configuredMenuList.value = []
loaded.value = true
}
}
function handleNavClick(item) {
if (item.route) {
uni.navigateTo({ url: item.route })
} else {
uni.showToast({ title: t('common.moduleBuilding'), icon: 'none' })
}
}
function handleMoreClick() {
showMoreModal.value = true
}
function handleEdit() {
showMoreModal.value = false
showEditModal.value = true
}
function handleUpdate() {
loadConfiguredMenus()
}
onMounted(() => {
loadConfiguredMenus()
})
onUnmounted(() => {
if (tabbarRestoreTimer) {
clearTimeout(tabbarRestoreTimer)
tabbarRestoreTimer = null
}
uni.$emit(TABBAR_VISIBILITY_EVENT, true)
})
watch(modalVisible, (visible) => {
if (tabbarRestoreTimer) {
clearTimeout(tabbarRestoreTimer)
tabbarRestoreTimer = null
}
if (visible) {
uni.$emit(TABBAR_VISIBILITY_EVENT, false)
return
}
tabbarRestoreTimer = setTimeout(() => {
uni.$emit(TABBAR_VISIBILITY_EVENT, true)
tabbarRestoreTimer = null
}, 300)
}, { immediate: true })
</script>
<style lang="scss" scoped>
.nav-section {
background: #ffffff;
border-radius: 20rpx;
padding: 28rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
}
.more-btn {
display: flex;
align-items: center;
gap: 6rpx;
padding: 8rpx 16rpx;
border-radius: 16rpx;
background: rgba(34, 72, 110, 0.08);
}
.more-text {
font-size: 26rpx;
color: #999;
}
.nav-grid {
display: flex;
justify-content: space-around;
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
&:active {
opacity: 0.7;
}
}
.nav-icon {
width: 96rpx;
height: 96rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
.nav-icon-text {
font-size: 32rpx;
color: #1a3a5c;
font-weight: 700;
}
}
.nav-text {
font-size: 26rpx;
color: #333333;
}
</style>

@ -0,0 +1,684 @@
<template>
<view class="plan-section">
<view class="section-header">
<text class="section-title">{{ t('dashboard.productionPlan') }}</text>
<text class="section-more" @click="viewMore">
{{ t('dashboard.viewMore') }}
</text>
</view>
<view class="filter-bar">
<picker mode="selector" :range="filterRange" range-key="text" :value="filterIndex" @change="onFilterChange">
<view class="filter-select">
<text class="filter-text">{{ currentFilterLabel }}</text>
<text class="filter-arrow"></text>
</view>
</picker>
<picker mode="selector" :range="rangeRange" range-key="text" :value="rangeIndex" @change="onRangeChange">
<view class="filter-select">
<text class="filter-text">{{ currentRangeLabel }}</text>
<text class="filter-arrow"></text>
</view>
</picker>
</view>
<view v-if="currentRange === 'custom'" class="filter-date-wrap">
<view class="date-picker-item">
<uni-datetime-picker v-model="dateRange.start" type="datetime"
:placeholder="t('dashboard.startDate')" @change="onStartDateChange" />
</view>
<view class="date-picker-item">
<uni-datetime-picker v-model="dateRange.end" type="datetime"
:placeholder="t('dashboard.endDate')" @change="onEndDateChange" />
</view>
</view>
<view v-if="currentFilter === 'product'" class="trend-content">
<view class="trend-stats">
<view class="trend-stat-card">
<text class="trend-stat-value">{{ formatNumber(trendData.baogongNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.baogongNum') }}</text>
</view>
<view class="trend-stat-card">
<text class="trend-stat-value pass">{{ formatNumber(trendData.passNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.passNum') }}</text>
</view>
<view class="trend-stat-card">
<text class="trend-stat-value fail">{{ formatNumber(trendData.noPassNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.noPassNum') }}</text>
</view>
<view class="trend-stat-card">
<text class="trend-stat-value rate">{{ formatPercent(trendData.passRate) }}</text>
<text class="trend-stat-label">{{ t('dashboard.passRate') }}</text>
</view>
</view>
<view v-if="currentRange !== 'custom'" class="trend-chart">
<text class="chart-title">{{ t('dashboard.baogongNum') }}</text>
<view class="chart-box">
<qiun-data-charts type="line" :chartData="baogongChartData" :canvas2d="false"
:opts="chartOpts" />
</view>
</view>
<view v-if="currentRange !== 'custom'" class="trend-chart">
<text class="chart-title">{{ t('dashboard.passRate') }}</text>
<view class="chart-box">
<qiun-data-charts type="line" :chartData="passRateChartData" :canvas2d="false"
:opts="passRateChartOpts" />
</view>
</view>
</view>
<view v-else class="trend-content">
<scroll-view scroll-x enable-flex class="trend-stats-scroll">
<view class="trend-stats">
<view class="trend-stat-card" @click="viewTaskList('')">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.totalNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.totalTask') }}</text>
</view>
<view class="trend-stat-card" @click="viewTaskList('2')">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.issuedNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.issuedNum') }}</text>
</view>
<view class="trend-stat-card" @click="viewTaskList('7')">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.partialScheduledNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.partialScheduledNum') }}</text>
</view>
<view class="trend-stat-card" @click="viewTaskList('8')">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.waitingProductionNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.waitingProduction') }}</text>
</view>
<view class="trend-stat-card" @click="viewTaskList('9')">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.producingNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.producing') }}</text>
</view>
<view class="trend-stat-card" @click="viewTaskList('10')">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.completedNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.completed') }}</text>
</view>
</view>
</scroll-view>
<view v-if="currentRange !== 'custom'" class="trend-chart">
<text class="chart-title">{{ t('dashboard.totalTask') }}</text>
<view class="chart-box">
<qiun-data-charts type="line" :chartData="taskChartData" :canvas2d="false"
:opts="chartOpts" />
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import request from '@/utils/request'
import { formatNumber, formatPercent } from '@/utils/format'
const { t } = useI18n()
const chartOpts = {
color: ['#1a3a5c'],
dataLabel: false,
dataPointShape: false,
legend: { show: false },
xAxis: { disableGrid: true, labelCount: 4 },
yAxis: { gridType: 'dash', dashLength: 2 },
extra: { line: { type: 'curve', width: 1, activeType: 'hollow' } }
}
const passRateChartOpts = {
color: ['#1a3a5c'],
dataLabel: false,
dataPointShape: false,
legend: { show: false },
xAxis: { disableGrid: true, labelCount: 4 },
yAxis: { gridType: 'dash', dashLength: 2, data: [{ min: 0, max: 100 }] },
extra: { line: { type: 'curve', width: 1, activeType: 'hollow' } }
}
const currentFilter = ref('task')
const currentRange = ref('year')
const isInitialLoad = ref(true)
const filterRange = computed(() => [
{ text: t('dashboard.filterTask'), value: 'task' },
{ text: t('dashboard.filterProduct'), value: 'product' }
])
const filterIndex = computed(() => {
const idx = filterRange.value.findIndex(item => item.value === currentFilter.value)
return idx >= 0 ? idx : 0
})
const currentFilterLabel = computed(() => {
return currentFilter.value === 'task' ? t('dashboard.filterTask') : t('dashboard.filterProduct')
})
const rangeRange = computed(() => [
{ text: t('dashboard.rangeYear'), value: 'year' },
{ text: t('dashboard.rangeMonth'), value: 'month' },
{ text: t('dashboard.rangeWeek'), value: 'week' },
{ text: t('dashboard.rangeToday'), value: 'today' },
{ text: t('dashboard.rangeCustom'), value: 'custom' }
])
const rangeIndex = computed(() => {
const idx = rangeRange.value.findIndex(item => item.value === currentRange.value)
return idx >= 0 ? idx : 0
})
const rangeLabelMap = computed(() => ({
year: t('dashboard.rangeYear'),
month: t('dashboard.rangeMonth'),
week: t('dashboard.rangeWeek'),
today: t('dashboard.rangeToday'),
custom: t('dashboard.rangeCustom')
}))
const currentRangeLabel = computed(() => {
return rangeLabelMap.value[currentRange.value] || t('dashboard.rangeMonth')
})
function getTodayZero() {
const now = new Date()
const pad2 = (n) => String(n).padStart(2, '0')
return `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())} 00:00:00`
}
const dateRange = reactive({ start: getTodayZero(), end: getTodayZero() })
const trendData = reactive({
baogongNum: 0,
passNum: 0,
noPassNum: 0,
passRate: 0
})
const taskTrendData = reactive({
totalNum: 0,
issuedNum: 0,
partialScheduledNum: 0,
waitingProductionNum: 0,
producingNum: 0,
completedNum: 0
})
const baogongChartData = reactive({
categories: [],
series: [{ name: t('dashboard.baogongNum'), data: [] }]
})
const passRateChartData = reactive({
categories: [],
series: [{ name: t('dashboard.passRate'), data: [] }]
})
const taskChartData = reactive({
categories: [],
series: [{ name: t('dashboard.taskTrend'), data: [] }]
})
const weekdayKeys = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']
function getDateRange(type) {
const now = new Date()
const pad2 = (n) => String(n).padStart(2, '0')
const fmt = (d) => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
const end = fmt(now)
if (type === 'year') {
const start = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate())
return { start: fmt(start), end }
}
if (type === 'month') {
const start = new Date(now.getFullYear(), now.getMonth(), 1)
return { start: fmt(start), end }
}
if (type === 'week') {
const day = now.getDay() || 7
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - day + 1)
return { start: fmt(start), end }
}
if (type === 'today') {
return { start: end, end }
}
return { start: '', end: '' }
}
function transformChartData(dayTrend, rangeType) {
if (!dayTrend || dayTrend.length === 0) {
return { categories: [], baogongData: [], passRateData: [] }
}
if (rangeType === 'year') {
const monthMap = new Map()
dayTrend.forEach((item) => {
const m = item.day ? item.day.substring(0, 7) : ''
if (!m) return
if (!monthMap.has(m)) {
monthMap.set(m, { baogongNum: 0, passRateSum: 0, count: 0 })
}
const entry = monthMap.get(m)
entry.baogongNum += item.baogongNum ?? 0
entry.passRateSum += item.passRate ?? 0
entry.count += 1
})
const categories = []
const baogongData = []
const passRateData = []
monthMap.forEach((val, key) => {
categories.push(key)
baogongData.push(val.baogongNum)
passRateData.push(val.count > 0 ? Math.round((val.passRateSum / val.count) * 100) / 100 : 0)
})
return { categories, baogongData, passRateData }
}
if (rangeType === 'week') {
const categories = dayTrend.map((item) => {
const d = new Date(item.day)
const dayIdx = d.getDay()
return t(`dashboard.${weekdayKeys[dayIdx]}`)
})
return {
categories,
baogongData: dayTrend.map((item) => item.baogongNum ?? 0),
passRateData: dayTrend.map((item) => item.passRate ?? 0)
}
}
if (rangeType === 'today') {
const categories = dayTrend.map((item) => (item.day || '').substring(5))
return {
categories,
baogongData: dayTrend.map((item) => item.baogongNum ?? 0),
passRateData: dayTrend.map((item) => item.passRate ?? 0)
}
}
const categories = dayTrend.map((item) => (item.day || '').substring(5))
return {
categories,
baogongData: dayTrend.map((item) => item.baogongNum ?? 0),
passRateData: dayTrend.map((item) => item.passRate ?? 0)
}
}
function onFilterChange(e) {
const idx = e.detail.value
const val = filterRange.value[idx]?.value
if (!val) return
currentFilter.value = val
if (val === 'product') {
loadTrendData()
} else if (val === 'task') {
loadTaskTrendData()
}
}
function onRangeChange(e) {
const idx = e.detail.value
const val = rangeRange.value[idx]?.value
if (!val) return
currentRange.value = val
if (val !== 'custom') {
const range = getDateRange(val)
dateRange.start = range.start
dateRange.end = range.end
if (currentFilter.value === 'product') {
loadTrendData()
} else if (currentFilter.value === 'task') {
loadTaskTrendData()
}
} else {
dateRange.start = getTodayZero()
dateRange.end = getTodayZero()
clearChartData()
}
}
function clearChartData() {
trendData.baogongNum = 0
trendData.passNum = 0
trendData.noPassNum = 0
trendData.passRate = 0
baogongChartData.categories = []
baogongChartData.series = [{ name: t('dashboard.baogongNum'), data: [] }]
passRateChartData.categories = []
passRateChartData.series = [{ name: t('dashboard.passRate'), data: [] }]
taskTrendData.totalNum = 0
taskTrendData.issuedNum = 0
taskTrendData.partialScheduledNum = 0
taskTrendData.waitingProductionNum = 0
taskTrendData.producingNum = 0
taskTrendData.completedNum = 0
taskChartData.categories = []
taskChartData.series = [{ name: t('dashboard.taskTrend'), data: [] }]
}
function onStartDateChange(val) {
dateRange.start = val
if (dateRange.start && dateRange.end) {
loadData()
}
}
function onEndDateChange(val) {
dateRange.end = val
if (dateRange.start && dateRange.end) {
loadData()
}
}
async function loadTrendData() {
const trendTypeMap = { year: 1, month: 2, week: 3, today: 4, custom: 5 }
const params = { trendType: trendTypeMap[currentRange.value] || 2 }
if (currentRange.value === 'custom') {
if (dateRange.start) params.beginBaogongTime = dateRange.start
if (dateRange.end) params.endBaogongTime = dateRange.end
}
const res = await request({
url: '/admin-api/mes/baogong-record/trend',
method: 'get',
params,
showLoading: !isInitialLoad.value
})
const data = res?.data || {}
trendData.baogongNum = data.baogongNum ?? 0
trendData.passNum = data.passNum ?? 0
trendData.noPassNum = data.noPassNum ?? 0
trendData.passRate = data.passRate ?? 0
if (currentRange.value === 'custom') {
baogongChartData.categories = []
baogongChartData.series = [{ name: t('dashboard.baogongNum'), data: [] }]
passRateChartData.categories = []
passRateChartData.series = [{ name: t('dashboard.passRate'), data: [] }]
return
}
const dayTrend = data.dayTrend || []
const transformed = transformChartData(dayTrend, currentRange.value)
baogongChartData.categories = transformed.categories
baogongChartData.series = [{ name: t('dashboard.baogongNum'), data: transformed.baogongData }]
passRateChartData.categories = transformed.categories
passRateChartData.series = [{ name: t('dashboard.passRate'), data: transformed.passRateData }]
}
async function loadTaskTrendData() {
const trendTypeMap = { year: 1, month: 2, week: 3, today: 4, custom: 5 }
const params = { trendType: trendTypeMap[currentRange.value] || 2 }
if (currentRange.value === 'custom') {
if (dateRange.start) params.beginTime = dateRange.start
if (dateRange.end) params.endTime = dateRange.end
}
const res = await request({
url: '/admin-api/mes/task/trend',
method: 'get',
params,
showLoading: !isInitialLoad.value
})
const data = res?.data || {}
taskTrendData.totalNum = data.totalNum ?? 0
taskTrendData.issuedNum = data.issuedNum ?? 0
taskTrendData.partialScheduledNum = data.partialScheduledNum ?? 0
taskTrendData.waitingProductionNum = data.waitingProductionNum ?? 0
taskTrendData.producingNum = data.producingNum ?? 0
taskTrendData.completedNum = data.completedNum ?? 0
if (currentRange.value === 'custom') {
taskChartData.categories = []
taskChartData.series = [{ name: t('dashboard.taskTrend'), data: [] }]
return
}
const dayTrend = data.dayTrend || []
const categories = []
const taskData = []
if (currentRange.value === 'year') {
const monthMap = new Map()
dayTrend.forEach((item) => {
const m = item.day ? item.day.substring(0, 7) : ''
if (!m) return
if (!monthMap.has(m)) {
monthMap.set(m, 0)
}
monthMap.set(m, (monthMap.get(m) ?? 0) + (item.count ?? 0))
})
monthMap.forEach((val, key) => {
categories.push(key)
taskData.push(val)
})
} else if (currentRange.value === 'week') {
dayTrend.forEach((item) => {
const d = new Date(item.day)
const dayIdx = d.getDay()
categories.push(t(`dashboard.${weekdayKeys[dayIdx]}`))
taskData.push(item.count ?? 0)
})
} else {
dayTrend.forEach((item) => {
const dayStr = item.day || ''
categories.push(dayStr.substring(5))
taskData.push(item.count ?? 0)
})
}
taskChartData.categories = categories
taskChartData.series = [{ name: t('dashboard.taskTrend'), data: taskData }]
}
function viewMore() {
if (currentFilter.value === 'task') {
uni.navigateTo({ url: '/pages_function/pages/taskList/index' })
} else {
uni.navigateTo({ url: '/pages_function/pages/planList/index' })
}
}
function viewTaskList(status) {
const url = status
? `/pages_function/pages/taskList/index?status=${status}`
: '/pages_function/pages/taskList/index'
uni.navigateTo({ url })
}
function loadData() {
if (currentFilter.value === 'task') {
loadTaskTrendData()
} else {
loadTrendData()
}
}
onMounted(async () => {
await loadData()
isInitialLoad.value = false
})
defineExpose({ loadData })
</script>
<style lang="scss" scoped>
.plan-section {
background: #ffffff;
border-radius: 20rpx;
padding: 28rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
margin-top: 24rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.section-more {
font-size: 26rpx;
color: #666666;
margin-bottom: 0.75rem;
}
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 24rpx;
}
.filter-bar {
display: flex;
align-items: center;
margin-bottom: 24rpx;
}
.filter-select {
display: flex;
align-items: center;
padding: 12rpx 24rpx;
background: #f0f2f5;
border-radius: 12rpx;
margin-right: 20rpx;
&:active {
background: #e8ecf0;
}
}
.filter-text {
font-size: 26rpx;
color: #1a3a5c;
font-weight: 500;
margin-right: 8rpx;
}
.filter-arrow {
font-size: 20rpx;
color: #999999;
}
.filter-date-wrap {
display: flex;
gap: 16rpx;
margin-bottom: 24rpx;
flex-wrap: wrap;
}
.date-picker-item {
flex: 1;
min-width: 200rpx;
display: flex;
align-items: center;
position: relative;
background: #f8fafc;
border-radius: 12rpx;
padding: 6rpx 12rpx;
border: 2rpx solid #e0e6ed;
transition: border-color 0.3s ease;
&:focus-within {
border-color: #4a90c2;
background: #ffffff;
}
:deep(.uni-date) {
flex: 1;
width: 100%;
.uni-date-editor--x {
border: none;
background: transparent;
padding: 0;
}
.uni-date__x-input {
background: transparent;
font-size: 24rpx;
}
}
}
.trend-content {
margin-top: 8rpx;
}
.trend-stats-scroll {
width: 100%;
white-space: nowrap;
margin-bottom: 24rpx;
}
.trend-stats {
display: inline-flex;
flex-wrap: nowrap;
}
.trend-stat-card {
flex-shrink: 0;
width: 152rpx;
background: #f8fafc;
border-radius: 12rpx;
padding: 20rpx 12rpx;
margin: 0 6rpx;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
&:active {
background: #e8ecf0;
}
}
.trend-stat-value {
display: block;
font-size: 36rpx;
font-weight: bold;
color: #1a3a5c;
margin-bottom: 6rpx;
&.pass {
color: #18bc37;
}
&.fail {
color: #ff4d4f;
}
&.rate {
color: #4a90c2;
}
}
.trend-stat-label {
display: block;
font-size: 22rpx;
color: #999999;
}
.trend-chart {
margin-top: 8rpx;
}
.chart-title {
display: block;
font-size: 28rpx;
font-weight: 500;
color: #1a3a5c;
margin-bottom: 16rpx;
}
.chart-box {
width: 100%;
height: 450rpx;
min-width: 100%;
border-radius: 12rpx;
}
</style>

@ -0,0 +1,374 @@
<template>
<view class="quality-section">
<view class="section-header">
<view>
<text class="section-title">{{ t('dashboard.qualityOverview') }}</text>
<picker mode="selector" :range="periodRange" range-key="text" :value="periodIndex" @change="onPeriodChange">
<view class="filter-select">
<text class="filter-text">{{ currentPeriodLabel }}</text>
<text class="filter-arrow"></text>
</view>
</picker>
</view>
<ModeSwitchPopup :currentMode="currentMode" @modeChange="switchMode" />
</view>
<view class="quality-stats">
<view class="quality-stat-card">
<text class="stat-value">{{ formatNumber(qualityData.totalWangongNumber) }}</text>
<text class="stat-label">{{ t('dashboard.totalWangongNumber') }}</text>
</view>
<view class="quality-stat-card">
<text class="stat-value pass">{{ formatNumber(qualityData.totalPassNumber) }}</text>
<text class="stat-label">{{ t('dashboard.totalPassNumber') }}</text>
</view>
<view class="quality-stat-card">
<text class="stat-value fail">{{ formatNumber(qualityData.totalNoPassNumber) }}</text>
<text class="stat-label">{{ t('dashboard.totalNoPassNumber') }}</text>
</view>
<view class="quality-stat-card">
<text class="stat-value rate">{{ formatPercent(qualityData.totalPassRate) }}</text>
<text class="stat-label">{{ t('dashboard.totalPassRate') }}</text>
</view>
</view>
<view class="chart-section">
<view class="chart-header">
<text class="chart-title">{{ t('dashboard.productPassRateRanking') }}</text>
</view>
<scroll-view class="ranking-scroll" scroll-y :style="{ height: rankingScrollHeight }">
<view :style="{ height: rankingChartHeight }">
<qiun-data-charts type="bar" :chartData="rankingChartData" :canvas2d="true" :opts="rankingChartOpts" />
</view>
</scroll-view>
</view>
<view class="chart-section">
<view class="chart-header">
<text class="chart-title">{{ t('dashboard.qualityTrend') }}</text>
</view>
<view class="chart-box">
<qiun-data-charts type="line" :chartData="trendChartData" :canvas2d="false" :opts="trendChartOpts" />
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import request from '@/utils/request'
import { formatNumber, formatPercent } from '@/utils/format'
import ModeSwitchPopup from '@/components/dashboard/ModeSwitchPopup.vue'
const { t } = useI18n()
const emit = defineEmits(['modeChange'])
const currentMode = ref('quality')
const currentPeriod = ref('LAST_7_DAYS')
const isInitialLoad = ref(true)
const periodRange = computed(() => [
{ text: t('dashboard.periodLast7Days'), value: 'LAST_7_DAYS' },
{ text: t('dashboard.periodLastWeek'), value: 'LAST_WEEK' },
{ text: t('dashboard.periodThisWeek'), value: 'THIS_WEEK' },
{ text: t('dashboard.periodLastMonth'), value: 'LAST_MONTH' },
{ text: t('dashboard.periodThisMonth'), value: 'THIS_MONTH' },
{ text: t('dashboard.periodLastYear'), value: 'LAST_YEAR' }
])
const periodIndex = computed(() => {
const idx = periodRange.value.findIndex(item => item.value === currentPeriod.value)
return idx >= 0 ? idx : 0
})
const periodLabelMap = computed(() => ({
LAST_7_DAYS: t('dashboard.periodLast7Days'),
LAST_WEEK: t('dashboard.periodLastWeek'),
THIS_WEEK: t('dashboard.periodThisWeek'),
LAST_MONTH: t('dashboard.periodLastMonth'),
THIS_MONTH: t('dashboard.periodThisMonth'),
LAST_YEAR: t('dashboard.periodLastYear')
}))
const currentPeriodLabel = computed(() => {
return periodLabelMap.value[currentPeriod.value] || t('dashboard.periodLast7Days')
})
const qualityData = reactive({
totalWangongNumber: 0,
totalPassNumber: 0,
totalNoPassNumber: 0,
totalPassRate: 0
})
const rankingChartOpts = {
color: ['#1a3a5c'],
dataLabel: true,
legend: { show: false },
padding: [0, 40, 0, 10],
xAxis: { disableGrid: true, max: 100, axisLabel: { padding: [0, 0, 0, 10] } },
yAxis: {
disableGrid: true
},
extra: {
bar: {
type: 'group',
width: 20,
seriesGap: 4,
categoryGap: 4,
barBorderRadius: [4, 4, 0, 0],
linearType: 'custom',
linearOpacity: 0.6,
activeBgColor: '#1a3a5c',
activeBgOpacity: 0.08
}
}
}
const rankingChartData = reactive({
categories: [],
series: [{ name: '', data: [] }]
})
const ITEM_HEIGHT_PX = 44
const MAX_VISIBLE = 6
const rankingScrollHeight = computed(() => {
const count = rankingChartData.categories.length || 1
const visible = Math.min(count, MAX_VISIBLE)
return `${visible * ITEM_HEIGHT_PX + 30}px`
})
const rankingChartHeight = computed(() => {
const count = rankingChartData.categories.length || 1
return `${count * ITEM_HEIGHT_PX + 30}px`
})
const trendChartOpts = {
color: ['#18bc37', '#ff4d4f'],
dataLabel: false,
dataPointShape: false,
legend: { show: true, position: 'bottom' },
xAxis: { disableGrid: true, labelCount: 5 },
yAxis: { gridType: 'dash', dashLength: 2 },
extra: { line: { type: 'straight', width: 1, activeType: 'hollow' } }
}
const trendChartData = reactive({
categories: [],
series: [
{ name: '', data: [] },
{ name: '', data: [] }
]
})
function switchMode(mode) {
currentMode.value = mode
emit('modeChange', mode)
}
function onPeriodChange(e) {
const idx = e.detail.value
const val = periodRange.value[idx]?.value
if (!val) return
currentPeriod.value = val
loadQualityData()
}
async function loadQualityData() {
const res = await request({
url: '/admin-api/mes/plan/quality-overview',
method: 'get',
params: { period: currentPeriod.value },
showLoading: !isInitialLoad.value
})
const data = res?.data || {}
qualityData.totalWangongNumber = data.totalWangongNumber ?? 0
qualityData.totalPassNumber = data.totalPassNumber ?? 0
qualityData.totalNoPassNumber = data.totalNoPassNumber ?? 0
qualityData.totalPassRate = data.totalPassRate ?? 0
const productList = data.productPassRateList || []
const sorted = [...productList].sort((a, b) => {
const va = parseFloat(a.passRate) || 0
const vb = parseFloat(b.passRate) || 0
return vb - va
})
const categories = sorted.map((item) => {
const name = item.productName || ''
const match = name.match(/^(【[^】]*】)(.*)$/)
if (match) {
return match[1] + '\n' + match[2]
}
return name
})
const rateData = sorted.map((item) => {
const v = parseFloat(item.passRate)
return isNaN(v) ? 0 : Math.round(v * 100) / 100
})
const total = sorted.length
const colors = sorted.map((_, index) => {
const ratio = total > 1 ? index / (total - 1) : 0
const r = Math.round(26 + ratio * (74 - 26))
const g = Math.round(58 + ratio * (144 - 58))
const b = Math.round(92 + ratio * (194 - 92))
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
})
rankingChartData.categories = categories
rankingChartData.series = [{
name: t('dashboard.passRate'),
data: rateData,
linearColor: colors.map((c) => ['#e8f0f8', c])
}]
const trendList = data.trendList || []
const trendCategories = trendList.map((item) => (item.day || '').substring(5))
const passData = trendList.map((item) => item.passNumber ?? 0)
const noPassData = trendList.map((item) => item.noPassNumber ?? 0)
trendChartData.categories = trendCategories
trendChartData.series = [
{ name: t('dashboard.passNumber'), data: passData },
{ name: t('dashboard.noPassNumber'), data: noPassData }
]
}
onMounted(async () => {
await loadQualityData()
isInitialLoad.value = false
})
defineExpose({ loadQualityData })
</script>
<style lang="scss" scoped>
.quality-section {
margin-bottom: 24rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
gap: 16rpx;
view {
display: flex;
align-items: center;
gap: 20rpx;
}
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
flex-shrink: 0;
}
.filter-select {
display: flex;
align-items: center;
padding: 12rpx 24rpx;
background: #ffffff;
border-radius: 12rpx;
margin-right: 20rpx;
&:active {
background: #e8ecf0;
}
}
.filter-text {
font-size: 26rpx;
color: #1a3a5c;
font-weight: 500;
margin-right: 8rpx;
}
.filter-arrow {
font-size: 20rpx;
color: #999999;
}
.quality-stats {
display: flex;
justify-content: space-between;
margin-bottom: 24rpx;
}
.quality-stat-card {
flex: 1;
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx 12rpx;
margin: 0 6rpx;
text-align: center;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
.stat-value {
display: block;
font-size: 40rpx;
font-weight: bold;
color: #1a3a5c;
margin-bottom: 8rpx;
&.pass {
color: #18bc37;
}
&.fail {
color: #ff4d4f;
}
&.rate {
color: #4a90c2;
}
}
.stat-label {
display: block;
font-size: 24rpx;
color: #666666;
word-break: break-all;
}
.chart-section {
background: #ffffff;
border-radius: 20rpx;
padding: 28rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
margin-bottom: 24rpx;
}
.chart-header {
margin-bottom: 20rpx;
}
.chart-title {
font-size: 28rpx;
font-weight: 500;
color: #1a3a5c;
}
.ranking-scroll {
width: 100%;
min-width: 100%;
}
.chart-box {
width: 100%;
height: 450rpx;
min-width: 100%;
}
</style>

@ -0,0 +1,153 @@
<template>
<view class="stats-section">
<view class="section-header">
<text class="section-title">{{ currentMode === 'production' ? t('dashboard.productionOverview') : t('dashboard.qualityOverview') }}</text>
<ModeSwitchPopup :currentMode="currentMode" @modeChange="switchMode" />
</view>
<scroll-view scroll-x enable-flex class="stats-scroll">
<view class="stats-grid">
<view
v-for="(stat, index) in taskStats"
:key="stat.key"
class="stat-card"
:style="cardStyle(index)"
>
<text class="stat-value">{{ formatNumber(stat.value) }}</text>
<text class="stat-label">{{ stat.label }}</text>
</view>
</view>
</scroll-view>
<PlanSection />
<DeviceSection />
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import request from '@/utils/request'
import { formatNumber } from '@/utils/format'
import PlanSection from '@/components/dashboard/PlanSection.vue'
import DeviceSection from '@/components/dashboard/DeviceSection.vue'
import ModeSwitchPopup from '@/components/dashboard/ModeSwitchPopup.vue'
const { t } = useI18n()
const emit = defineEmits(['modeChange'])
const currentMode = ref('production')
const COLOR_PALETTE = [
{ border: '#1a3a5c', value: '#1a3a5c' },
{ border: '#ff8c00', value: '#ff8c00' },
{ border: '#18bc37', value: '#18bc37' },
{ border: '#4a90c2', value: '#4a90c2' },
{ border: '#e74c3c', value: '#e74c3c' },
{ border: '#8e44ad', value: '#8e44ad' },
{ border: '#16a085', value: '#16a085' },
{ border: '#d35400', value: '#d35400' },
{ border: '#2980b9', value: '#2980b9' },
{ border: '#c0392b', value: '#c0392b' }
]
const taskStats = ref([])
function cardStyle(index) {
const color = COLOR_PALETTE[index % COLOR_PALETTE.length]
return {
borderLeft: `6rpx solid ${color.border}`,
color: color.value
}
}
async function loadProductionStats() {
const res = await request({ url: '/admin-api/mes/dashboard/getProduction', method: 'get' })
const items = res?.data?.taskItems || []
taskStats.value = items.map((i) => ({
key: String(i.key),
label: i.label || '',
value: Number(i.value ?? 0)
}))
}
function switchMode(mode) {
currentMode.value = mode
emit('modeChange', mode)
}
onMounted(() => {
loadProductionStats()
})
defineExpose({ loadProductionStats })
</script>
<style lang="scss" scoped>
.stats-section {
margin-bottom: 24rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
}
.stats-scroll {
width: 100%;
white-space: nowrap;
}
.stats-grid {
display: inline-flex;
flex-wrap: nowrap;
padding: 0 4rpx;
}
.stat-card {
flex-shrink: 0;
width: 174rpx;
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx 12rpx;
margin: 0 6rpx;
text-align: center;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
white-space: normal;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
&:active {
transform: scale(0.98);
}
}
.stat-value {
display: block;
font-size: 40rpx;
font-weight: bold;
margin-bottom: 6rpx;
}
.stat-label {
display: block;
font-size: 24rpx;
color: #666666;
word-break: break-all;
}
</style>

@ -869,9 +869,11 @@ export default {
const ctx = cfu.option[cid].context;
if(typeof ctx === "object" && !!!cfu.option[cid].update){
ctx.clearRect(0, 0, this.cWidth*this.pixel, this.cHeight*this.pixel);
if(typeof ctx.draw === 'function'){
ctx.draw();
}
}
}
},
init() {
let cid = this.cid

@ -2006,7 +2006,15 @@ function calYAxisData(series, opts, config, context) {
};
rangesFormatArr[i] = rangesArr[i].map(function(items,index) {
items = yData.formatter(items,index,opts);
yAxisWidthArr[i].width = Math.max(yAxisWidthArr[i].width, measureText(items, yAxisFontSizes, context) + 5);
var itemWidth;
if (items.indexOf('\n') !== -1) {
itemWidth = Math.max.apply(null, items.split('\n').map(function(line) {
return measureText(line, yAxisFontSizes, context);
}));
} else {
itemWidth = measureText(items, yAxisFontSizes, context);
}
yAxisWidthArr[i].width = Math.max(yAxisWidthArr[i].width, itemWidth + 5);
return items;
});
let calibration = yData.calibration ? 4 * opts.pix : 0;
@ -2037,7 +2045,15 @@ function calYAxisData(series, opts, config, context) {
var yAxisFontSize = opts.yAxis.fontSize * opts.pix || config.fontSize;
rangesFormatArr[0] = rangesArr[0].map(function(item,index) {
item = opts.yAxis.formatter(item,index,opts);
yAxisWidthArr[0].width = Math.max(yAxisWidthArr[0].width, measureText(item, yAxisFontSize, context) + 5);
var itemWidth;
if (item.indexOf('\n') !== -1) {
itemWidth = Math.max.apply(null, item.split('\n').map(function(line) {
return measureText(line, yAxisFontSize, context);
}));
} else {
itemWidth = measureText(item, yAxisFontSize, context);
}
yAxisWidthArr[0].width = Math.max(yAxisWidthArr[0].width, itemWidth + 5);
return item;
});
yAxisWidthArr[0].width += 3 * opts.pix;
@ -4583,7 +4599,19 @@ function drawXAxis(categories, opts, config, context) {
context.beginPath();
context.setFontSize(xAxisFontSize);
context.setFillStyle(opts.xAxis.fontColor || opts.fontColor);
context.fillText(String(xitem), xAxisPoints[index] + offset, startY + opts.xAxis.marginTop * opts.pix + (opts.xAxis.lineHeight - opts.xAxis.fontSize) * opts.pix / 2 + opts.xAxis.fontSize * opts.pix);
var xitemStr = String(xitem);
if (xitemStr.indexOf('\n') !== -1) {
var lines = xitemStr.split('\n');
var lineHeight = opts.xAxis.lineHeight * opts.pix || xAxisFontSize * 1.2;
var totalHeight = lines.length * lineHeight;
var baseY = startY + opts.xAxis.marginTop * opts.pix + (opts.xAxis.lineHeight - opts.xAxis.fontSize) * opts.pix / 2 + opts.xAxis.fontSize * opts.pix - (totalHeight - lineHeight) / 2;
lines.forEach(function(line, lineIndex) {
var lineOffset = -measureText(line, xAxisFontSize, context) / 2;
context.fillText(line, xAxisPoints[index] + lineOffset, baseY + lineIndex * lineHeight);
});
} else {
context.fillText(xitemStr, xAxisPoints[index] + offset, startY + opts.xAxis.marginTop * opts.pix + (opts.xAxis.lineHeight - opts.xAxis.fontSize) * opts.pix / 2 + opts.xAxis.fontSize * opts.pix);
}
context.closePath();
context.stroke();
}
@ -4764,7 +4792,18 @@ function drawYAxis(series, opts, config, context) {
context.setTextAlign('center');
tmpstrat = tStartLeft - yAxisWidth.width / 2
}
context.fillText(String(item), tmpstrat, pos + yAxisFontSize / 2 - 3 * opts.pix);
var itemStr = String(item);
if (itemStr.indexOf('\n') !== -1) {
var lines = itemStr.split('\n');
var lineHeight = yAxisFontSize * 1.2;
var totalHeight = lines.length * lineHeight;
var baseY = pos + yAxisFontSize / 2 - 3 * opts.pix - (totalHeight - lineHeight) / 2;
lines.forEach(function(line, lineIndex) {
context.fillText(line, tmpstrat, baseY + lineIndex * lineHeight);
});
} else {
context.fillText(itemStr, tmpstrat, pos + yAxisFontSize / 2 - 3 * opts.pix);
}
} else if (yAxisWidth.position == 'right') {
//画刻度线
@ -4786,7 +4825,18 @@ function drawYAxis(series, opts, config, context) {
context.setTextAlign('center');
tmpstrat = tStartRight + yAxisWidth.width / 2
}
context.fillText(String(item), tmpstrat, pos + yAxisFontSize / 2 - 3 * opts.pix);
var itemStr = String(item);
if (itemStr.indexOf('\n') !== -1) {
var lines = itemStr.split('\n');
var lineHeight = yAxisFontSize * 1.2;
var totalHeight = lines.length * lineHeight;
var baseY = pos + yAxisFontSize / 2 - 3 * opts.pix - (totalHeight - lineHeight) / 2;
lines.forEach(function(line, lineIndex) {
context.fillText(line, tmpstrat, baseY + lineIndex * lineHeight);
});
} else {
context.fillText(itemStr, tmpstrat, pos + yAxisFontSize / 2 - 3 * opts.pix);
}
} else if (yAxisWidth.position == 'center') {
//画刻度线
if (yData.calibration == true) {
@ -4808,7 +4858,18 @@ function drawYAxis(series, opts, config, context) {
context.setTextAlign('center');
tmpstrat = tStartCenter - yAxisWidth.width / 2
}
context.fillText(String(item), tmpstrat, pos + yAxisFontSize / 2 - 3 * opts.pix);
var itemStr = String(item);
if (itemStr.indexOf('\n') !== -1) {
var lines = itemStr.split('\n');
var lineHeight = yAxisFontSize * 1.2;
var totalHeight = lines.length * lineHeight;
var baseY = pos + yAxisFontSize / 2 - 3 * opts.pix - (totalHeight - lineHeight) / 2;
lines.forEach(function(line, lineIndex) {
context.fillText(line, tmpstrat, baseY + lineIndex * lineHeight);
});
} else {
context.fillText(itemStr, tmpstrat, pos + yAxisFontSize / 2 - 3 * opts.pix);
}
}
context.closePath();
context.stroke();

@ -3,16 +3,16 @@
<template v-for="(item, index) in options">
<template v-if="values.includes(item.value)">
<span
v-if="item.elTagType == 'default' || item.elTagType == ''"
v-if="item.tagType == 'default' || item.tagType == ''"
:key="item.value"
:index="index"
:class="item.elTagClass"
>{{ item.label }}</span>
<uni-tag
<u-tag
v-else
:key="item.value + ''"
:index="index"
:type="elTagType(item.elTagType)"
:type="mapTagType(item.tagType)"
:class="item.elTagClass"
:text="item.label"
/>
@ -40,15 +40,16 @@ const values = computed(() => {
}
})
const elTagType = (tagType) =>{
tagType === 'danger' ? 'error' : tagType
tagType === 'info' ? 'default' : tagType
const mapTagType = (tagType) => {
if (tagType === 'danger') return 'error'
if (tagType === 'default') return 'info'
return tagType
}
</script>
<style scoped>
.uni-tag + .uni-tag {
.u-tag + .u-tag {
margin-left: 10px;
}
</style>

@ -1,7 +1,6 @@
// 应用全局配置
const config = {
// baseUrl: 'http://47.106.185.127:48080',127.0.0.1
baseUrl: 'http://47.106.185.127:48080',
baseUrl: import.meta.env.VITE_APP_BASE_URL,
// 应用信息
appInfo: {
// 应用名称

@ -9,7 +9,12 @@ export default {
mounted(el, binding, vnode) {
const { value } = binding
const all_permission = "*:*:*";
const permissions = useUserStore().permissions
let permissions = []
try {
permissions = useUserStore().permissions || []
} catch (e) {
permissions = []
}
if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value

@ -9,7 +9,12 @@ export default {
mounted(el, binding, vnode) {
const { value } = binding
const super_admin = "admin";
const roles = useUserStore().roles
let roles = []
try {
roles = useUserStore().roles || []
} catch (e) {
roles = []
}
if (value && value instanceof Array && value.length > 0) {
const roleFlag = value

@ -0,0 +1,823 @@
export default {
common: {
submit: 'Submit',
close: 'Close',
exit: 'Exit',
notice: 'Notice',
moduleBuilding: 'This module is under construction',
updateSuccess: 'Updated successfully',
confirmLogout: 'Are you sure you want to log out?',
languageSwitched: 'Language switched',
more: 'More',
reset: 'Reset',
complete: 'Done',
edit: 'Edit',
saveFailed: 'Save failed'
},
tab: {
home: 'Home',
report: 'Reports',
work: 'Manage',
mine: 'Mine'
},
nav: {
home: 'Home',
mine: 'Profile',
avatar: 'Edit Avatar',
info: 'Profile',
editInfo: 'Edit Profile',
pwd: 'Change Password',
setting: 'App Settings',
help: 'FAQ',
about: 'About'
},
dashboard: {
welcome: 'Welcome to',
subtitle: 'Besure Digital Intelligent Control Platform',
functionNav: 'Function Navigation',
editNavMenu: 'Edit Shortcuts',
configuredNav: 'Added',
unconfiguredNav: 'Add More',
dragHint: 'Drag icons to reorder, icons above will be displayed in control center',
clickHint: 'Tap icons to add or remove from configuration',
allNavMenu: 'All Functions',
productionOverview: 'Production Overview',
qualityOverview: 'Quality Overview',
productionPlan: 'Production Summary',
production: 'Production',
quality: 'Quality',
totalCount: 'Total',
passCount: 'Pass Count',
failCount: 'Fail Count',
qualityTrend: 'Quality Trend',
chartPlaceholder: 'Chart loading...',
totalWangongNumber: 'Total Reported',
totalPassNumber: 'Total Passed',
totalNoPassNumber: 'Total Failed',
totalPassRate: 'Total Pass Rate',
productPassRateRanking: 'Product Pass Rate Ranking',
passNumber: 'Pass Count',
noPassNumber: 'Fail Count',
periodLastWeek: 'Last Week',
periodThisWeek: 'This Week',
periodLast7Days: 'Last 7 Days',
periodLastMonth: 'Last Month',
periodThisMonth: 'This Month',
periodLastYear: 'Last Year',
collapseList: 'Collapse',
viewMore: 'View More ',
productName: 'Product',
pipeline: 'Production Line',
planNumber: 'Planned Qty',
planStart: 'Start',
planEnd: 'End',
back: 'Back',
todoTitle: 'To-do Tasks',
noTodo: 'No pending tasks',
taskCode: 'Task Code: {value}',
taskType: 'Task Type: {value}',
taskTarget: 'Target: {value}',
createTime: 'Created At: {value}',
all: 'Total',
pending: 'Pending',
running: 'In Progress',
finished: 'Done',
mold: 'Mold',
equipment: 'Equipment',
keypart: 'Key Part',
spare: 'Spare Part',
product: 'Product Material',
statusScheduled: 'Scheduled',
statusTrial: 'Trial',
statusMass: 'Mass',
statusPause: 'Paused',
statusWaitStockIn: 'Waiting Stock-in',
viewPlan: 'View Plan: {code}',
filterTask: 'Task',
filterProduct: 'Product',
dateRange: 'Date Range',
startDate: 'Start Time',
endDate: 'End Time',
baogongNum: 'Reported',
passNum: 'Qualified',
noPassNum: 'Unqualified',
passRate: 'Pass Rate',
trendChart: 'Trend',
taskPlaceholder: 'Task mode under development',
totalTask: 'Total Tasks',
issuedNum: 'Issued',
partialScheduledNum: 'Partial Scheduled',
waitingProduction: 'Waiting',
producing: 'Producing',
completed: 'Completed',
taskTrend: 'Task Trend',
rangeYear: 'Last Year',
rangeMonth: 'This Month',
rangeWeek: 'This Week',
rangeToday: 'Today',
rangeCustom: 'Custom',
monday: 'Mon',
tuesday: 'Tue',
wednesday: 'Wed',
thursday: 'Thu',
friday: 'Fri',
saturday: 'Sat',
sunday: 'Sun'
},
deviceOverview: {
title: 'Device Overview',
totalDevices: 'Total',
runningCount: 'Running',
standbyCount: 'Standby',
faultCount: 'Fault',
offlineCount: 'Offline',
utilizationRate: 'Utilization',
bootRate: 'Boot Rate',
faultRate: 'Fault Rate',
rateTrend: 'Utilization / Boot Rate Trend',
onlyScheduled: 'Scheduled Only',
skipHoliday: 'Skip Holidays',
periodLastWeek: 'Last Week',
periodThisWeek: 'This Week',
periodLast7Days: 'Last 7 Days',
periodLastMonth: 'Last Month',
periodThisMonth: 'This Month',
periodLastYear: 'Last Year',
utilizationRanking: 'Last 7 Days Utilization Ranking',
utilizationRateTrend: 'Utilization Rate',
deviceRateTrend: 'Single Device 7-Day Utilization/Boot Rate Trend',
selectDevice: 'Select Device',
selectDeviceHint: 'Please select a device to view trend'
},
taskList: {
filter: 'Filter',
code: 'Task Code',
status: 'Status',
taskType: 'Task Type',
orderDate: 'Order Date',
deliveryDate: 'Delivery Date',
remark: 'Remark',
createTime: 'Create Time',
totalNumber: 'Total',
planNumber: 'Planned',
unPlanNumber: 'Unplanned',
storedPlanNumber: 'Stored',
urgent: 'Urgent',
noData: 'No task data',
viewTask: 'View Task: {code}'
},
planList: {
taskCode: 'Task Code',
planCode: 'Plan Code',
employeeName: 'Reporter',
baogongNum: 'Reported',
passNum: 'Passed',
noPassNum: 'Failed',
passRate: 'Pass Rate',
baogongTime: 'Report Time',
reason: 'Reason',
noData: 'No report data',
viewDetail: 'View Detail: {code}'
},
functionCommon: {
search: 'Search',
cancel: 'Cancel',
save: 'Save',
loading: 'Loading...',
loadingMore: 'Loading more...',
noMoreData: 'No more data',
noIdView: 'Missing ID, cannot view details',
noIdEdit: 'Missing ID, cannot edit',
noIdDelete: 'Missing ID, cannot delete',
loadFailed: 'Load failed',
deleteSuccess: 'Deleted successfully',
deleteFailed: 'Delete failed',
saveFailed: 'Save failed',
createSuccess: 'Created successfully',
updateSuccess: 'Updated successfully',
confirmDelete: 'Confirm deletion',
uploading: 'Uploading',
uploadImageFailed: 'Image upload failed',
yes: 'Yes',
no: 'No',
all: 'All',
noMore: 'No more data',
confirmTitle: 'Confirm'
},
moldGet: {
moduleName: 'Mold Stock-out',
subTitle: 'Filter quickly by no and status',
detailTitle: 'Mold Stock-out Detail',
basicInfo: 'Basic Info',
outNo: 'Stock-out No',
outType: 'Stock-out Type',
outTime: 'Stock-out Time',
outTimeSingle: 'Stock-out Date',
outTimePlaceholder: 'Select stock-out date',
warehouse: 'Warehouse',
allWarehouse: 'All Warehouses',
warehousePlaceholder: 'Select warehouse',
creator: 'Creator',
status: 'Status',
allStatus: 'All Status',
remark: 'Remark',
itemRemark: 'Item Remark',
attachment: 'Attachment',
fileUrlPlaceholder: 'Enter attachment URL',
remarkPlaceholder: 'Enter remark',
moldName: 'Mold',
moldCode: 'Mold Code',
moldStatus: 'Mold Status',
moldUseTime: 'Use Time',
machineName: 'Machine',
isEnable: 'Enabled',
createTime: 'Created At',
searchNo: 'Enter stock-out no',
searchMold: 'Enter mold code or name',
searchCode: 'Enter mold code',
searchName: 'Enter mold name',
itemListTitle: 'Item List',
selectMold: 'Select Mold',
noItems: 'No items',
count: 'Count',
noAuto: 'Generated automatically',
createTitle: 'Create Mold Stock-out',
editTitle: 'Edit Mold Stock-out',
edit: 'Edit',
delete: 'Delete',
approve: 'Approve',
empty: 'No mold stock-out data',
noMoldData: 'No mold options',
loadEditFailed: 'Failed to load edit data',
validatorOutTimeRequired: 'Stock-out date is required',
validatorWarehouseRequired: 'Warehouse is required',
validatorItemRequired: 'Select at least one mold',
validatorCountRequired: 'Count must be greater than 0',
confirmDelete: 'Delete stock-out {no}?',
confirmApprove: 'Approve stock-out {no}?',
approveSuccess: 'Approved successfully'
},
moldReturn: {
moduleName: 'Mold Stock-in',
subTitle: 'Filter quickly by no and status',
detailTitle: 'Mold Stock-in Detail',
basicInfo: 'Basic Info',
inNo: 'Stock-in No',
inType: 'Stock-in Type',
inTime: 'Stock-in Time',
inTimeSingle: 'Stock-in Date',
inTimePlaceholder: 'Select stock-in date',
warehouse: 'Warehouse',
allWarehouse: 'All Warehouses',
warehousePlaceholder: 'Select warehouse',
creator: 'Creator',
status: 'Status',
allStatus: 'All Status',
remark: 'Remark',
itemRemark: 'Item Remark',
attachment: 'Attachment',
fileUrlPlaceholder: 'Enter attachment URL',
remarkPlaceholder: 'Enter remark',
moldName: 'Mold',
moldCode: 'Mold Code',
moldStatus: 'Mold Status',
moldUseTime: 'Use Time',
searchNo: 'Enter stock-in no',
searchCode: 'Enter mold code',
searchName: 'Enter mold name',
itemListTitle: 'Item List',
selectMold: 'Select Mold',
noItems: 'No items',
count: 'Count',
noAuto: 'Generated automatically',
createTitle: 'Create Mold Stock-in',
editTitle: 'Edit Mold Stock-in',
approve: 'Approve',
empty: 'No mold stock-in data',
noMoldData: 'No mold options',
loadEditFailed: 'Failed to load edit data',
validatorInTimeRequired: 'Stock-in date is required',
validatorWarehouseRequired: 'Warehouse is required',
validatorItemRequired: 'Select at least one mold',
validatorCountRequired: 'Count must be greater than 0',
confirmDelete: 'Delete stock-in {no}?',
confirmApprove: 'Approve stock-in {no}?',
approveSuccess: 'Approved successfully'
},
moldOperate: {
moduleName: 'Mold Operate',
subTitle: 'Mold mounting and dismounting records',
detailTitle: 'Mold Operate Detail',
basicInfo: 'Basic Info',
tabUp: 'Mount',
tabDown: 'Dismount',
operateType: 'Operation Type',
mold: 'Mold',
lowerMold: 'Dismount Mold',
selectMold: 'Select Mold',
noSelectedMold: 'No selected molds',
allMold: 'All Molds',
searchRemark: 'Enter remark keyword',
searchCode: 'Enter mold code',
searchName: 'Enter mold name',
moldCode: 'Mold Code',
moldName: 'Mold Name',
device: 'Device',
deviceName: 'Device Name',
creatorName: 'Creator',
remark: 'Remark',
createTime: 'Created At',
createTitle: 'Create Mold Operate',
editTitle: 'Edit Mold Operate',
empty: 'No mold operate data',
noMoldData: 'No mold options',
placeholderDevice: 'Select device',
placeholderRemark: 'Enter remark',
validatorOperateTypeRequired: 'Operation type is required',
validatorDeviceRequired: 'Device is required',
validatorMoldRequired: 'Select at least one mold for mounting',
validatorLowerMoldRequired: 'Select at least one mold for dismounting',
loadEditFailed: 'Failed to load edit data',
confirmDelete: 'Confirm delete this mold operate record?'
},
moldInspectionItems: {
moduleName: 'Inspection Items',
subTitle: 'Mold management inspection items',
detailTitle: 'Inspection Item Detail',
basicInfo: 'Basic Info',
code: 'Item Code',
name: 'Item Name',
inspectionMethod: 'Inspection Method',
valueType: 'Value Type',
isEnable: 'Enabled',
judgmentCriteria: 'Judgment Criteria',
creatorName: 'Creator',
createTime: 'Created At',
searchPlaceholder: 'Enter code/name/criteria',
createTitle: 'Create Inspection Item',
editTitle: 'Edit Inspection Item',
empty: 'No inspection items',
loadEditFailed: 'Failed to load edit data',
confirmDelete: 'Confirm delete this inspection item?',
placeholderCode: 'Enter item code',
placeholderName: 'Enter item name',
placeholderInspectionMethod: 'Select inspection method',
placeholderValueType: 'Select value type',
placeholderIsEnable: 'Select enabled status',
placeholderJudgmentCriteria: 'Enter judgment criteria',
validatorSubjectCodeRequired: 'Item code is required',
validatorSubjectNameRequired: 'Item name is required',
validatorInspectionMethodRequired: 'Inspection method is required',
validatorValueTypeRequired: 'Value type is required',
validatorIsEnableRequired: 'Enabled status is required',
validatorJudgmentCriteriaRequired: 'Judgment criteria is required'
},
moldInspectionPlan: {
moduleName: 'Inspection Plan',
subTitle: 'Mold management inspection plan',
detailTitle: 'Inspection Plan Detail',
basicInfo: 'Basic Info',
planName: 'Plan Name',
planType: 'Plan Type',
planTypeMaintain: 'Maintenance',
planTypeInspect: 'Inspection',
description: 'Description',
subjectName: 'Inspection Items',
creatorName: 'Creator',
createTime: 'Created At',
updateTime: 'Updated At',
searchPlaceholder: 'Enter plan name',
createTitle: 'Create Inspection Plan',
editTitle: 'Edit Inspection Plan',
empty: 'No inspection plan data',
loadEditFailed: 'Failed to load edit data',
confirmDelete: 'Confirm delete this inspection plan?',
placeholderPlanName: 'Enter plan name',
placeholderPlanType: 'Select plan type',
placeholderDescription: 'Enter description',
placeholderSubjectSelect: 'Select inspection items',
subjectSelectTitle: 'Select Inspection Items',
noSubjectData: 'No inspection items',
validatorPlanNameRequired: 'Plan name is required',
validatorPlanTypeRequired: 'Plan type is required',
subjectListTitle: 'Related Inspection Items',
subjectCode: 'Item Code',
inspectionMethod: 'Inspection Method',
judgmentCriteria: 'Judgment Criteria'
},
moldTaskConfig: {
moduleName: 'Inspection Task',
subTitle: 'Mold management inspection task',
detailTitle: 'Inspection Task Detail',
basicInfo: 'Basic Info',
name: 'Task Name',
taskType: 'Task Type',
taskTypeInspect: 'Inspection',
taskTypeMaintain: 'Maintenance',
moldList: 'Mold List',
projectForm: 'Inspection Plan',
projectFormName: 'Project Form',
dateRange: 'Valid Date Range',
startDate: 'Start Date',
endDate: 'End Date',
cronExpression: 'Cron Expression',
operableUsers: 'Operable Users',
enabled: 'Enabled',
creatorName: 'Creator',
createTime: 'Created At',
updateTime: 'Updated At',
searchPlaceholder: 'Enter task name',
createTitle: 'Create Inspection Task',
editTitle: 'Edit Inspection Task',
empty: 'No inspection task data',
loadEditFailed: 'Failed to load edit data',
confirmDeleteContent: 'Confirm delete this inspection task [{name}]?',
placeholderName: 'Enter task name',
placeholderTaskType: 'Select task type',
placeholderMoldList: 'Select molds',
placeholderProjectForm: 'Select inspection plans',
placeholderStartDate: 'Select start date',
placeholderEndDate: 'Select end date',
placeholderCron: 'Enter cron expression',
placeholderOperableUsers: 'Select operable users',
validatorNameRequired: 'Task name is required',
validatorTaskTypeRequired: 'Task type is required',
validatorMoldListRequired: 'Mold list is required',
validatorProjectFormRequired: 'Inspection plan is required',
validatorDateRangeRequired: 'Date range is required',
createTicketSuccess: 'Work order created successfully',
createTicketFail: 'Work order creation failed',
moldSelectTitle: 'Select Molds',
planSelectTitle: 'Select Inspection Plans',
userSelectTitle: 'Select Operable Users'
},
moldWorkOrder: {
moduleName: 'Inspection Records',
subTitle: 'Mold management inspection records',
detailTitle: 'Inspection Record Detail',
basicInfo: 'Basic Info',
resultListTitle: 'Inspection Results',
planNo: 'Ticket No.',
moldName: 'Mold Name',
planType: 'Task Type',
planTypeInspect: 'Inspection',
planTypeMaintain: 'Maintenance',
configName: 'Task Config',
jobStatus: 'Job Status',
jobStatusPending: 'Pending',
jobStatusProcessing: 'Processing',
jobStatusCompleted: 'Completed',
jobStatusTimeout: 'Timeout',
jobStatusCancelled: 'Cancelled',
jobResult: 'Job Result',
jobResultOk: 'OK',
jobResultNg: 'NG',
operatorName: 'Operator',
taskTime: 'Task Time',
taskEndTime: 'End Time',
cancelReason: 'Cancel Reason',
createTime: 'Created At',
searchPlaceholder: 'Enter ticket no.',
empty: 'No inspection records',
cancelTask: 'Cancel Task',
cancelSuccess: 'Cancelled successfully',
cancelFail: 'Cancel failed',
placeholderCancelReason: 'Enter cancel reason',
validatorCancelReasonRequired: 'Cancel reason is required',
inspectionItemName: 'Inspection Item',
inspectionMethod: 'Inspection Method',
judgmentCriteria: 'Judgment Criteria',
valueType: 'Value Type',
inspectionResult: 'Inspection Result',
inspectionResultPending: 'Pending',
inspectionResultPass: 'Pass',
inspectionResultFail: 'Fail',
textInput: 'Input Value',
remark: 'Remark',
images: 'Images',
noResultData: 'No inspection result data',
loadMore: 'Load More'
},
mine: {
clickLogin: 'Tap to sign in',
username: 'Username: {name}',
profile: 'Profile',
feedback: 'Feedback',
service: 'Support',
changePassword: 'Change Password',
logout: 'Log Out',
editProfile: 'Edit Profile',
faq: 'FAQ',
about: 'About',
appSettings: 'Settings'
},
setting: {
language: 'System Language',
currentLanguage: 'Current: {language}',
switchLanguage: 'Switch Language',
checkUpdate: 'Check Updates',
cleanCache: 'Clear Cache',
logout: 'Log Out',
zhCN: 'Chinese',
enUS: 'English'
},
about: {
appName: 'Besure Production System',
version: 'Version',
email: 'Official Email',
hotline: 'Service Hotline',
website: 'Website'
},
help: {
appUserQuestion: 'App User Questions',
otherQuestion: 'Other Questions',
appFeatureQuestion: 'What business features are supported by the app?',
appFeatureAnswer: 'Plan start, production report, material feeding records, production records',
reportQuestion: 'How can I submit a production report?',
reportAnswer: 'You can submit it on the report page.',
planQuestion: 'How is plan management handled?',
planAnswer: 'Use the plan management module.',
materialQuestion: 'How to manage feeding records?',
materialAnswer: 'Use the feeding record module.',
logoutQuestion: 'How do I log out?',
logoutAnswer: 'Go to [Mine] - [App Settings] - [Log Out] to sign out.',
avatarQuestion: 'How do I change my avatar?',
avatarAnswer: 'Go to [Mine] - [Choose Avatar] - [Submit] to update your avatar.',
passwordQuestion: 'How do I change my login password?',
passwordAnswer: 'Go to [Mine] - [App Settings] - [Change Password] to update your password.'
},
info: {
username: 'Username',
nickname: 'Nickname',
gender: 'Gender',
male: 'Male',
female: 'Female',
phone: 'Mobile',
email: 'Email',
createdAt: 'Created At'
},
editInfo: {
nickname: 'Nickname',
nicknamePlaceholder: 'Enter nickname',
phone: 'Mobile',
phonePlaceholder: 'Enter mobile number',
email: 'Email',
emailPlaceholder: 'Enter email',
gender: 'Gender',
nicknameRequired: 'Nickname is required',
phoneRequired: 'Mobile number is required',
phoneInvalid: 'Please enter a valid mobile number',
emailRequired: 'Email is required',
emailInvalid: 'Please enter a valid email'
},
pwd: {
oldPassword: 'Current Password',
newPassword: 'New Password',
confirmPassword: 'Confirm Password',
oldPasswordPlaceholder: 'Enter current password',
newPasswordPlaceholder: 'Enter new password',
confirmPasswordPlaceholder: 'Confirm new password',
oldPasswordRequired: 'Current password is required',
newPasswordRequired: 'New password is required',
passwordLength: 'Length must be between 6 and 20 characters',
confirmPasswordRequired: 'Please confirm password',
passwordNotMatch: 'The two passwords do not match'
},
avatar: {
chooseAvatar: 'Choose Avatar'
},
materialCategory: {
moduleName: 'Product Category',
subTitle: 'Product material category management',
detailTitle: 'Category Detail',
basicInfo: 'Basic Info',
code: 'Category Code',
name: 'Category Name',
parentName: 'Parent Category',
rootCategory: 'None (Top Level)',
sort: 'Sort',
status: 'Status',
statusEnable: 'Enable',
statusDisable: 'Disable',
createTime: 'Created At',
searchPlaceholder: 'Enter category code/name',
createTitle: 'Create Category',
editTitle: 'Edit Category',
empty: 'No category data',
loadEditFailed: 'Failed to load edit data',
noId: 'Missing category ID',
loadFailed: 'Failed to load category detail',
placeholderCode: 'Enter category code',
placeholderName: 'Enter category name',
placeholderParent: 'Select parent category',
placeholderSort: 'Enter sort order',
placeholderStatus: 'Select status',
validatorCodeRequired: 'Category code is required',
validatorNameRequired: 'Category name is required',
validatorSortRequired: 'Sort order is required',
validatorStatusRequired: 'Status is required',
confirmDeleteContent: 'Confirm delete category [{name}]?'
},
materialInfo: {
moduleName: 'Product Material',
subTitle: 'Product material information management',
detailTitle: 'Material Detail',
basicInfo: 'Basic Info',
barCode: 'Barcode',
name: 'Material Name',
category: 'Category',
unit: 'Unit',
standard: 'Specification',
expiryDay: 'Shelf Life (Days)',
status: 'Status',
statusEnable: 'Enable',
statusDisable: 'Disable',
remark: 'Remark',
createTime: 'Created At',
autoCode: 'Auto Generate',
searchPlaceholder: 'Enter material name/barcode',
createTitle: 'Create Material',
editTitle: 'Edit Material',
empty: 'No material data',
loadEditFailed: 'Failed to load edit data',
noId: 'Missing material ID',
loadFailed: 'Failed to load material detail',
placeholderBarCode: 'Enter barcode',
placeholderName: 'Enter material name',
placeholderCategory: 'Select category',
placeholderUnit: 'Select unit',
placeholderStandard: 'Enter specification',
placeholderExpiryDay: 'Enter shelf life days',
placeholderStatus: 'Select status',
placeholderRemark: 'Enter remark',
validatorBarCodeRequired: 'Barcode is required',
validatorNameRequired: 'Material name is required',
validatorCategoryRequired: 'Category is required',
validatorUnitRequired: 'Unit is required',
confirmDeleteContent: 'Confirm delete material [{name}]?'
},
productBom: {
moduleName: 'Product BOM',
subTitle: 'Product BOM management',
detailTitle: 'BOM Detail',
basicInfo: 'Basic Info',
code: 'BOM Code',
version: 'Version',
product: 'Product',
unit: 'Unit',
yieldRate: 'Yield Rate',
isEnable: 'Enabled',
enableYes: 'Yes',
enableNo: 'No',
remark: 'Remark',
createTime: 'Created At',
searchPlaceholder: 'Enter BOM code',
createTitle: 'Create BOM',
editTitle: 'Edit BOM',
empty: 'No BOM data',
loadEditFailed: 'Failed to load edit data',
noId: 'Missing BOM ID',
loadFailed: 'Failed to load BOM detail',
placeholderCode: 'Enter BOM code',
placeholderVersion: 'Enter version',
placeholderProduct: 'Select product',
placeholderUnit: 'Select unit',
placeholderYieldRate: 'Enter yield rate',
placeholderEnable: 'Select enabled status',
placeholderRemark: 'Enter remark',
validatorCodeRequired: 'BOM code is required',
validatorVersionRequired: 'Version is required',
validatorProductRequired: 'Product is required',
validatorUnitRequired: 'Unit is required',
validatorEnableRequired: 'Enabled status is required',
confirmDeleteContent: 'Confirm delete BOM [{code}]?',
detailTab: 'BOM Details',
detailUsageNumber: 'Usage Qty',
detailUnit: 'Unit',
detailLossRate: 'Loss Rate',
detailRemark: 'Remark',
detailEmpty: 'No BOM detail data'
},
equipmentCategory: {
moduleName: 'Equipment Category',
subTitle: 'Equipment category management',
detailTitle: 'Category Detail',
basicInfo: 'Basic Info',
code: 'Category Code',
name: 'Category Name',
parentName: 'Parent Category',
rootCategory: 'None (Top Level)',
sort: 'Sort',
remark: 'Remark',
createTime: 'Created At',
searchPlaceholder: 'Enter category code/name',
createTitle: 'Create Category',
editTitle: 'Edit Category',
empty: 'No category data',
loadEditFailed: 'Failed to load edit data',
noId: 'Missing category ID',
loadFailed: 'Failed to load category detail',
placeholderCode: 'Enter category code',
placeholderName: 'Enter category name',
placeholderParent: 'Select parent category',
placeholderSort: 'Enter sort order',
placeholderRemark: 'Enter remark',
validatorCodeRequired: 'Category code is required',
validatorNameRequired: 'Category name is required',
confirmDeleteContent: 'Confirm delete category [{name}]?'
},
equipmentLedger: {
moduleName: 'Equipment Ledger',
subTitle: 'Equipment ledger management',
detailTitle: 'Equipment Detail',
basicInfo: 'Basic Info',
deviceCode: 'Device Code',
deviceName: 'Device Name',
deviceType: 'Device Type',
deviceStatus: 'Device Status',
deviceSpec: 'Device Spec',
isScheduled: 'Scheduled',
ratedCapacity: 'Rated Capacity',
dailyAverageValue: 'Daily Avg Value',
dataCollectionCapacity: 'Data Collection Capacity',
productionDate: 'Production Date',
factoryEntryDate: 'Factory Entry Date',
deviceLocation: 'Device Location',
deviceManagerName: 'Device Manager',
workshop: 'Workshop',
remark: 'Remark',
creatorName: 'Creator',
createTime: 'Created At',
autoCode: 'Auto Generate',
yes: 'Yes',
no: 'No',
searchPlaceholder: 'Enter device code/name',
createTitle: 'Create Equipment',
editTitle: 'Edit Equipment',
empty: 'No equipment data',
loadEditFailed: 'Failed to load edit data',
noId: 'Missing equipment ID',
loadFailed: 'Failed to load equipment detail',
placeholderDeviceCode: 'Enter device code',
placeholderDeviceName: 'Enter device name',
placeholderDeviceType: 'Select device type',
placeholderDeviceSpec: 'Enter device spec',
placeholderRatedCapacity: 'Enter rated capacity',
placeholderDailyAverageValue: 'Enter daily average value',
placeholderDataCollectionCapacity: 'Enter data collection capacity',
placeholderProductionDate: 'Select production date',
placeholderFactoryEntryDate: 'Select factory entry date',
placeholderDeviceLocation: 'Enter device location',
placeholderRemark: 'Enter remark',
validatorDeviceCodeRequired: 'Device code is required',
validatorDeviceNameRequired: 'Device name is required',
validatorDeviceTypeRequired: 'Device type is required',
validatorProductionDateRequired: 'Production date is required',
validatorFactoryEntryDateRequired: 'Factory entry date is required',
validatorRatedCapacityRequired: 'Rated capacity is required',
validatorDailyAverageValueRequired: 'Daily average value is required',
validatorDataCollectionCapacityRequired: 'Data collection capacity is required',
confirmDeleteContent: 'Confirm delete equipment [{name}]?',
checkHistory: 'Inspection',
maintainHistory: 'Maintenance',
repairHistory: 'Repair',
operator: 'Operator',
noHistoryData: 'No history data',
resultPass: 'Pass',
resultFail: 'Fail',
repairPending: 'Pending',
repairProcessing: 'Processing',
repairCompleted: 'Completed'
},
criticalComponent: {
moduleName: 'Critical Component',
subTitle: 'Equipment critical component management',
detailTitle: 'Component Detail',
basicInfo: 'Basic Info',
code: 'Component Code',
name: 'Component Name',
deviceSpec: 'Specification',
description: 'Description',
count: 'Count',
remark: 'Remark',
creatorName: 'Creator',
createTime: 'Created At',
autoCode: 'Auto Generate',
searchPlaceholder: 'Enter code/name',
createTitle: 'Create Component',
editTitle: 'Edit Component',
empty: 'No component data',
loadEditFailed: 'Failed to load edit data',
noId: 'Missing component ID',
loadFailed: 'Failed to load component detail',
placeholderCode: 'Enter component code',
placeholderName: 'Enter component name',
placeholderDeviceSpec: 'Enter specification',
placeholderDescription: 'Enter description',
placeholderCount: 'Enter count',
placeholderRemark: 'Enter remark',
validatorCodeRequired: 'Component code is required',
validatorNameRequired: 'Component name is required',
confirmDeleteContent: 'Confirm delete component [{name}]?'
}
}

@ -0,0 +1,123 @@
import { createI18n } from 'vue-i18n'
import zhCN from './zh-CN'
import enUS from './en-US'
const LOCALE_STORAGE_KEY = 'app_locale'
const LOCALE_CHANGE_EVENT = 'app-locale-changed'
const DEFAULT_LOCALE = 'zh-CN'
const messages = {
'zh-CN': zhCN,
'en-US': enUS
}
function normalizeLocale(locale) {
if (locale === 'zh' || locale === 'zh_CN' || locale === 'zh-Hans') return 'zh-CN'
if (locale === 'en' || locale === 'en_US') return 'en-US'
return locale === 'en-US' ? 'en-US' : 'zh-CN'
}
function getSavedLocale() {
try {
const locale = uni.getStorageSync(LOCALE_STORAGE_KEY)
if (locale) {
return normalizeLocale(locale)
}
} catch (error) {
}
const systemLocale = typeof uni.getLocale === 'function' ? uni.getLocale() : DEFAULT_LOCALE
return normalizeLocale(systemLocale)
}
const i18n = createI18n({
legacy: false,
locale: getSavedLocale(),
fallbackLocale: DEFAULT_LOCALE,
globalInjection: true,
messages,
missingWarn: false,
fallbackWarn: false,
missing: (locale, key) => key
})
const literalMap = {
'首页': 'nav.home',
'报表': 'tab.report',
'管理': 'tab.work',
'我的': 'tab.mine',
'返回': 'dashboard.back',
'查询': 'functionCommon.search',
'取消': 'functionCommon.cancel',
'保存': 'functionCommon.save',
'加载中...': 'functionCommon.loading',
'正在加载更多...': 'functionCommon.loadingMore',
'没有更多数据了': 'functionCommon.noMoreData',
'缺少ID无法查看详情': 'functionCommon.noIdView',
'缺少ID无法编辑': 'functionCommon.noIdEdit',
'缺少ID无法删除': 'functionCommon.noIdDelete',
'加载失败': 'functionCommon.loadFailed',
'删除成功': 'functionCommon.deleteSuccess',
'删除失败': 'functionCommon.deleteFailed',
'保存失败': 'functionCommon.saveFailed',
'新增成功': 'functionCommon.createSuccess',
'更新成功': 'functionCommon.updateSuccess',
'确认删除': 'functionCommon.confirmDelete',
'上传中': 'functionCommon.uploading',
'图片上传失败': 'functionCommon.uploadImageFailed',
'是': 'functionCommon.yes',
'否': 'functionCommon.no',
'暂无待办任务': 'dashboard.noTodo',
'模具出库': 'moldGet.moduleName',
'模具出库详情': 'moldGet.detailTitle',
'模具入库': 'moldReturn.moduleName',
'模具入库详情': 'moldReturn.detailTitle',
'上下模': 'moldOperate.moduleName',
'上下模详情': 'moldOperate.detailTitle',
'点检项库': 'moldInspectionItems.moduleName',
'点检项库详情': 'moldInspectionItems.detailTitle',
'点检模板': 'moldInspectionPlan.moduleName',
'点检模板详情': 'moldInspectionPlan.detailTitle',
'点检任务': 'moldTaskConfig.moduleName',
'点检任务详情': 'moldTaskConfig.detailTitle',
'点检记录': 'moldWorkOrder.moduleName',
'点检记录详情': 'moldWorkOrder.detailTitle'
}
export function getCurrentLocale() {
return i18n.global.locale.value
}
export function setLocale(locale) {
const nextLocale = normalizeLocale(locale)
i18n.global.locale.value = nextLocale
uni.setStorageSync(LOCALE_STORAGE_KEY, nextLocale)
uni.$emit(LOCALE_CHANGE_EVENT, nextLocale)
return nextLocale
}
export function translateLiteral(text) {
if (typeof text !== 'string') return text
const key = literalMap[text]
if (key) return i18n.global.t(key)
return text
}
export function initializeLocale() {
setLocale(getSavedLocale())
}
export function setNavigationTitle(key) {
uni.setNavigationBarTitle({
title: i18n.global.t(key)
})
}
export function onLocaleChange(callback) {
uni.$on(LOCALE_CHANGE_EVENT, callback)
}
export function offLocaleChange(callback) {
uni.$off(LOCALE_CHANGE_EVENT, callback)
}
export default i18n

@ -0,0 +1,823 @@
export default {
common: {
submit: '提交',
close: '关闭',
exit: '退出',
notice: '通知',
moduleBuilding: '模块建设中~',
updateSuccess: '修改成功',
confirmLogout: '确定注销并退出系统吗',
languageSwitched: '语言已切换',
more: '更多',
reset: '重置',
complete: '完成',
edit: '编辑',
saveFailed: '保存失败'
},
tab: {
home: '首页',
report: '报表',
work: '管理',
mine: '我的'
},
nav: {
home: '首页',
mine: '个人中心',
avatar: '修改头像',
info: '个人信息',
editInfo: '编辑资料',
pwd: '修改密码',
setting: '应用设置',
help: '常见问题',
about: '关于我们'
},
dashboard: {
welcome: '欢迎您使用',
subtitle: '必硕数字化智能中控平台',
functionNav: '功能导航',
editNavMenu: '编辑快捷开关',
configuredNav: '已添加',
unconfiguredNav: '添加更多',
dragHint: '拖动图标进行排序,上方图标将显示在控制中心',
clickHint: '点击图标添加到已配置或取消配置',
allNavMenu: '全部功能',
productionOverview: '生产整体概况',
qualityOverview: '质量概况',
productionPlan: '生产概括',
production: '生产',
quality: '质量',
totalCount: '总数',
passCount: '合格数',
failCount: '不合格数',
qualityTrend: '质量趋势',
chartPlaceholder: '图表加载中...',
totalWangongNumber: '报工总数',
totalPassNumber: '合格总数',
totalNoPassNumber: '不合格总数',
totalPassRate: '总合格率',
productPassRateRanking: '产品合格率排行',
passNumber: '合格数',
noPassNumber: '不合格数',
periodLastWeek: '上周',
periodThisWeek: '本周',
periodLast7Days: '近7日',
periodLastMonth: '上月',
periodThisMonth: '本月',
periodLastYear: '近一年',
collapseList: '收起列表',
viewMore: '查看更多 ',
productName: '产品名称',
pipeline: '生产线',
planNumber: '计划数量',
planStart: '计划开始',
planEnd: '计划结束',
back: '返回',
todoTitle: '待办任务',
noTodo: '暂无待办任务',
taskCode: '任务编号:{value}',
taskType: '任务类型:{value}',
taskTarget: '目标:{value}',
createTime: '创建时间:{value}',
all: '总数',
pending: '未开工',
running: '生产中',
finished: '已完成',
mold: '模具',
equipment: '设备',
keypart: '关键件',
spare: '备件',
product: '产品物料',
statusScheduled: '已排产',
statusTrial: '试产',
statusMass: '量产',
statusPause: '暂停',
statusWaitStockIn: '待入库',
viewPlan: '查看计划: {code}',
filterTask: '任务',
filterProduct: '产品',
dateRange: '日期范围',
startDate: '开始时间',
endDate: '结束时间',
baogongNum: '报工数',
passNum: '合格数',
noPassNum: '不合格数',
passRate: '合格率',
trendChart: '趋势图',
taskPlaceholder: '任务模式开发中',
totalTask: '任务总数',
issuedNum: '下达数',
partialScheduledNum: '部分排产数',
waitingProduction: '待生产',
producing: '生产中',
completed: '已完成',
taskTrend: '任务趋势',
rangeYear: '近一年',
rangeMonth: '本月',
rangeWeek: '本周',
rangeToday: '今日',
rangeCustom: '自定义',
monday: '周一',
tuesday: '周二',
wednesday: '周三',
thursday: '周四',
friday: '周五',
saturday: '周六',
sunday: '周日'
},
deviceOverview: {
title: '设备概括',
totalDevices: '设备总数',
runningCount: '运行',
standbyCount: '待机',
faultCount: '故障',
offlineCount: '离线',
utilizationRate: '利用率',
bootRate: '开机率',
faultRate: '故障率',
rateTrend: '稼动率/开机率趋势',
onlyScheduled: '只统计排产设备',
skipHoliday: '跳过节假日',
periodLastWeek: '上周',
periodThisWeek: '本周',
periodLast7Days: '近7日',
periodLastMonth: '上月',
periodThisMonth: '本月',
periodLastYear: '近一年',
utilizationRanking: '近7日平均稼动率排名',
utilizationRateTrend: '稼动率',
deviceRateTrend: '单设备近7日稼动率/开机率趋势',
selectDevice: '选择设备',
selectDeviceHint: '请选择设备查看趋势'
},
taskList: {
filter: '筛选条件',
code: '任务单编码',
status: '状态',
taskType: '任务类型',
orderDate: '下单日期',
deliveryDate: '交付日期',
remark: '备注',
createTime: '创建时间',
totalNumber: '总数量',
planNumber: '已排产',
unPlanNumber: '未排产',
storedPlanNumber: '已入库',
urgent: '紧急',
noData: '暂无任务数据',
viewTask: '查看任务: {code}'
},
planList: {
taskCode: '任务编号',
planCode: '计划编号',
employeeName: '报工人',
baogongNum: '报工数',
passNum: '合格数',
noPassNum: '不合格数',
passRate: '合格率',
baogongTime: '报工时间',
reason: '原因',
noData: '暂无报工数据',
viewDetail: '查看详情: {code}'
},
functionCommon: {
search: '查询',
cancel: '取消',
save: '保存',
loading: '加载中...',
loadingMore: '正在加载更多...',
noMoreData: '没有更多数据了',
noIdView: '缺少ID无法查看详情',
noIdEdit: '缺少ID无法编辑',
noIdDelete: '缺少ID无法删除',
loadFailed: '加载失败',
deleteSuccess: '删除成功',
deleteFailed: '删除失败',
saveFailed: '保存失败',
createSuccess: '新增成功',
updateSuccess: '更新成功',
confirmDelete: '确认删除',
uploading: '上传中',
uploadImageFailed: '图片上传失败',
yes: '是',
no: '否',
all: '全部',
noMore: '没有更多数据了',
confirmTitle: '提示'
},
moldGet: {
moduleName: '模具出库',
subTitle: '按出库单号与状态快速筛选',
detailTitle: '模具出库详情',
basicInfo: '基础信息',
outNo: '出库单号',
outType: '出库类型',
outTime: '出库时间',
outTimeSingle: '出库日期',
outTimePlaceholder: '请选择出库日期',
warehouse: '仓库',
allWarehouse: '全部仓库',
warehousePlaceholder: '请选择仓库',
creator: '创建人',
status: '状态',
allStatus: '全部状态',
remark: '备注',
itemRemark: '明细备注',
attachment: '附件',
fileUrlPlaceholder: '请输入附件地址',
remarkPlaceholder: '请输入备注',
moldName: '模具',
moldCode: '模具编码',
moldStatus: '模具状态',
moldUseTime: '使用次数',
machineName: '使用设备',
isEnable: '是否启用',
createTime: '创建时间',
searchNo: '请输入出库单号',
searchMold: '请输入模具编码或名称',
searchCode: '请输入模具编码',
searchName: '请输入模具名称',
itemListTitle: '出库明细',
selectMold: '选择模具',
noItems: '暂无出库明细',
count: '数量',
noAuto: '系统自动生成',
createTitle: '新增模具出库',
editTitle: '编辑模具出库',
edit: '编辑',
delete: '删除',
approve: '审批',
empty: '暂无模具出库数据',
noMoldData: '暂无可选模具',
loadEditFailed: '加载编辑数据失败',
validatorOutTimeRequired: '出库日期不能为空',
validatorWarehouseRequired: '仓库不能为空',
validatorItemRequired: '请至少选择一个模具',
validatorCountRequired: '数量必须大于0',
confirmDelete: '确认删除出库单 {no} 吗?',
confirmApprove: '确认审批出库单 {no} 吗?',
approveSuccess: '审批成功'
},
moldReturn: {
moduleName: '模具入库',
subTitle: '按入库单号与状态快速筛选',
detailTitle: '模具入库详情',
basicInfo: '基础信息',
inNo: '入库单号',
inType: '入库类型',
inTime: '入库时间',
inTimeSingle: '入库日期',
inTimePlaceholder: '请选择入库日期',
warehouse: '仓库',
allWarehouse: '全部仓库',
warehousePlaceholder: '请选择仓库',
creator: '创建人',
status: '状态',
allStatus: '全部状态',
remark: '备注',
itemRemark: '明细备注',
attachment: '附件',
fileUrlPlaceholder: '请输入附件地址',
remarkPlaceholder: '请输入备注',
moldName: '模具',
moldCode: '模具编码',
moldStatus: '模具状态',
moldUseTime: '使用次数',
searchNo: '请输入入库单号',
searchCode: '请输入模具编码',
searchName: '请输入模具名称',
itemListTitle: '入库明细',
selectMold: '选择模具',
noItems: '暂无入库明细',
count: '数量',
noAuto: '系统自动生成',
createTitle: '新增模具入库',
editTitle: '编辑模具入库',
approve: '审批',
empty: '暂无模具入库数据',
noMoldData: '暂无可选模具',
loadEditFailed: '加载编辑数据失败',
validatorInTimeRequired: '入库日期不能为空',
validatorWarehouseRequired: '仓库不能为空',
validatorItemRequired: '请至少选择一个模具',
validatorCountRequired: '数量必须大于0',
confirmDelete: '确认删除入库单 {no} 吗?',
confirmApprove: '确认审批入库单 {no} 吗?',
approveSuccess: '审批成功'
},
moldOperate: {
moduleName: '上下模',
subTitle: '模具上模/下模操作记录',
detailTitle: '上下模详情',
basicInfo: '基础信息',
tabUp: '上模',
tabDown: '下模',
operateType: '操作类型',
mold: '模具',
lowerMold: '下模模具',
selectMold: '选择模具',
noSelectedMold: '暂无已选模具',
allMold: '全部模具',
searchRemark: '请输入备注关键字',
searchCode: '请输入模具编码',
searchName: '请输入模具名称',
moldCode: '模具编码',
moldName: '模具名称',
device: '设备',
deviceName: '设备名称',
creatorName: '创建人',
remark: '备注',
createTime: '创建时间',
createTitle: '新增上下模',
editTitle: '编辑上下模',
empty: '暂无上下模数据',
noMoldData: '暂无可选模具',
placeholderDevice: '请选择设备',
placeholderRemark: '请输入备注',
validatorOperateTypeRequired: '操作类型不能为空',
validatorDeviceRequired: '设备不能为空',
validatorMoldRequired: '请至少选择一个上模模具',
validatorLowerMoldRequired: '请至少选择一个下模模具',
loadEditFailed: '加载编辑数据失败',
confirmDelete: '确认删除该上下模记录吗?'
},
moldInspectionItems: {
moduleName: '点检项库',
subTitle: '模具管理点检项维护',
detailTitle: '点检项库详情',
basicInfo: '基础信息',
code: '项目编码',
name: '项目名称',
inspectionMethod: '检验方式',
valueType: '值类型',
isEnable: '是否启用',
judgmentCriteria: '判定基准',
creatorName: '创建人',
createTime: '创建时间',
searchPlaceholder: '请输入编码/名称/判定基准',
createTitle: '新增点检项',
editTitle: '编辑点检项',
empty: '暂无点检项数据',
loadEditFailed: '加载编辑数据失败',
confirmDelete: '确认删除该点检项吗?',
placeholderCode: '请输入项目编码',
placeholderName: '请输入项目名称',
placeholderInspectionMethod: '请选择检验方式',
placeholderValueType: '请选择值类型',
placeholderIsEnable: '请选择是否启用',
placeholderJudgmentCriteria: '请输入判定基准',
validatorSubjectCodeRequired: '项目编码不能为空',
validatorSubjectNameRequired: '项目名称不能为空',
validatorInspectionMethodRequired: '检验方式不能为空',
validatorValueTypeRequired: '值类型不能为空',
validatorIsEnableRequired: '是否启用不能为空',
validatorJudgmentCriteriaRequired: '判定基准不能为空'
},
moldInspectionPlan: {
moduleName: '点检模板',
subTitle: '模具管理点检模板维护',
detailTitle: '点检模板详情',
basicInfo: '基础信息',
planName: '模板名称',
planType: '模板类型',
planTypeMaintain: '保养',
planTypeInspect: '点检',
description: '描述',
subjectName: '点检项',
creatorName: '创建人',
createTime: '创建时间',
updateTime: '更新时间',
searchPlaceholder: '请输入模板名称',
createTitle: '新增点检模板',
editTitle: '编辑点检模板',
empty: '暂无点检模板数据',
loadEditFailed: '加载编辑数据失败',
confirmDelete: '确认删除该点检模板吗?',
placeholderPlanName: '请输入模板名称',
placeholderPlanType: '请选择模板类型',
placeholderDescription: '请输入描述',
placeholderSubjectSelect: '请选择点检项',
subjectSelectTitle: '选择点检项',
noSubjectData: '暂无点检项数据',
validatorPlanNameRequired: '模板名称不能为空',
validatorPlanTypeRequired: '模板类型不能为空',
subjectListTitle: '关联点检项',
subjectCode: '项目编码',
inspectionMethod: '检验方式',
judgmentCriteria: '判定基准'
},
moldTaskConfig: {
moduleName: '点检任务',
subTitle: '模具管理点检任务维护',
detailTitle: '点检任务详情',
basicInfo: '基础信息',
name: '任务名称',
taskType: '任务类型',
taskTypeInspect: '点检',
taskTypeMaintain: '保养',
moldList: '模具列表',
projectForm: '点检模板',
projectFormName: '项目方案',
dateRange: '有效日期',
startDate: '开始日期',
endDate: '结束日期',
cronExpression: 'Cron表达式',
operableUsers: '可操作用户',
enabled: '是否启用',
creatorName: '创建人',
createTime: '创建时间',
updateTime: '更新时间',
searchPlaceholder: '请输入任务名称',
createTitle: '新增点检任务',
editTitle: '编辑点检任务',
empty: '暂无点检任务数据',
loadEditFailed: '加载编辑数据失败',
confirmDeleteContent: '确认删除该点检任务【{name}】吗?',
placeholderName: '请输入任务名称',
placeholderTaskType: '请选择任务类型',
placeholderMoldList: '请选择模具',
placeholderProjectForm: '请选择点检模板',
placeholderStartDate: '选择开始日期',
placeholderEndDate: '选择结束日期',
placeholderCron: '请输入Cron表达式',
placeholderOperableUsers: '请选择可操作用户',
validatorNameRequired: '任务名称不能为空',
validatorTaskTypeRequired: '任务类型不能为空',
validatorMoldListRequired: '模具列表不能为空',
validatorProjectFormRequired: '点检模板不能为空',
validatorDateRangeRequired: '有效日期不能为空',
createTicketSuccess: '工单创建成功',
createTicketFail: '工单创建失败',
moldSelectTitle: '选择模具',
planSelectTitle: '选择点检模板',
userSelectTitle: '选择可操作用户'
},
moldWorkOrder: {
moduleName: '点检记录',
subTitle: '模具管理点检记录查询',
detailTitle: '点检记录详情',
basicInfo: '基础信息',
resultListTitle: '检验结果',
planNo: '工单编号',
moldName: '模具名称',
planType: '任务类型',
planTypeInspect: '点检',
planTypeMaintain: '保养',
configName: '任务配置',
jobStatus: '作业状态',
jobStatusPending: '待处理',
jobStatusProcessing: '处理中',
jobStatusCompleted: '已完成',
jobStatusTimeout: '已超时',
jobStatusCancelled: '已取消',
jobResult: '作业结果',
jobResultOk: 'OK',
jobResultNg: 'NG',
operatorName: '操作人',
taskTime: '任务时间',
taskEndTime: '结束时间',
cancelReason: '取消原因',
createTime: '创建时间',
searchPlaceholder: '请输入工单编号',
empty: '暂无点检记录数据',
cancelTask: '取消任务',
cancelSuccess: '取消成功',
cancelFail: '取消失败',
placeholderCancelReason: '请输入取消原因',
validatorCancelReasonRequired: '取消原因不能为空',
inspectionItemName: '检验项名称',
inspectionMethod: '检验方式',
judgmentCriteria: '判定基准',
valueType: '值类型',
inspectionResult: '检验结果',
inspectionResultPending: '待检',
inspectionResultPass: '合格',
inspectionResultFail: '不合格',
textInput: '输入值',
remark: '备注',
images: '图片',
noResultData: '暂无检验结果数据',
loadMore: '加载更多'
},
mine: {
clickLogin: '点击登录',
username: '用户名:{name}',
profile: '个人信息',
feedback: '反馈中心',
service: '在线客服',
changePassword: '修改密码',
logout: '退出登录',
editProfile: '编辑资料',
faq: '常见问题',
about: '关于我们',
appSettings: '应用设置'
},
setting: {
language: '系统语言',
currentLanguage: '当前语言:{language}',
switchLanguage: '切换语言',
checkUpdate: '检查更新',
cleanCache: '清理缓存',
logout: '退出登录',
zhCN: '中文',
enUS: '英文'
},
about: {
appName: '必硕生管系统',
version: '版本信息',
email: '官方邮箱',
hotline: '服务热线',
website: '公司网站'
},
help: {
appUserQuestion: 'APP用户问题',
otherQuestion: '其他问题',
appFeatureQuestion: 'APP支持的功能业务有哪些',
appFeatureAnswer: '计划开工、生产报工、投料记录、生产记录',
reportQuestion: '生产报工如何报工?',
reportAnswer: '可以',
planQuestion: '计划管理如何进行?',
planAnswer: '计划管理',
materialQuestion: '投料记录如何进行?',
materialAnswer: '投料记录',
logoutQuestion: '如何退出登录?',
logoutAnswer: '请点击[我的] - [应用设置] - [退出登录]即可退出登录',
avatarQuestion: '如何修改用户头像?',
avatarAnswer: '请点击[我的] - [选择头像] - [点击提交]即可更换用户头像',
passwordQuestion: '如何修改登录密码?',
passwordAnswer: '请点击[我的] - [应用设置] - [修改密码]即可修改登录密码'
},
info: {
username: '用户名称',
nickname: '昵称',
gender: '性别',
male: '男',
female: '女',
phone: '手机号码',
email: '邮箱',
createdAt: '创建日期'
},
editInfo: {
nickname: '用户昵称',
nicknamePlaceholder: '请输入昵称',
phone: '手机号码',
phonePlaceholder: '请输入手机号码',
email: '邮箱',
emailPlaceholder: '请输入邮箱',
gender: '性别',
nicknameRequired: '用户昵称不能为空',
phoneRequired: '手机号码不能为空',
phoneInvalid: '请输入正确的手机号码',
emailRequired: '邮箱地址不能为空',
emailInvalid: '请输入正确的邮箱地址'
},
pwd: {
oldPassword: '旧密码',
newPassword: '新密码',
confirmPassword: '确认密码',
oldPasswordPlaceholder: '请输入旧密码',
newPasswordPlaceholder: '请输入新密码',
confirmPasswordPlaceholder: '请确认新密码',
oldPasswordRequired: '旧密码不能为空',
newPasswordRequired: '新密码不能为空',
passwordLength: '长度在 6 到 20 个字符',
confirmPasswordRequired: '确认密码不能为空',
passwordNotMatch: '两次输入的密码不一致'
},
avatar: {
chooseAvatar: '选择头像'
},
materialCategory: {
moduleName: '产品物料分类',
subTitle: '产品物料分类管理',
detailTitle: '分类详情',
basicInfo: '基础信息',
code: '分类编码',
name: '分类名称',
parentName: '上级分类',
rootCategory: '无(顶级分类)',
sort: '排序',
status: '状态',
statusEnable: '启用',
statusDisable: '禁用',
createTime: '创建时间',
searchPlaceholder: '请输入分类编码/名称',
createTitle: '新增分类',
editTitle: '编辑分类',
empty: '暂无分类数据',
loadEditFailed: '加载编辑数据失败',
noId: '缺少分类ID',
loadFailed: '加载分类详情失败',
placeholderCode: '请输入分类编码',
placeholderName: '请输入分类名称',
placeholderParent: '请选择上级分类',
placeholderSort: '请输入排序',
placeholderStatus: '请选择状态',
validatorCodeRequired: '分类编码不能为空',
validatorNameRequired: '分类名称不能为空',
validatorSortRequired: '排序不能为空',
validatorStatusRequired: '状态不能为空',
confirmDeleteContent: '确认删除分类【{name}】吗?'
},
materialInfo: {
moduleName: '产品物料信息',
subTitle: '产品物料信息管理',
detailTitle: '物料详情',
basicInfo: '基础信息',
barCode: '物料条码',
name: '物料名称',
category: '物料分类',
unit: '单位',
standard: '规格',
expiryDay: '保质期天数',
status: '状态',
statusEnable: '启用',
statusDisable: '禁用',
remark: '备注',
createTime: '创建时间',
autoCode: '自动生成',
searchPlaceholder: '请输入物料名称/条码',
createTitle: '新增物料',
editTitle: '编辑物料',
empty: '暂无物料数据',
loadEditFailed: '加载编辑数据失败',
noId: '缺少物料ID',
loadFailed: '加载物料详情失败',
placeholderBarCode: '请输入物料条码',
placeholderName: '请输入物料名称',
placeholderCategory: '请选择物料分类',
placeholderUnit: '请选择单位',
placeholderStandard: '请输入规格',
placeholderExpiryDay: '请输入保质期天数',
placeholderStatus: '请选择状态',
placeholderRemark: '请输入备注',
validatorBarCodeRequired: '物料条码不能为空',
validatorNameRequired: '物料名称不能为空',
validatorCategoryRequired: '物料分类不能为空',
validatorUnitRequired: '单位不能为空',
confirmDeleteContent: '确认删除物料【{name}】吗?'
},
productBom: {
moduleName: '产品BOM',
subTitle: '产品BOM管理',
detailTitle: 'BOM详情',
basicInfo: '基础信息',
code: 'BOM编码',
version: '版本',
product: '产品',
unit: '单位',
yieldRate: '良品率',
isEnable: '是否启用',
enableYes: '是',
enableNo: '否',
remark: '备注',
createTime: '创建时间',
searchPlaceholder: '请输入BOM编码',
createTitle: '新增BOM',
editTitle: '编辑BOM',
empty: '暂无BOM数据',
loadEditFailed: '加载编辑数据失败',
noId: '缺少BOM ID',
loadFailed: '加载BOM详情失败',
placeholderCode: '请输入BOM编码',
placeholderVersion: '请输入版本',
placeholderProduct: '请选择产品',
placeholderUnit: '请选择单位',
placeholderYieldRate: '请输入良品率',
placeholderEnable: '请选择是否启用',
placeholderRemark: '请输入备注',
validatorCodeRequired: 'BOM编码不能为空',
validatorVersionRequired: '版本不能为空',
validatorProductRequired: '产品不能为空',
validatorUnitRequired: '单位不能为空',
validatorEnableRequired: '是否启用不能为空',
confirmDeleteContent: '确认删除BOM【{code}】吗?',
detailTab: 'BOM明细',
detailUsageNumber: '用量',
detailUnit: '单位',
detailLossRate: '损耗率',
detailRemark: '备注',
detailEmpty: '暂无BOM明细数据'
},
equipmentCategory: {
moduleName: '设备分类',
subTitle: '设备分类管理',
detailTitle: '设备分类详情',
basicInfo: '基础信息',
code: '分类编码',
name: '分类名称',
parentName: '上级分类',
rootCategory: '无(顶级分类)',
sort: '排序',
remark: '备注',
createTime: '创建时间',
searchPlaceholder: '请输入分类编码/名称',
createTitle: '新增设备分类',
editTitle: '编辑设备分类',
empty: '暂无设备分类数据',
loadEditFailed: '加载编辑数据失败',
noId: '缺少分类ID',
loadFailed: '加载分类详情失败',
placeholderCode: '请输入分类编码',
placeholderName: '请输入分类名称',
placeholderParent: '请选择上级分类',
placeholderSort: '请输入排序',
placeholderRemark: '请输入备注',
validatorCodeRequired: '分类编码不能为空',
validatorNameRequired: '分类名称不能为空',
confirmDeleteContent: '确认删除分类【{name}】吗?'
},
equipmentLedger: {
moduleName: '设备台账',
subTitle: '设备台账管理',
detailTitle: '设备台账详情',
basicInfo: '基础信息',
deviceCode: '设备编码',
deviceName: '设备名称',
deviceType: '设备类型',
deviceStatus: '设备状态',
deviceSpec: '设备规格',
isScheduled: '是否排产',
ratedCapacity: '额定产能',
dailyAverageValue: '每日报工平均值',
dataCollectionCapacity: '数据采集产能',
productionDate: '生产日期',
factoryEntryDate: '入厂日期',
deviceLocation: '设备位置',
deviceManagerName: '设备负责人',
workshop: '所属车间',
remark: '备注',
creatorName: '创建人',
createTime: '创建时间',
autoCode: '自动生成',
yes: '是',
no: '否',
searchPlaceholder: '请输入设备编码/名称',
createTitle: '新增设备台账',
editTitle: '编辑设备台账',
empty: '暂无设备台账数据',
loadEditFailed: '加载编辑数据失败',
noId: '缺少设备ID',
loadFailed: '加载设备详情失败',
placeholderDeviceCode: '请输入设备编码',
placeholderDeviceName: '请输入设备名称',
placeholderDeviceType: '请选择设备类型',
placeholderDeviceSpec: '请输入设备规格',
placeholderRatedCapacity: '请输入额定产能',
placeholderDailyAverageValue: '请输入每日报工平均值',
placeholderDataCollectionCapacity: '请输入数据采集产能',
placeholderProductionDate: '请选择生产日期',
placeholderFactoryEntryDate: '请选择入厂日期',
placeholderDeviceLocation: '请输入设备位置',
placeholderRemark: '请输入备注',
validatorDeviceCodeRequired: '设备编码不能为空',
validatorDeviceNameRequired: '设备名称不能为空',
validatorDeviceTypeRequired: '设备类型不能为空',
validatorProductionDateRequired: '生产日期不能为空',
validatorFactoryEntryDateRequired: '入厂日期不能为空',
validatorRatedCapacityRequired: '额定产能不能为空',
validatorDailyAverageValueRequired: '每日报工平均值不能为空',
validatorDataCollectionCapacityRequired: '数据采集产能不能为空',
confirmDeleteContent: '确认删除设备【{name}】吗?',
checkHistory: '点检记录',
maintainHistory: '保养记录',
repairHistory: '报修记录',
operator: '操作人',
noHistoryData: '暂无历史记录',
resultPass: '合格',
resultFail: '不合格',
repairPending: '待处理',
repairProcessing: '处理中',
repairCompleted: '已完成'
},
criticalComponent: {
moduleName: '设备关键件',
subTitle: '设备关键件管理',
detailTitle: '设备关键件详情',
basicInfo: '基础信息',
code: '关键件编码',
name: '关键件名称',
deviceSpec: '规格',
description: '描述',
count: '数量',
remark: '备注',
creatorName: '创建人',
createTime: '创建时间',
autoCode: '自动生成',
searchPlaceholder: '请输入编码/名称',
createTitle: '新增设备关键件',
editTitle: '编辑设备关键件',
empty: '暂无设备关键件数据',
loadEditFailed: '加载编辑数据失败',
noId: '缺少关键件ID',
loadFailed: '加载关键件详情失败',
placeholderCode: '请输入关键件编码',
placeholderName: '请输入关键件名称',
placeholderDeviceSpec: '请输入规格',
placeholderDescription: '请输入描述',
placeholderCount: '请输入数量',
placeholderRemark: '请输入备注',
validatorCodeRequired: '关键件编码不能为空',
validatorNameRequired: '关键件名称不能为空',
confirmDeleteContent: '确认删除关键件【{name}】吗?'
}
}

@ -2,29 +2,22 @@ import App from './App.vue'
import plugins from './plugins'
import store from './store'
import uviewPlus from 'uview-plus'
import ElementPlus from 'element-plus' //添加
import 'element-plus/dist/index.css' //添加
import locale from "element-plus/es/locale/lang/zh-cn"
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import i18n from '@/locales'
import { createSSRApp } from 'vue'
import directive from './directive' // directive
import { translateLiteral } from '@/locales'
import { useDict } from '@/utils/dict'
import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
export function createApp() {
const app = createSSRApp(App)
app.use(store)
app.use(i18n)
app.use(uviewPlus)
app.use(plugins)
app.use(ElementPlus, {locale})
for (const [key, component] of Object.entries(ElementPlusIconsVue)){
app.component(key, component)
}
directive(app)
// 全局方法挂载
@ -35,6 +28,7 @@ export function createApp() {
app.config.globalProperties.addDateRange = addDateRange
app.config.globalProperties.selectDictLabel = selectDictLabel
app.config.globalProperties.selectDictLabels = selectDictLabels
app.config.globalProperties.$tl = translateLiteral
return {
app

@ -1,6 +1,6 @@
{
"name" : "必硕生管系统",
"appid" : "__UNI__AED03F6",
"appid" : "__UNI__333F1FF",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
@ -9,8 +9,8 @@
"app-plus" : {
"compatible" : {
"ignoreVersion" : true,
"runtimeVersion" : "4.15", //
"compilerVersion" : "4.15" //
"runtimeVersion" : "4.23", //
"compilerVersion" : "4.23" //
},
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
@ -44,7 +44,8 @@
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
],
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ]
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
"minSdkVersion" : 21
},
/* ios */
"ios" : {

@ -1,5 +1,6 @@
<template>
<view class="container">
<NavBar title="新增投料记录" />
<view class="example">
<uni-forms ref="customForm" :rules="customRules" labelWidth="60px" :modelValue="formData">
@ -52,6 +53,7 @@
</template>
<script>
import NavBar from '@/components/common/NavBar.vue'
import {create} from "@/api/mes/record"
import {getUnitList,getItemList} from "@/api/mes/product"
import { pipelineTypes,feedingTypes, findTextByValue} from "@/api/system/dict/data";
@ -59,7 +61,10 @@ import {showConfirm} from "@/utils/common";
import tab from "@/plugins/tab";
import modal from "@/plugins/modal";
export default {
components: {},
components: {
NavBar
},
name: "feedingRecordForm",
data() {
return {
//

@ -1,58 +1,60 @@
<template>
<view class="container">
<NavBar title="计划进度" />
<uni-card :is-shadow="false" is-full>
<uni-row>
<uni-col :span="10">
<text class="uni-h6" >计划:
<el-text class="mx-1" type="primary">{{planDo.code}}</el-text>
<text class="uni-h6">计划:
<text class="highlight primary">{{ (planDo && planDo.code) ? planDo.code : '-' }}</text>
</text>
</uni-col>
<uni-col :span="14">
<text class="uni-h6">产品:
<el-text class="mx-1" type="warning">{{planDo.productName}}</el-text>
<text class="highlight warning">{{ (planDo && planDo.productName) ? planDo.productName : '-' }}</text>
</text>
</uni-col>
</uni-row>
</uni-card>
<ContentWrap>
<el-table :data="progressList" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="工序" align="center" prop="orgType" >
<template #default="scope">
<text> {{findTextByValue(scope.row.orgType)}}</text>
</template>
</el-table-column>
<el-table-column label="数量" align="center" prop="totalQualityNumber" width="80px"/>
<el-table-column label="进度" align="center">
<template #default="scope">
<el-progress
:percentage="Number((scope.row.totalQualityNumber/planDo.planNumber)*100).toFixed(2)"
:stroke-width="15"
:color="finishColors"
:striped="true"
:striped-flow="true"
<view class="progress-table">
<view class="progress-row progress-header">
<text class="col col-process">工序</text>
<text class="col col-num">数量</text>
<text class="col col-progress">进度</text>
<text class="col col-waste">废品</text>
<text class="col col-rate">废品率</text>
</view>
<view v-for="(row, index) in progressList" :key="index" class="progress-row">
<text class="col col-process">{{ findTextByValue(row.orgType) || '-' }}</text>
<text class="col col-num">{{ (row && row.totalQualityNumber !== null && row.totalQualityNumber !== undefined) ? row.totalQualityNumber : 0 }}</text>
<view class="col col-progress">
<progress
:percent="calcQualityPercent(row)"
:stroke-width="12"
activeColor="#18b566"
backgroundColor="#e5e5e5"
/>
</template>
</el-table-column>
<el-table-column label="废品" align="center" prop="totalWasteNumber" width="60px"/>
<el-table-column label="废品率" align="center" >
<template #default="scope">
<span class="percentage-value">{{Number((scope.row.totalWasteNumber/(scope.row.totalQualityNumber+scope.row.totalWasteNumber))*100 ).toFixed(2)}}%</span>
</template>
</el-table-column>
</el-table>
</ContentWrap>
<text class="progress-text">{{ calcQualityPercent(row).toFixed(2) }}%</text>
</view>
<text class="col col-waste">{{ (row && row.totalWasteNumber !== null && row.totalWasteNumber !== undefined) ? row.totalWasteNumber : 0 }}</text>
<text class="col col-rate">{{ calcWastePercent(row).toFixed(2) }}%</text>
</view>
<view v-if="!progressList || progressList.length === 0" class="empty">
<text class="empty-text">暂无数据</text>
</view>
</view>
</view>
</template>
<script>
import NavBar from '@/components/common/NavBar.vue'
import {processTypes,findTextByValue} from "@/api/system/dict/data";
import {getPlanProgress,getById} from "@/api/mes/plan"
import {getPlanProgress} from "@/api/mes/plan"
import tab from "@/plugins/tab";
import modal from "@/plugins/modal";
export default {
components: {},
components: { NavBar },
data() {
return {
planDo: undefined,
@ -72,18 +74,127 @@ export default {
findTextByValue(value) {
return findTextByValue(this.processTypes, value)
},
calcQualityPercent(row) {
const planNumber = Number(this.planDo && this.planDo.planNumber ? this.planDo.planNumber : 0)
const totalQualityNumber = Number(row && row.totalQualityNumber ? row.totalQualityNumber : 0)
if (!Number.isFinite(planNumber) || planNumber <= 0) return 0
const raw = (totalQualityNumber / planNumber) * 100
const safe = Number.isFinite(raw) ? raw : 0
return Math.max(0, Math.min(100, Number(safe.toFixed(2))))
},
calcWastePercent(row) {
const waste = Number(row && row.totalWasteNumber ? row.totalWasteNumber : 0)
const quality = Number(row && row.totalQualityNumber ? row.totalQualityNumber : 0)
const denom = waste + quality
if (!Number.isFinite(denom) || denom <= 0) return 0
const raw = (waste / denom) * 100
const safe = Number.isFinite(raw) ? raw : 0
return Math.max(0, Math.min(100, Number(safe.toFixed(2))))
},
getProgressList() {
modal.loading("正在加载...")
if(this.planDo)
getPlanProgress(this.planDo.id).then(response => {
this.progressList = response.data;
if (!this.planDo || !this.planDo.id) {
modal.closeLoading()
return
}
getPlanProgress(this.planDo.id)
.then(response => {
this.progressList = (response && response.data) ? response.data : []
})
.catch(() => {
this.progressList = []
modal.msgError("获取进度失败")
})
.finally(() => {
modal.closeLoading()
})
}
}
}
</script>
<style lang="scss">
.container {
padding: 24rpx;
}
.highlight {
margin-left: 8rpx;
font-weight: 600;
}
.primary {
color: #1a73e8;
}
.warning {
color: #f59e0b;
}
.progress-table {
background: #ffffff;
border-radius: 16rpx;
overflow: hidden;
}
.progress-row {
display: flex;
align-items: center;
padding: 20rpx 16rpx;
border-bottom: 1px solid #f2f2f2;
}
.progress-header {
background: #0f172a;
}
.progress-header .col {
color: #e5e7eb;
font-weight: 600;
}
.col {
color: #111827;
font-size: 26rpx;
}
.col-process {
width: 160rpx;
}
.col-num {
width: 90rpx;
text-align: center;
}
.col-progress {
flex: 1;
padding: 0 12rpx;
}
.progress-text {
margin-top: 8rpx;
font-size: 22rpx;
color: #6b7280;
}
.col-waste {
width: 90rpx;
text-align: center;
}
.col-rate {
width: 120rpx;
text-align: right;
}
.empty {
padding: 40rpx 16rpx;
text-align: center;
}
.empty-text {
color: #6b7280;
font-size: 26rpx;
}
</style>

@ -1,5 +1,6 @@
<template>
<view class="container">
<NavBar title="代报工" />
<view class="example">
<!-- 自定义表单校验 -->
<uni-forms ref="customForm" :rules="customRules" labelWidth="60px" :modelValue="customFormData">
@ -91,6 +92,7 @@
</template>
<script>
import NavBar from '@/components/common/NavBar.vue'
import {processTypes, groupTypes} from "@/api/system/dict/data";
import {
@ -106,7 +108,9 @@ import tab from "@/plugins/tab";
import modal from "@/plugins/modal";
import {getById, getDetailByReportId, update} from "@/api/mes/report";
export default {
components: {},
components: {
NavBar
},
data() {
return {
//

@ -1,5 +1,6 @@
<template>
<view class="container">
<NavBar title="生产报工" />
<uni-card :is-shadow="false" is-full>
<text class="uni-h6">如个人无法填报请寻找主管代为报工</text>
</uni-card>
@ -91,6 +92,7 @@
</template>
<script>
import NavBar from '@/components/common/NavBar.vue'
import useUserStore from "@/store/modules/user";
import {processTypes, groupTypes} from "@/api/system/dict/data";
@ -99,10 +101,21 @@ import {getCurrentDate} from "@/utils/dateUtil"
import {showConfirm} from "@/utils/common";
import tab from "@/plugins/tab";
import modal from "@/plugins/modal";
const userStore = useUserStore()
export default {
components: {},
components: { NavBar },
data() {
let userStore = {
name: '',
userId: ''
}
try {
userStore = useUserStore()
} catch (e) {
userStore = {
name: '',
userId: ''
}
}
return {
//
customFormData: {

@ -11,51 +11,60 @@
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
"globalStyle": {
"navigationBarTitleText": "BESURE"
},
"pages": [
{
"path": "pages/index",
"style": {
"navigationBarTitleText": "生产计划",
"enablePullDownRefresh": true
"navigationBarTitleText": "首页",
"navigationStyle": "custom",
"enablePullDownRefresh": false
}
},
{
"path": "pages/login",
"style": {
"navigationBarTitleText": "登录"
"navigationBarTitleText": "登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/work",
"style": {
"navigationBarTitleText": "近三天投料记录",
"enablePullDownRefresh": true
"navigationBarTitleText": "管理",
"navigationStyle": "custom",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mine",
"style": {
"navigationBarTitleText": "个人中心"
"navigationBarTitleText": "个人中心",
"navigationStyle": "custom"
}
},
{
"path": "pages/common/webview/index",
"style": {
"navigationBarTitleText": "浏览网页"
"navigationBarTitleText": "浏览网页",
"navigationStyle": "custom"
}
},
{
"path": "pages/common/textview/index",
"style": {
"navigationBarTitleText": "浏览文本"
"navigationBarTitleText": "浏览文本",
"navigationStyle": "custom"
}
},
{
"path": "pages/report",
"style": {
"navigationBarTitleText": "生产报工",
"enablePullDownRefresh": true
"navigationBarTitleText": "报表",
"navigationStyle": "custom",
"enablePullDownRefresh": false
}
}
],
@ -66,20 +75,22 @@
{
"path": "reportForm",
"style": {
"navigationBarTitleText": "生产报工"
"navigationBarTitleText": "生产报工",
"navigationStyle": "custom"
}
},
{
"path": "replaceForm",
"style": {
"navigationBarTitleText": "代报工"
}
"navigationBarTitleText": "代报工",
"navigationStyle": "custom"
}
,
},
{
"path": "planProgress",
"style": {
"navigationBarTitleText": "计划进度"
"navigationBarTitleText": "计划进度",
"navigationStyle": "custom"
}
}
]
@ -90,7 +101,8 @@
{
"path": "feedingRecordForm",
"style": {
"navigationBarTitleText": "新增投料记录"
"navigationBarTitleText": "新增投料记录",
"navigationStyle": "custom"
}
}
]
@ -101,43 +113,50 @@
{
"path": "avatar/index",
"style": {
"navigationBarTitleText": "修改头像"
"navigationBarTitleText": "修改头像",
"navigationStyle": "custom"
}
},
{
"path": "info/index",
"style": {
"navigationBarTitleText": "个人信息"
"navigationBarTitleText": "个人信息",
"navigationStyle": "custom"
}
},
{
"path": "info/edit",
"style": {
"navigationBarTitleText": "编辑资料"
"navigationBarTitleText": "编辑资料",
"navigationStyle": "custom"
}
},
{
"path": "pwd/index",
"style": {
"navigationBarTitleText": "修改密码"
"navigationBarTitleText": "修改密码",
"navigationStyle": "custom"
}
},
{
"path": "setting/index",
"style": {
"navigationBarTitleText": "应用设置"
"navigationBarTitleText": "应用设置",
"navigationStyle": "custom"
}
},
{
"path": "help/index",
"style": {
"navigationBarTitleText": "常见问题"
"navigationBarTitleText": "常见问题",
"navigationStyle": "custom"
}
},
{
"path": "about/index",
"style": {
"navigationBarTitleText": "关于我们"
"navigationBarTitleText": "关于我们",
"navigationStyle": "custom"
}
}
]
@ -277,43 +296,334 @@
"path": "code/index"
}
]
},
{
"root": "pages_function/pages",
"pages": [
{
"path": "mold/index",
"style": {
"navigationBarTitleText": "模具查询",
"navigationStyle": "custom"
}
],
"tabBar": {
"color": "#000000",
"selectedColor": "#000000",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": [
},
{
"pagePath": "pages/index",
"iconPath": "static/images/tabbar/home.png",
"selectedIconPath": "static/images/tabbar/home_.png",
"text": "计划"
"path": "mold/detail",
"style": {
"navigationBarTitleText": "模具详情",
"navigationStyle": "custom"
}
},
{
"path": "equipment/index",
"style": {
"navigationBarTitleText": "设备查询",
"navigationStyle": "custom"
}
},
{
"pagePath": "pages/report",
"iconPath": "static/images/tabbar/work.png",
"selectedIconPath": "static/images/tabbar/work_.png",
"text": "报工"
"path": "equipment/detail",
"style": {
"navigationBarTitleText": "设备详情",
"navigationStyle": "custom"
}
},
{
"pagePath": "pages/work",
"iconPath": "static/images/tabbar/work.png",
"selectedIconPath": "static/images/tabbar/work_.png",
"text": "投料"
"path": "spare/index",
"style": {
"navigationBarTitleText": "备件查询",
"navigationStyle": "custom"
}
},
{
"pagePath": "pages/mine",
"iconPath": "static/images/tabbar/mine.png",
"selectedIconPath": "static/images/tabbar/mine_.png",
"text": "我的"
"path": "spare/detail",
"style": {
"navigationBarTitleText": "备件详情",
"navigationStyle": "custom"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "BESURE",
"navigationBarBackgroundColor": "#FFFFFF"
{
"path": "keypart/index",
"style": {
"navigationBarTitleText": "关键件查询",
"navigationStyle": "custom"
}
},
{
"path": "keypart/detail",
"style": {
"navigationBarTitleText": "关键件详情",
"navigationStyle": "custom"
}
},
{
"path": "warehouse/index",
"style": {
"navigationBarTitleText": "仓库信息",
"navigationStyle": "custom"
}
},
{
"path": "product/index",
"style": {
"navigationBarTitleText": "产品物料查询",
"navigationStyle": "custom"
}
},
{
"path": "product/detail",
"style": {
"navigationBarTitleText": "产品物料详情",
"navigationStyle": "custom"
}
},
{
"path": "inspection/index",
"style": {
"navigationBarTitleText": "检验类型",
"navigationStyle": "custom"
}
},
{
"path": "inspectionItem/index",
"style": {
"navigationBarTitleText": "检验项库",
"navigationStyle": "custom"
}
},
{
"path": "inspectionTemplate/index",
"style": {
"navigationBarTitleText": "检验模板",
"navigationStyle": "custom"
}
},
{
"path": "materialCategory/index",
"style": {
"navigationBarTitleText": "产品物料分类",
"navigationStyle": "custom"
}
},
{
"path": "materialCategory/detail",
"style": {
"navigationBarTitleText": "分类详情",
"navigationStyle": "custom"
}
},
{
"path": "materialInfo/index",
"style": {
"navigationBarTitleText": "产品物料信息",
"navigationStyle": "custom"
}
},
{
"path": "materialInfo/detail",
"style": {
"navigationBarTitleText": "物料详情",
"navigationStyle": "custom"
}
},
{
"path": "productBom/index",
"style": {
"navigationBarTitleText": "产品BOM",
"navigationStyle": "custom"
}
},
{
"path": "productBom/detail",
"style": {
"navigationBarTitleText": "BOM详情",
"navigationStyle": "custom"
}
},
{
"path": "equipmentCategory/index",
"style": {
"navigationBarTitleText": "设备分类",
"navigationStyle": "custom"
}
},
{
"path": "equipmentCategory/detail",
"style": {
"navigationBarTitleText": "设备分类详情",
"navigationStyle": "custom"
}
},
{
"path": "equipmentLedger/index",
"style": {
"navigationBarTitleText": "设备台账",
"navigationStyle": "custom"
}
},
{
"path": "equipmentLedger/detail",
"style": {
"navigationBarTitleText": "设备台账详情",
"navigationStyle": "custom"
}
},
{
"path": "criticalComponent/index",
"style": {
"navigationBarTitleText": "设备关键件",
"navigationStyle": "custom"
}
},
{
"path": "criticalComponent/detail",
"style": {
"navigationBarTitleText": "设备关键件详情",
"navigationStyle": "custom"
}
},
{
"path": "moldType/index",
"style": {
"navigationBarTitleText": "模具类型",
"navigationStyle": "custom"
}
},
{
"path": "moldType/detail",
"style": {
"navigationBarTitleText": "模具类型详情",
"navigationStyle": "custom"
}
},
{
"path": "moldLedger/index",
"style": {
"navigationBarTitleText": "模具台账",
"navigationStyle": "custom"
}
},
{
"path": "moldLedger/detail",
"style": {
"navigationBarTitleText": "模具台账详情",
"navigationStyle": "custom"
}
},
{
"path": "moldget/index",
"style": {
"navigationBarTitleText": "模具出库",
"navigationStyle": "custom"
}
},
{
"path": "moldget/detail",
"style": {
"navigationBarTitleText": "模具出库详情",
"navigationStyle": "custom"
}
},
{
"path": "moldreturn/index",
"style": {
"navigationBarTitleText": "模具入库",
"navigationStyle": "custom"
}
},
{
"path": "moldreturn/detail",
"style": {
"navigationBarTitleText": "模具入库详情",
"navigationStyle": "custom"
}
},
{
"path": "moldoperate/index",
"style": {
"navigationBarTitleText": "上下模",
"navigationStyle": "custom"
}
},
{
"path": "moldoperate/detail",
"style": {
"navigationBarTitleText": "上下模详情",
"navigationStyle": "custom"
}
},
{
"path": "moldInspectionItems/index",
"style": {
"navigationBarTitleText": "点检项库",
"navigationStyle": "custom"
}
},
{
"path": "moldInspectionItems/detail",
"style": {
"navigationBarTitleText": "点检项库详情",
"navigationStyle": "custom"
}
},
{
"path": "moldInspectionPlan/index",
"style": {
"navigationBarTitleText": "点检模板",
"navigationStyle": "custom"
}
},
{
"path": "moldInspectionPlan/detail",
"style": {
"navigationBarTitleText": "点检模板详情",
"navigationStyle": "custom"
}
},
{
"path": "moldTaskConfiguration/index",
"style": {
"navigationBarTitleText": "点检任务",
"navigationStyle": "custom"
}
},
{
"path": "moldTaskConfiguration/detail",
"style": {
"navigationBarTitleText": "点检任务详情",
"navigationStyle": "custom"
}
},
{
"path": "moldWorkOrderInquiry/index",
"style": {
"navigationBarTitleText": "点检记录",
"navigationStyle": "custom"
}
},
{
"path": "moldWorkOrderInquiry/detail",
"style": {
"navigationBarTitleText": "点检记录详情",
"navigationStyle": "custom"
}
},
{
"path": "planList/index",
"style": {
"navigationBarTitleText": "生产计划",
"navigationStyle": "custom"
}
},
{
"path": "taskList/index",
"style": {
"navigationBarTitleText": "任务计划",
"navigationStyle": "custom"
}
}
]
}
],
"preloadRule": {}
}

@ -1,5 +1,6 @@
<template>
<view>
<NavBar :title="title" />
<uni-card class="view-title" :title="title">
<text class="uni-body view-content">{{ content }}</text>
</uni-card>
@ -7,7 +8,12 @@
</template>
<script>
import NavBar from '@/components/common/NavBar.vue'
export default {
components: {
NavBar
},
data() {
return {
title: '',
@ -17,9 +23,6 @@
onLoad(options) {
this.title = options.title
this.content = options.content
uni.setNavigationBarTitle({
title: options.title
})
}
}
</script>

@ -1,11 +1,17 @@
<template>
<view v-if="params.url">
<web-view :webview-styles="webviewStyles" :src="`${params.url}`"></web-view>
<view>
<NavBar v-if="params.title" :title="params.title" />
<web-view v-if="params.url" :webview-styles="webviewStyles" :src="`${params.url}`"></web-view>
</view>
</template>
<script>
import NavBar from '@/components/common/NavBar.vue'
export default {
components: {
NavBar
},
data() {
return {
params: {},
@ -24,11 +30,6 @@
},
onLoad(event) {
this.params = event
if (event.title) {
uni.setNavigationBarTitle({
title: event.title
})
}
}
}
</script>

@ -1,232 +1,141 @@
<template>
<view >
<uni-notice-bar show-icon scrollable text="安全生产!有序生产!高效生产!" />
<uni-section title="开工中" type="line" title-color="#18bc37"> </uni-section>
<el-collapse accordion>
<el-collapse-item v-for="(item, index) in startList" :name="item.id">
<template #title>
<el-icon><Tickets /></el-icon>
<el-text type="success">{{item.code}}</el-text>-
<el-text size="large">{{item.productName}}</el-text>
</template>
<el-row>
<el-col :span="12">
<el-text type="success">计划开始:{{ timestampToTime(item.planStartTime) }}</el-text>
</el-col>
<el-col :span="8">
<el-text type="success">计划数:{{ item.planNumber }}</el-text>
</el-col>
<el-col v-if="auth.hasPermi('mes:plan:update')" :span="4">
<el-button type="success" plain @click="updatePlan(item.id, item.code,'end')">完工</el-button>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-text type="warning">计划结束:{{ timestampToTime(item.planEndTime) }}</el-text>
</el-col>
<el-col :span="8">
<el-text type="warning">入库数:{{ item.finishNumber }}</el-text>
</el-col>
<el-col v-if="auth.hasPermi('mes:plan:update')" :span="4">
<el-button type="warning" plain @click="updatePlan(item.id,item.code,'pause')"></el-button>
</el-col>
</el-row>
<el-row>
<el-col :span="20">
<el-text>备注:{{ item.remark }}</el-text>
</el-col>
<el-col :span="4">
<el-button type="info" plain @click="planProgress(item)"></el-button>
</el-col>
</el-row>
</el-collapse-item>
</el-collapse>
<!-- 派工中-->
<uni-section title="派工中" type="line" title-color="#2979ff"></uni-section>
<el-collapse accordion>
<el-collapse-item v-for="(item, index) in paigongList" :name="item.id">
<template #title>
<el-icon><Tickets /></el-icon>
<el-text type="success">{{item.code}}</el-text>-
<el-text size="large">{{item.productName}}</el-text>
</template>
<el-row>
<el-col :span="12">
<el-text type="success">计划开始:{{ timestampToTime(item.planStartTime) }}</el-text>
</el-col>
<el-col :span="8">
<el-text type="success">计划数:{{ item.planNumber }}</el-text>
</el-col>
<el-col v-if="auth.hasPermi('mes:plan:update')" :span="4">
<el-button type="info" plain @click="updatePlan(item.id,item.code, 'start')">开工</el-button>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-text type="warning">计划结束:{{ timestampToTime(item.planEndTime) }}</el-text>
</el-col>
</el-row>
<el-text>备注:{{ item.remark }}</el-text>
</el-collapse-item>
</el-collapse>
<!-- 计划中-->
<uni-section title="计划中" type="line" title-color="#f3a73f"></uni-section>
<el-collapse accordion>
<el-collapse-item v-for="(item, index) in planList" :name="item.id">
<template #title>
<el-icon><Tickets /></el-icon>
<el-text type="success">{{item.code}}</el-text>-
<el-text size="large">{{item.productName}}</el-text>
</template>
<el-row>
<el-col :span="12">
<el-text type="success">计划开始:{{ timestampToTime(item.planStartTime) }}</el-text>
</el-col>
<el-col :span="8">
<el-text type="success">计划数:{{ item.planNumber }}</el-text>
</el-col>
<el-col v-if="auth.hasPermi('mes:plan:update')" :span="4">
<el-button type="primary" plain>派工</el-button>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-text type="warning">计划结束:{{ timestampToTime(item.planEndTime) }}</el-text>
</el-col>
</el-row>
<el-text>备注:{{ item.remark }}</el-text>
</el-collapse-item>
</el-collapse>
<!-- 暂停中 -->
<uni-section title="暂停中" type="line" title-color="#e43d33"></uni-section>
<el-collapse accordion>
<el-collapse-item v-for="(item, index) in pauseList" :name="item.id">
<template #title>
<el-icon><Tickets /></el-icon>
<el-text type="success">{{item.code}}</el-text>-
<el-text size="large">{{item.productName}}</el-text>
</template>
<el-row>
<el-col :span="12">
<el-text type="success">计划开始:{{ timestampToTime(item.planStartTime) }}</el-text>
</el-col>
<el-col :span="8">
<el-text type="success">计划数:{{ item.planNumber }}</el-text>
</el-col>
<el-col v-if="auth.hasPermi('mes:plan:update')" :span="4">
<el-button type="success" plain @click="updatePlan(item.id,item.code, 'end')">完工</el-button>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-text type="warning">计划结束:{{ timestampToTime(item.planEndTime) }}</el-text>
</el-col>
<el-col :span="8">
<el-text type="warning">入库数:{{ item.finishNumber }}</el-text>
</el-col>
<el-col v-if="auth.hasPermi('mes:plan:update')" :span="4">
<el-button type="info" plain @click="updatePlan(item.id, item.code,'start')">开工</el-button>
</el-col>
</el-row>
<el-text>备注:{{ item.remark }}</el-text>
</el-collapse-item>
</el-collapse>
<!-- <view class="charts-box">-->
<!-- <qiun-data-charts :chartData="chartData" type="column"/>-->
<!-- </view>-->
<view class="page-container">
<NavBar :title="pageTitle" />
<scroll-view scroll-y class="main-scroll" :scroll-top="scrollTop" scroll-with-animation @scroll="onScroll">
<BannerSection />
<view class="content-section">
<NavSection />
<AppEmptyState
v-if="showEmptyState"
title="请先配置菜单权限"
desc="当前账号还未配置首页菜单权限,请联系管理员完成配置"
/>
<StatsSection v-if="showStats" @mode-change="onModeChange" />
<QualitySection v-if="showQuality" @mode-change="onModeChange" />
</view>
</scroll-view>
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<text class="go-top-icon"></text>
</view>
<TabBar />
</view>
</template>
<script setup>
import {ref, onMounted} from 'vue';
import { onMounted, onUnmounted, ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { storeToRefs } from 'pinia'
import { onLocaleChange, offLocaleChange, setNavigationTitle } from '@/locales'
import useUserStore from '@/store/modules/user'
import {getPage, getById, paigong, updateStatus, getByStatus} from "@/api/mes/plan"
import {timestampToTime} from "@/utils/dateUtil";
import {Tickets} from "@element-plus/icons-vue";
import modal from "@/plugins/modal";
import tab from "@/plugins/tab";
import {showConfirm} from "@/utils/common";
import auth from "@/plugins/auth";
import NavBar from '@/components/common/NavBar.vue'
import TabBar from '@/components/common/TabBar.vue'
import AppEmptyState from '@/components/common/AppEmptyState.vue'
import BannerSection from '@/components/dashboard/BannerSection.vue'
import NavSection from '@/components/dashboard/NavSection.vue'
import StatsSection from '@/components/dashboard/StatsSection.vue'
import QualitySection from '@/components/dashboard/QualitySection.vue'
const { t } = useI18n()
const pageTitle = computed(() => t('nav.home'))
const userStore = useUserStore()
const startList = ref([]);
const paigongList = ref([]);
const pauseList = ref([]);
const planList = ref([]);
const chartData = ref({});
onMounted(() => {
getServerData()
getPlanList()
});
function getServerData() {
//
const { menus } = storeToRefs(userStore)
const homeMenu = computed(() => {
return (menus.value || []).find((m) => m.name === '首页')
})
const hasProduction = computed(() => {
const children = homeMenu.value?.children || []
return children.some((c) => c.name === '生产')
})
const hasQuality = computed(() => {
const children = homeMenu.value?.children || []
return children.some((c) => c.name === '质量')
})
const showStats = computed(() => {
if (!hasProduction.value) return false
if (!hasQuality.value) return true
return currentMode.value === 'production'
})
const showQuality = computed(() => {
if (!hasQuality.value) return false
if (!hasProduction.value) return true
return currentMode.value === 'quality'
})
const showEmptyState = computed(() => !hasProduction.value && !hasQuality.value)
const scrollTop = ref(0)
const currentMode = ref('production')
function onModeChange(mode) {
currentMode.value = mode
}
const currentScrollTop = ref(0)
const showGoTop = ref(false)
function onScroll(e) {
const top = Number(e?.detail?.scrollTop || 0)
currentScrollTop.value = top
showGoTop.value = top > 600
}
function goTop() {
scrollTop.value = currentScrollTop.value + 1
setTimeout(() => {
let res = {
categories: ['2016', '2017', '2018', '2019', '2020', '2021'],
series: [
{
name: '目标值',
data: [35, 36, 31, 33, 13, 34],
},
{
name: '完成量',
data: [18, 27, 21, 24, 6, 28],
},
],
};
chartData.value = JSON.parse(JSON.stringify(res));
}, 500);
scrollTop.value = 0
}, 0)
}
//
function getPlanList() {
getByStatus(0).then(response => {
planList.value = response.data
})
getByStatus(1).then(response => {
paigongList.value = response.data
})
getByStatus(2).then(response => {
startList.value = response.data
})
getByStatus(3).then(response => {
pauseList.value = response.data
})
</script>
<style lang="scss" scoped>
.page-container {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f0f2f5;
}
/** 开工 */
function updatePlan(id, planCode, type){
let content = '确定['
if(type==='start')content= content+'开工]' + planCode +'?'
else if(type==='end')content= content+'完工]'+ planCode +'?'
else if(type==='pause')content= content+'暂停]'+ planCode +'?'
showConfirm(content).then(res => {
if (res.confirm) {
const data = {'id':id, 'code':type}
updateStatus(data).then(response => {
console.log(response)
getPlanList()
})
modal.msgSuccess("操作成功")
}
})
.main-scroll {
flex: 1;
height: 100%;
}
//
function planProgress(plan){
tab.navigateTo('/page_report/planProgress',plan)
.content-section {
padding: 0 24rpx 24rpx;
margin-top: -40rpx;
position: relative;
z-index: 1;
}
</script>
<style scoped>
.go-top-btn {
position: fixed;
bottom: 140rpx;
right: 30rpx;
width: 88rpx;
height: 88rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(26, 58, 92, 0.3);
z-index: 50;
.title {
font-size: 36rpx;
color: #8f8f94;
&:active {
transform: scale(0.95);
}
}
.charts-box {
width: 100%;
height: 300px;
.go-top-icon {
font-size: 44rpx;
color: #ffffff;
font-weight: bold;
}
</style>

@ -1,50 +1,62 @@
<template>
<view class="normal-login-container">
<view class="logo-content align-center justify-center flex">
<image style="width: 100rpx;height: 100rpx;" :src="globalConfig.appInfo.logo" mode="widthFix">
</image>
<text class="title">生产运营管理系统</text>
<view class="scroll-container">
<NavBar title="登录" />
<view class="login-container">
<!-- Logo区域 -->
<view class="logo-section">
<view class="logo-wrapper">
<image :src="globalConfig.appInfo.logo" mode="aspectFit" class="logo-img"></image>
</view>
<view class="login-form-content">
<view class="input-item flex align-center">
<view class="iconfont icon-user icon"></view>
<input v-model="loginForm.username" class="input" type="text" placeholder="请输入账号" maxlength="30" />
</view>
<view class="input-item flex align-center">
<view class="iconfont icon-password icon"></view>
<input v-model="loginForm.password" type="password" class="input" placeholder="请输入密码" maxlength="20" />
<!-- 顶部装饰背景 -->
<view class="background-decoration">
<view class="wave-bg"></view>
</view>
<view v-if="captchaEnabled" class="input-item flex align-center" style="width: 60%;margin: 0px;">
<view class="iconfont icon-code icon"></view>
<input v-model="loginForm.code" type="number" class="input" placeholder="请输入验证码" maxlength="4" />
<view class="login-code">
<image :src="codeUrl" @click="getCode" class="login-code-img"></image>
<view class="login-content">
<!-- 欢迎文案 -->
<view class="welcome-section">
<text class="welcome-title">欢迎来到必硕数字化平台</text>
<text class="welcome-subtitle">登录以实现您的需求</text>
</view>
<!-- 登录表单 -->
<view class="form-section">
<!-- 账号输入 -->
<up-input v-model="loginForm.username" placeholder="请输入账号" :maxlength="30" border="bottom" clearable
class="form-input" />
<!-- 密码输入 -->
<up-input v-model="loginForm.password" type="password" placeholder="请输入密码" :maxlength="20" border="bottom"
clearable class="form-input password-input" />
<!-- 忘记密码 -->
<view class="forgot-password">
<text class="forgot-link">忘记密码</text>
</view>
<!-- 登录按钮 -->
<view class="login-btn-wrapper">
<button @click="handleLogin" class="login-btn">登录</button>
</view>
<view class="action-btn">
<button @click="handleLogin" class="login-btn cu-btn block bg-blue lg round">登录</button>
</view>
</view>
<view class="xieyi text-center">
<text class="text-grey1">登录即代表同意</text>
<text class="text-blue">用户协议</text>
<text class="text-blue">隐私协议</text>
</view>
</view>
</template>
<script setup>
import modal from '@/plugins/modal'
import { getCodeImg } from '@/api/login'
import { ref } from "vue";
import config from '@/config.js'
import useUserStore from '@/store/modules/user'
import { getWxCode } from '@/utils/geek';
import { wxLogin } from '@/api/oauth';
import { setToken } from '@/utils/auth';
import NavBar from '@/components/common/NavBar.vue'
const userStore = useUserStore()
const codeUrl = ref("");
const captchaEnabled = ref(false); //
const captchaEnabled = ref(false);
const globalConfig = ref(config);
const loginForm = ref({
@ -57,7 +69,6 @@ const loginForm = ref({
uuid: ''
});
//
function getCode() {
getCodeImg().then(res => {
captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled
@ -67,7 +78,7 @@ function getCode() {
}
})
}
// getCode();
async function handleLogin() {
if (loginForm.value.username === "") {
modal.msgError("请输入您的账号")
@ -79,8 +90,8 @@ async function handleLogin() {
modal.loading("登录中,请耐心等待...")
pwdLogin()
}
};
//
}
async function pwdLogin() {
userStore.login(loginForm.value).then(() => {
modal.closeLoading()
@ -91,90 +102,146 @@ async function pwdLogin() {
getCode()
}
})
};
}
function loginSuccess(result) {
//
userStore.getInfo().then(res => {
uni.switchTab({
uni.reLaunch({
url: '/pages/index'
});
})
}
</script>
<style lang="scss">
page {
background-color: #ffffff;
min-height: 100vh;
}
.normal-login-container {
.scroll-container {
height: 100%;
width: 100%;
overflow: auto;
.logo-content {
.login-container {
width: 100%;
font-size: 21px;
text-align: center;
padding-top: 15%;
min-height: 100vh;
position: relative;
box-sizing: border-box;
padding: 0 50rpx;
image {
border-radius: 4px;
/* 顶部装饰背景 */
.background-decoration {
position: absolute;
top: 20rpx;
left: -9.2%;
right: -9.2%;
width: 118.4%;
height: 400rpx;
overflow: hidden;
.wave-bg {
width: 100%;
height: 100%;
background-image: url(@/assets/img/backgroundshadow1.png);
background-size: 100% 100%;
background-repeat: no-repeat;
}
}
.title {
margin-left: 10px;
/* Logo区域 */
.logo-section {
padding-top: 40rpx;
display: flex;
justify-content: center;
.logo-wrapper {
width: 160rpx;
height: 160rpx;
background: #ffffff;
border-radius: 40rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: center;
.logo-img {
width: 80rpx;
height: 80rpx;
}
}
}
.login-form-content {
text-align: center;
margin: 20px auto;
margin-top: 15%;
width: 80%;
/* 登录内容区 */
.login-content {
position: relative;
top: 110rpx;
.input-item {
margin: 20px auto;
background-color: #f5f6f7;
height: 45px;
border-radius: 20px;
/* 欢迎文案 */
.welcome-section {
text-align: left;
margin-top: 10rpx;
.icon {
font-size: 38rpx;
margin-left: 10px;
color: #999;
.welcome-title {
font-size: 44rpx;
font-weight: 600;
color: #333333;
display: block;
}
.input {
width: 100%;
font-size: 14px;
line-height: 20px;
text-align: left;
padding-left: 15px;
.welcome-subtitle {
font-size: 28rpx;
color: #999999;
margin-top: 16rpx;
display: block;
}
}
.login-btn {
margin-top: 40px;
height: 45px;
/* 表单区域 */
.form-section {
margin-top: 80rpx;
.form-input {
margin-bottom: 32rpx;
&.password-input {
margin-bottom: 24rpx;
}
}
.xieyi {
color: #333;
margin-top: 20px;
/* 忘记密码 */
.forgot-password {
text-align: right;
margin-bottom: 48rpx;
.forgot-link {
font-size: 26rpx;
color: #666666;
}
}
.login-code {
height: 38px;
float: right;
/* 登录按钮 */
.login-btn-wrapper {
margin-bottom: 60rpx;
.login-code-img {
height: 38px;
position: absolute;
margin-left: 10px;
width: 200rpx;
.login-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
border-radius: 20rpx;
font-size: 30rpx;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
border: none;
&:active {
opacity: 0.9;
}
}
}
}
}
}

@ -1,6 +1,6 @@
<template>
<view class="mine-container" :style="{ height: `${windowHeight}px` }">
<!--顶部个人信息栏-->
<view class="mine-container">
<NavBar :title="pageTitle" />
<view class="header-section">
<view class="flex padding justify-between">
<view class="flex align-center">
@ -10,64 +10,45 @@
<image v-if="avatar" @click="handleToAvatar" :src="avatar" class="cu-avatar xl round" mode="widthFix">
</image>
<view v-if="!name" @click="handleToLogin" class="login-tip">
点击登录
{{ t('mine.clickLogin') }}
</view>
<view v-if="name" @click="handleToInfo" class="user-info">
<view class="u_title">
用户名{{ name }}
{{ t('mine.username', { name }) }}
</view>
</view>
</view>
<view @click="handleToInfo" class="flex align-center">
<text>个人信息</text>
<text>{{ t('mine.profile') }}</text>
<view class="iconfont icon-right"></view>
</view>
</view>
</view>
<view class="content-section">
<view class="mine-actions grid col-4 text-center">
<view class="action-item" @click="handleBuilding">
<view class="iconfont icon-friendfill text-green icon"></view>
<text class="text">反馈中心</text>
</view>
<view class="action-item" @click="handleBuilding">
<view class="iconfont icon-service text-blue icon"></view>
<text class="text">在线客服</text>
</view>
<view class="action-item" @click="handleToPwd">
<view class="iconfont icon-password text-mauve icon"></view>
<text class="text">修改密码</text>
</view>
<view class="action-item" @click="handleLogout">
<view class="iconfont icon-logout text-red icon"></view>
<text class="text">退出登录</text>
</view>
</view>
<view class="menu-list">
<view class="list-cell list-cell-arrow" @click="handleToEditInfo">
<view class="menu-item-box">
<view class="iconfont icon-user menu-icon"></view>
<view>编辑资料</view>
<view>{{ t('mine.editProfile') }}</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleHelp">
<view class="menu-item-box">
<view class="iconfont icon-help menu-icon"></view>
<view>常见问题</view>
<view>{{ t('mine.faq') }}</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleAbout">
<view class="menu-item-box">
<view class="iconfont icon-aixin menu-icon"></view>
<view>关于我们</view>
<view>{{ t('mine.about') }}</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleToSetting">
<view class="menu-item-box">
<view class="iconfont icon-setting menu-icon"></view>
<view>应用设置</view>
<view>{{ t('mine.appSettings') }}</view>
</view>
</view>
</view>
@ -75,33 +56,35 @@
<view>
<uni-popup ref="popup" type="dialog">
<uni-popup-dialog type="info" cancelText="关闭" confirmText="退出" title="通知" content="确定注销并退出系统吗"
<uni-popup-dialog type="info" :cancelText="t('common.close')" :confirmText="t('common.exit')" :title="t('common.notice')" :content="t('common.confirmLogout')"
@confirm="dialogConfirm" @close="dialogClose">
</uni-popup-dialog>
</uni-popup>
</view>
<TabBar />
</view>
</template>
<script setup>
import { ref } from "vue";
import config from '@/config.js'
import { computed, onUnmounted, ref } from "vue";
import { useI18n } from 'vue-i18n'
import useUserStore from '@/store/modules/user'
import { onLocaleChange, offLocaleChange } from '@/locales'
import NavBar from '@/components/common/NavBar.vue'
import TabBar from '@/components/common/TabBar.vue'
const userStore = useUserStore()
const name = userStore.name;
const version = config.appInfo.version;
const { t } = useI18n()
const pageTitle = computed(() => t('nav.mine'))
const avatar = ref(userStore.avatar);
const windowHeight = ref(uni.getSystemInfoSync().windowHeight - 50);
const popup = ref(null);
uni.$on('refresh', () => {
avatar.value = userStore.avatar;
})
console.log(avatar.value)
function handleToInfo() {
uni.navigateTo({
url: '/pages_mine/pages/info/index'
@ -131,7 +114,6 @@ function handleLogout() {
popup.value.open();
};
function dialogConfirm() {
//console.log('----------------------------')
userStore.logOut().then(() => {
uni.reLaunch({
url: '/pages/login'
@ -139,7 +121,6 @@ function dialogConfirm() {
})
};
function dialogClose() {
//console.log('')
};
function handleToPwd() {
uni.navigateTo({
@ -150,7 +131,8 @@ function handleHelp() {
uni.navigateTo({
url: '/pages_mine/pages/help/index'
});
};
}
function handleAbout() {
uni.navigateTo({
url: '/pages_mine/pages/about/index'
@ -158,13 +140,12 @@ function handleAbout() {
}
function handleBuilding() {
uni.showToast({
title: '模块建设中~',
title: t('common.moduleBuilding'),
mask: false,
icon: "none",
duration: 1000
});
}
</script>
<style lang="scss">
@ -174,12 +155,74 @@ page {
.mine-container {
width: 100%;
height: 100%;
min-height: 100vh;
.header-section {
padding: 15px 15px 45px 15px;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
color: white;
.login-tip {
font-size: 18px;
margin-left: 10px;
}
.cu-avatar {
border: 2px solid #eaeaea;
.icon {
font-size: 40px;
}
}
.user-info {
margin-left: 15px;
.u_title {
font-size: 18px;
line-height: 30px;
}
}
}
.content-section {
position: relative;
top: -50px;
.mine-actions {
margin: 15px 15px;
padding: 20px 0px;
border-radius: 8px;
background-color: white;
.action-item {
.icon {
font-size: 28px;
}
.text {
display: block;
font-size: 13px;
margin: 8px 0px;
}
}
}
}
}
</style>
<style lang="scss">
page {
background-color: #f5f6f7;
}
.mine-container {
width: 100%;
min-height: 100vh;
.header-section {
padding: 15px 15px 45px 15px;
background-color: #3c96f3;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
color: white;
.login-tip {

@ -1,321 +1,38 @@
<template>
<view class="container">
<view class="example">
<el-tabs v-loading="loading" v-model="activeName" class="demo-tabs" @tab-click="handleTabClick">
<el-tab-pane label="个人报工" name="first">
<uni-list :border="true">
<uni-list-item v-for="(item, index) in reportList">
<!-- 自定义 header -->
<template v-slot:header>
<div onclick="">
<text class="u-success"> {{timestampToTime(item.reportDate)}}</text>
<text class="u-primary">/</text>
{{item.userName}}
<text class="u-primary">/</text>
{{item.orgName}}
</div>
</template>
<!-- 自定义 body -->
<template v-slot:body>
</template>
<!-- 自定义 footer-->
<template v-slot:footer>
<el-button v-if="item.reportStatus === 0" type="primary" size="small" :icon="Edit" circle @click="editOrAddReport(item.id)"/>
<el-button v-if="item.reportStatus === 0" type="success" size="small" :icon="Check" circle @click="updateReport(item.id)"/>
<el-button v-if="item.reportStatus < 2" type="danger" size="small" :icon="Delete" circle @click="deleteReport(item.id)"/>
</template>
</uni-list-item>
</uni-list>
<uni-fab ref="fab" @fabClick="editOrAddReport" />
</el-tab-pane>
<el-tab-pane label="代报工" name="second" v-if="auth.hasPermi('mes:produce-report-detail:replace')">
<uni-fab :pattern="pattern" ref="fabReplace" @fabClick="addReplaceReport" />
<uni-forms ref="valiReplaceForm" :model="valiFormData">
<uni-row>
<uni-col :xs="14" :sm="12" :md="6" :lg="3" :xl="1">
<uni-forms-item label-width="50px" label-align="left" label="日期" name="reportDateString">
<uni-datetime-picker v-model="valiFormData.reportDateString" type="date" :clear-icon="true" @change="maskClick" />
</uni-forms-item>
</uni-col>
<uni-col :xs="10" :sm="8" :md="6" :lg="3" :xl="1">
<uni-forms-item label-width="50px" label-align="left" label="工人" name="userId">
<uni-data-select v-model="valiFormData.userId" :localdata="userList" placement="top" @change="handleReplaceReportChange()">
</uni-data-select>
</uni-forms-item>
</uni-col>
</uni-row>
<uni-forms-item label="工序" name="orgType">
<uni-data-checkbox v-model="valiFormData.orgType" :localdata="processTypes()" @change="handleReplaceReportChange()"/>
</uni-forms-item>
</uni-forms>
<uni-list :border="true">
<uni-list-item v-for="(item, index) in replaceReportList">
<!-- 自定义 header -->
<template v-slot:header>
<div onclick="">
<text class="u-success"> {{timestampToTime(item.reportDate)}}</text>
<text class="u-primary">/</text>
{{item.userName}}
<text class="u-primary">/</text>
{{item.orgName}}
</div>
</template>
<!-- 自定义 body -->
<template v-slot:body>
</template>
<!-- 自定义 footer-->
<template v-slot:footer>
<el-button v-if="item.reportStatus === 0" type="primary" size="small" :icon="Edit" circle @click="addReplaceReport(item.id)"/>
<el-button v-if="item.reportStatus === 0" type="success" size="small" :icon="Promotion" circle @click="updateReport(item.id,1,'提交')"/>
<el-button v-if="item.reportStatus === 1" type="success" size="small" :icon="Check" circle @click="updateReport(item.id,2,'通过')"/>
<el-button v-if="item.reportStatus < 2" type="danger" size="small" :icon="Delete" circle @click="deleteReport(item.id)"/>
</template>
</uni-list-item>
</uni-list>
</el-tab-pane>
<el-tab-pane label="个人报工审核" name="third" v-if="auth.hasPermi('mes:produce-report-detail:replace')">
<uni-forms ref="valiForm" :model="valiFormData">
<uni-row>
<uni-col :xs="14" :sm="12" :md="6" :lg="3" :xl="1">
<uni-forms-item label-width="50px" label-align="left" label="日期" name="reportDateString">
<uni-datetime-picker v-model="valiFormData.reportDateString" type="date" :clear-icon="true" @change="maskClick" />
</uni-forms-item>
</uni-col>
<uni-col :xs="10" :sm="8" :md="6" :lg="3" :xl="1">
<uni-forms-item label-width="50px" label-align="left" label="工人" name="userId">
<uni-data-select v-model="valiFormData.userId" :localdata="userList" placement="top" @change="handleOtherReportChange()">
</uni-data-select>
</uni-forms-item>
</uni-col>
</uni-row>
<uni-forms-item label="工序" name="orgType">
<uni-data-checkbox v-model="valiFormData.orgType" :localdata="processTypes()" @change="handleOtherReportChange()"/>
</uni-forms-item>
</uni-forms>
<uni-list :border="true">
<uni-list-item v-for="(item, index) in otherReportList">
<!-- 自定义 header -->
<template v-slot:header>
<div onclick="">
<text class="u-success"> {{timestampToTime(item.reportDate)}}</text>
<text class="u-primary">/</text>
{{item.userName}}
<text class="u-primary">/</text>
{{item.orgName}}
</div>
</template>
<!-- 自定义 body -->
<template v-slot:body>
</template>
<!-- 自定义 footer-->
<template v-slot:footer>
<el-button v-if="item.reportStatus === 0" type="primary" size="small" :icon="Edit" circle @click="addReplaceReport(item.id)"/>
<el-button v-if="item.reportStatus === 0" type="success" size="small" :icon="Promotion" circle @click="updateReport(item.id,1,'提交')"/>
<el-button v-if="item.reportStatus === 1" type="success" size="small" :icon="Check" circle @click="updateReport(item.id,2,'通过')"/>
<el-button v-if="item.reportStatus === 1" type="warning" size="small" :icon="Close" circle @click="updateReport(item.id,3,'驳回')"/>
<el-button v-if="item.reportStatus < 2" type="danger" size="small" :icon="Delete" circle @click="deleteReport(item.id)"/>
</template>
</uni-list-item>
</uni-list>
<view>
<div></div>
</view>
</el-tab-pane>
<el-tab-pane label="报工报表" name="four">报工报表</el-tab-pane>
</el-tabs>
</view>
<view class="page-container">
<NavBar :title="pageTitle" />
<PermissionMenuPage
class="flex-fill"
page-path="pages/report"
title="报表中心"
subtitle="数据驱动决策 · 智能分析"
:safe-bottom="true"
/>
<TabBar />
</view>
</template>
<script>
import {getMyList, getOtherList, getById, updateStatus,deleteByReportId } from "@/api/mes/report";
import {Check, Delete, Edit,Promotion,Close} from '@element-plus/icons-vue'
import tab from "@/plugins/tab";
import modal from "@/plugins/modal";
import auth from "@/plugins/auth";
import {showConfirm} from "@/utils/common";
import {getCurrentDate, timestampToTime} from "@/utils/dateUtil";
import {processTypes} from "@/api/system/dict/data";
import {getOtherPersonalUser} from "@/api/mes/organization";
export default {
computed: {
auth() {return auth},
Delete() {return Delete},
Promotion() {return Promotion},
Close() {return Close},
Edit() {return Edit},
Check() {return Check}
},
data() {
return {
loading: false,
reportList: [],
otherReportList: [],
replaceReportList: [],
userList: [],
pattern: {
color: '#7A7E83',
backgroundColor: '#fff',
selectedColor: '#007AFF',
buttonColor: '#e66126',
iconColor: '#fff'
},
activeName: 'first',
valiFormData: {
reportDate: [],
reportDateString: getCurrentDate(),
userId: '',
orgType: 'chengxing',
reportType: '个人',
},
};
},
onLoad() {
this.loading = true
this.getList();
this.getUserList()
this.loading = false
},
onPullDownRefresh() {
console.log('refresh');
setTimeout(function () {
uni.stopPullDownRefresh();
}, 1000);
},
methods: {
processTypes() {
return processTypes
},
handleTabClick(tab) {
this.loading = true
console.log(tab.paneName)
//
if(tab.paneName==='third'){
this.getOtherReportList()
}
//
else if(tab.paneName==='second'){
this.getReplaceReportList();
}
else {
//
this.getList();
}
this.loading = false
},
timestampToTime,
getOtherReportList(){
let date = this.valiFormData.reportDateString
if(date.length<5)date = getCurrentDate()
this.valiFormData.reportDate = [date+' 00:00:00', date+' 23:59:59']
this.valiFormData.reportType = '个人'
getOtherList(this.valiFormData).then(response => {
this.otherReportList = response.data
})
},
getReplaceReportList(){
let date = this.valiFormData.reportDateString
if(date.length<5)date = getCurrentDate()
this.valiFormData.reportDate = [date+' 00:00:00', date+' 23:59:59']
this.valiFormData.reportType = '代报工'
getOtherList(this.valiFormData).then(response => {
this.replaceReportList = response.data
})
},
getUserList() {
getOtherPersonalUser().then(response => {
this.userList = response.data
})
},
getList() {
getMyList().then(response => {
this.reportList = response.data
})
},
deleteReport(id) {
showConfirm("确认删除该报工信息吗?").then(res => {
this.loading = true
if (res.confirm) {
deleteByReportId(id).then(response => {
modal.msgSuccess("操作成功")
if(this.activeName==='first')this.getList()
if(this.activeName==='third')this.getOtherReportList()
if(this.activeName==='second')this.getReplaceReportList()
})
}
this.loading = false
})
},
updateReport(id,status,type) {
showConfirm("确认"+type+"该报工信息吗?").then(res => {
this.loading = true
if (res.confirm) {
updateStatus(id,status).then(response => {
modal.msgSuccess("操作成功")
if(this.activeName==='first')this.getList()
if(this.activeName==='third')this.getOtherReportList()
if(this.activeName==='second')this.getReplaceReportList()
})
}
this.loading = false
})
},
editOrAddReport(id){
if(id===null || id===undefined)tab.navigateTo('/page_report/reportForm')
else tab.navigateTo("",id)
},
addReplaceReport(id){
if(id===null || id===undefined)tab.navigateTo('/page_report/replaceForm')
else tab.navigateTo('/page_report/replaceForm',id)
},
maskClick(e){
this.valiFormData.reportDateString = e
if(this.activeName==='first')this.getList()
if(this.activeName==='third')this.getOtherReportList()
if(this.activeName==='second')this.getReplaceReportList()
},
handleOtherReportChange(){
this.loading = true
//
this.getOtherReportList()
this.loading = false
},
handleReplaceReportChange(){
this.loading = true
//
this.getReplaceReportList()
this.loading = false
}
}
};
<script setup>
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import TabBar from '@/components/common/TabBar.vue'
import PermissionMenuPage from '@/components/common/PermissionMenuPage.vue'
const { t } = useI18n()
const pageTitle = computed(() => t('tab.report'))
</script>
<style lang="scss" scoped>
.example {
padding: 15px;
background-color: #fff;
}
.addSite {
display: flex;
justify-content: space-around;
width: 600rpx;
line-height: 100rpx;
position: absolute;
bottom: 30rpx;
left: 80rpx;
background-color: red;
border-radius: 60rpx;
font-size: 30rpx;
.add{
.page-container {
display: flex;
align-items: center;
color: #ffffff;
.icon{
margin-right: 10rpx;
}
}
flex-direction: column;
height: 100vh;
background-color: #f5f6f7;
}
.flex-fill {
flex: 1;
overflow: visible;
}
</style>

@ -1,159 +1,33 @@
<template>
<view >
<uni-notice-bar show-icon scrollable
text="安全生产!有序生产!高效生产!" />
<!-- 已提交-->
<uni-section title="已提交" type="line" title-color="#18bc37"></uni-section>
<el-collapse accordion>
<el-collapse-item v-for="(item, index) in finishList" :name="item.id" @click="handleFinishClick(item.id,index)">
<template #title>
<el-icon><Tickets /></el-icon>
<el-text type="success">{{timestampToTime(item.feedingTime)}}</el-text>-
<el-text size="large">{{item.feedingRecordCode}}</el-text>-
<el-text type="warning">{{ findTextByValue(pipelineTypes, item.feedingPipeline)}}</el-text>
</template>
<el-row>
<el-col :span="6">投料类型:
<el-text type="warning">{{findTextByValue(feedingTypes, item.feedingType) }}</el-text>
</el-col>
<el-col :span="8">记录人:
<el-text type="success">{{ item.userName }}</el-text>
</el-col>
<el-col :span="8" v-if="item.feedingType !=='org'">/kg:
<el-text>{{ item.weight }} </el-text>
</el-col>
</el-row>
<el-row v-for="(item2, index2) in finishList[index].detailList">
<el-col :span="8">原料:
<el-text type="warning">{{item2.itemName}}</el-text>
</el-col>
<el-col :span="8">数量:
<el-text type="success">{{ item2.weight }}</el-text>
</el-col>
<el-col :span="8">单位:
<el-text>{{item2.unitName }} </el-text>
</el-col>
</el-row>
</el-collapse-item>
</el-collapse>
<!-- 草稿状态-->
<uni-section title="草稿" type="line" title-color="#f3a73f"></uni-section>
<el-collapse accordion>
<el-collapse-item v-for="(item, index) in draftList" :name="item.id" @click="handleDraftClick(item.id,index)">
<template #title>
<el-icon><Tickets /></el-icon>
<el-text type="success">{{timestampToTime(item.feedingTime)}}</el-text>-
<el-text size="large">{{item.feedingRecordCode}}</el-text>-
<el-text type="warning">{{ findTextByValue(pipelineTypes, item.feedingPipeline)}}</el-text>
</template>
<el-row>
<el-col :span="6">投料类型:
<el-text type="warning">{{findTextByValue(feedingTypes, item.feedingType) }}</el-text>
</el-col>
<el-col :span="8">记录人:
<el-text type="success">{{ item.userName }}</el-text>
</el-col>
<el-col :span="4" v-if="item.feedingType !=='org'">/kg:
<el-text>{{ item.weight }} </el-text>
</el-col>
<el-col :span="3">
<el-button type="info" @click="handleUpdate(item.id)"></el-button>
</el-col>
<el-col :span="3">
<el-button type="danger" @click="handleDelete(item.id)"></el-button>
</el-col>
</el-row>
<el-row v-for="(item2, index2) in draftList[index].detailList">
<el-col :span="8">原料:
<el-text type="warning">{{item2.itemName}}</el-text>
</el-col>
<el-col :span="8">数量:
<el-text type="success">{{ item2.weight }}</el-text>
</el-col>
<el-col :span="8">单位:
<el-text>{{item2.unitName }} </el-text>
</el-col>
</el-row>
</el-collapse-item>
</el-collapse>
<uni-fab ref="fab" :pattern="pattern" @fabClick="handleAdd" />
<view class="page-container">
<NavBar :title="pageTitle" />
<PermissionMenuPage class="flex-fill" page-path="pages/work" title="管理中心" subtitle="系统配置与管理" :searchable="true"
:show-go-top="true" :safe-bottom="true" />
<TabBar />
</view>
</template>
<script setup>
import {ref, onMounted} from 'vue';
import {getRecordList, updateStatus, deleteById, getDetailByRecordId} from "@/api/mes/record"
import {timestampToTime,timestampToDateTime} from "@/utils/dateUtil";
import { pipelineTypes,feedingTypes, findTextByValue} from "@/api/system/dict/data";
import {Tickets} from "@element-plus/icons-vue";
import modal from "@/plugins/modal";
import tab from "@/plugins/tab";
import {showConfirm} from "@/utils/common";
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import TabBar from '@/components/common/TabBar.vue'
import PermissionMenuPage from '@/components/common/PermissionMenuPage.vue'
const { t } = useI18n()
const pageTitle = computed(() => t('tab.work'))
</script>
//
const finishList = ref([]);
//稿
const draftList = ref([]);
const pattern = {
color: '#7A7E83',
backgroundColor: '#fff',
selectedColor: '#007AFF',
buttonColor: '#f4c7c7',
iconColor: '#fff'
}
onMounted(() => {
getList()
});
//
function getList() {
getRecordList("2").then(response => {
finishList.value = response.data
})
getRecordList("1").then(response => {
draftList.value = response.data
})
}
function handleFinishClick(id, index){
getDetailByRecordId(id).then(response => {
finishList.value[index].detailList = response.data
})
}
function handleDraftClick(id, index){
getDetailByRecordId(id).then(response => {
draftList.value[index].detailList = response.data
})
}
/** 提交 */
function handleUpdate(id){
showConfirm("确认提交投料记录吗?").then(res => {
if (res.confirm) {
updateStatus(id, 2).then(response => {
modal.msgSuccess("操作成功")
getList()
})
}
})
}
/** 提交 */
function handleDelete(id){
showConfirm("确认删除投料记录吗?").then(res => {
if (res.confirm) {
deleteById(id).then(response => {
modal.msgSuccess("操作成功")
getList()
})
}
})
<style lang="scss" scoped>
.page-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f6f7;
}
//
function handleAdd(){
tab.navigateTo('/page_record/feedingRecordForm')
.flex-fill {
flex: 1;
overflow: visible;
}
</script>
<style scoped>
</style>

@ -0,0 +1,140 @@
<template>
<view class="page-container">
<view class="fixed-header">
<NavBar :title="t('criticalComponent.detailTitle')" />
</view>
<scroll-view scroll-y class="detail-scroll">
<view class="content-section">
<!-- 基础信息 -->
<view class="info-card">
<view class="card-title">{{ t('criticalComponent.basicInfo') }}</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">{{ t('criticalComponent.code') }}</text>
<text class="info-value">{{ fieldValue('code') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('criticalComponent.name') }}</text>
<text class="info-value">{{ fieldValue('name') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('criticalComponent.deviceSpec') }}</text>
<text class="info-value">{{ fieldValue('deviceSpec') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('criticalComponent.description') }}</text>
<text class="info-value">{{ fieldValue('description') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('criticalComponent.count') }}</text>
<text class="info-value">{{ fieldValue('count') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('criticalComponent.creatorName') }}</text>
<text class="info-value">{{ fieldValue('creatorName') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('criticalComponent.createTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData?.createTime) }}</text>
</view>
<view class="info-row remark-row">
<text class="info-label">{{ t('criticalComponent.remark') }}</text>
<text class="info-value remark-value">{{ fieldValue('remark') }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getCriticalComponent } from '@/api/mes/criticalComponent'
const { t } = useI18n()
const detailId = ref(undefined)
const detailData = ref(null)
onLoad(async (query) => {
const id = query?.id ? decodeURIComponent(String(query.id)) : ''
detailId.value = id || undefined
await fetchDetail()
})
async function fetchDetail() {
if (!detailId.value) {
uni.showToast({ title: t('criticalComponent.noId'), icon: 'none' })
return
}
try {
const res = await getCriticalComponent(detailId.value)
detailData.value = normalizeDetail(res)
} catch (e) {
uni.showToast({ title: t('criticalComponent.loadFailed'), icon: 'none' })
}
}
function normalizeDetail(res) {
const root = res && res.data !== undefined ? res.data : res
if (root?.data && typeof root.data === 'object') return root.data
if (root && typeof root === 'object') return root
return {}
}
function fieldValue(field) {
return textValue(detailData.value ? detailData.value[field] : undefined)
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return t('functionCommon.no')
if (value === true) return t('functionCommon.yes')
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function formatDateTime(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = value
const pad = (n) => String(n).padStart(2, '0')
return `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}:${pad(ss)}`
}
const text = String(value).trim()
if (!text) return '-'
const numeric = Number(text)
if (Number.isFinite(numeric)) {
const timestamp = text.length === 10 ? numeric * 1000 : numeric
const date = new Date(timestamp)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
}
const date = new Date(text)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
return text
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.fixed-header { position: sticky; top: 0; z-index: 10; }
.detail-scroll { height: calc(100vh - 120rpx); }
.content-section { padding: 0 24rpx 24rpx; }
.info-card { margin-top: 20rpx; background: #ffffff; border-radius: 20rpx; padding: 28rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); }
.card-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; margin-bottom: 18rpx; }
.info-list { background: #ffffff; }
.info-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 18rpx 0; border-bottom: 1rpx solid #edf0f3; }
.info-label { font-size: 27rpx; color: #8a9099; width: 220rpx; }
.info-value { flex: 1; text-align: right; font-size: 28rpx; color: #30363d; line-height: 1.45; }
.remark-row { border-bottom: none; }
.remark-value { white-space: pre-wrap; }
</style>

@ -0,0 +1,422 @@
<template>
<view class="page-container">
<NavBar :title="t('criticalComponent.moduleName')" :subTitle="t('criticalComponent.subTitle')" />
<!-- 搜索区域 -->
<view class="search-card">
<view class="search-row">
<view class="search-input-wrap">
<text class="iconfont icon-search search-icon"></text>
<input v-model="searchKeyword" class="search-input" :placeholder="t('criticalComponent.searchPlaceholder')" @confirm="handleSearch" />
</view>
<view class="search-btn" @click="handleSearch">{{ t('functionCommon.search') }}</view>
</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="type-card" @click="openDetail(item)">
<view class="card-header">
<view class="header-left">
<text class="type-name">{{ textValue(item.name) }}</text>
<text class="type-code">{{ t('criticalComponent.code') }}: {{ textValue(item.code) }}</text>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('criticalComponent.deviceSpec') }}</text>
<text class="value">{{ textValue(item.deviceSpec) }}</text>
</view>
<view class="row">
<text class="label">{{ t('criticalComponent.description') }}</text>
<text class="value">{{ textValue(item.description) }}</text>
</view>
<view class="row">
<text class="label">{{ t('criticalComponent.count') }}</text>
<text class="value">{{ textValue(item.count) }}</text>
</view>
</view>
<view class="card-actions">
<view class="action-btn edit-btn" @click.stop="openEdit(item)">
<uni-icons type="compose" size="18" color="#ffffff"></uni-icons>
</view>
<view class="action-btn delete-btn" @click.stop="confirmDelete(item)">
<uni-icons type="trash" size="18" color="#ffffff"></uni-icons>
</view>
</view>
</view>
<view v-if="loading && pageNo === 1" class="loading-text">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!list.length" class="empty-text">{{ t('criticalComponent.empty') }}</view>
<view v-else-if="loadingMore" class="loading-text">{{ t('functionCommon.loadingMore') }}</view>
<view v-else-if="finished" class="finished-text">{{ t('functionCommon.noMoreData') }}</view>
</view>
</scroll-view>
<!-- 新增悬浮按钮 -->
<view class="add-btn" @click="openCreate">
<text class="add-icon">+</text>
</view>
<!-- 返回顶部 -->
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<text class="go-top-icon"></text>
</view>
<!-- 新增/编辑弹框 -->
<uni-popup ref="formPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ formMode === 'create' ? t('criticalComponent.createTitle') : t('criticalComponent.editTitle') }}</text>
<view class="popup-close" @click="closeForm">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="form-scroll">
<view class="form-content">
<!-- 编码 -->
<view class="form-item">
<text class="form-label">{{ t('criticalComponent.code') }} <text class="required-star">*</text></text>
<view class="code-row">
<input v-model="formData.code" class="form-input code-input" type="text" :placeholder="t('criticalComponent.placeholderCode')" :disabled="formData.isCode || formMode === 'update'" />
<view class="auto-code-wrap" @click="toggleAutoCode">
<text class="auto-code-label">{{ t('criticalComponent.autoCode') }}</text>
<view :class="['auto-code-switch', formData.isCode ? 'active' : '']">
<text class="switch-dot"></text>
</view>
</view>
</view>
</view>
<!-- 名称 -->
<view class="form-item">
<text class="form-label">{{ t('criticalComponent.name') }} <text class="required-star">*</text></text>
<input v-model="formData.name" class="form-input" type="text" :placeholder="t('criticalComponent.placeholderName')" />
</view>
<!-- 规格 -->
<view class="form-item">
<text class="form-label">{{ t('criticalComponent.deviceSpec') }}</text>
<input v-model="formData.deviceSpec" class="form-input" type="text" :placeholder="t('criticalComponent.placeholderDeviceSpec')" />
</view>
<!-- 描述 -->
<view class="form-item">
<text class="form-label">{{ t('criticalComponent.description') }}</text>
<textarea v-model="formData.description" class="form-textarea" :placeholder="t('criticalComponent.placeholderDescription')" :maxlength="500" />
</view>
<!-- 数量 -->
<view class="form-item">
<text class="form-label">{{ t('criticalComponent.count') }}</text>
<input v-model="formData.count" class="form-input" type="digit" :placeholder="t('criticalComponent.placeholderCount')" />
</view>
<!-- 备注 -->
<view class="form-item">
<text class="form-label">{{ t('criticalComponent.remark') }}</text>
<textarea v-model="formData.remark" class="form-textarea" :placeholder="t('criticalComponent.placeholderRemark')" :maxlength="200" />
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeForm">
<text class="btn-text">{{ t('functionCommon.cancel') }}</text>
</view>
<view class="footer-btn confirm-btn" @click="submitForm">
<text class="btn-text">{{ t('functionCommon.save') }}</text>
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getCriticalComponentPage, getCriticalComponent, createCriticalComponent, updateCriticalComponent, deleteCriticalComponent } from '@/api/mes/criticalComponent'
const { t } = useI18n()
const formPopupRef = ref(null)
const searchKeyword = ref('')
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const finished = ref(false)
const pageNo = ref(1)
const pageSize = ref(10)
const total = ref(0)
const scrollTop = ref(0)
const showGoTop = ref(false)
const formMode = ref('create')
const formData = reactive({
id: undefined,
code: '',
isCode: true,
name: '',
deviceSpec: '',
description: '',
count: '',
remark: ''
})
onLoad(async () => {
await fetchList(true)
})
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,
code: searchKeyword.value.trim() || undefined,
name: searchKeyword.value.trim() || undefined
}
const res = await getCriticalComponentPage(params)
const page = normalizePageData(res)
total.value = page.total
if (reset) {
list.value = page.list
} else {
list.value = [...list.value, ...page.list]
}
const loadedCount = list.value.length
finished.value = loadedCount >= total.value || 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 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 || root?.data?.records || []
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 handleSearch() {
await fetchList(true)
}
function onScroll(e) {
const top = e?.detail?.scrollTop || 0
showGoTop.value = top > 600
}
function goTop() {
scrollTop.value = 0
}
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
}
function openCreate() {
formMode.value = 'create'
resetForm()
formPopupRef.value?.open()
}
async function openEdit(item) {
const id = item?.id
if (id === undefined || id === null) {
uni.showToast({ title: t('criticalComponent.noId'), icon: 'none' })
return
}
try {
const res = await getCriticalComponent(id)
const detail = normalizeDetail(res)
formMode.value = 'update'
formData.id = detail?.id
formData.code = textValueForInput(detail?.code)
formData.isCode = Boolean(detail?.isCode)
formData.name = textValueForInput(detail?.name)
formData.deviceSpec = textValueForInput(detail?.deviceSpec)
formData.description = textValueForInput(detail?.description)
formData.count = textValueForInput(detail?.count)
formData.remark = textValueForInput(detail?.remark)
formPopupRef.value?.open()
} catch (e) {
uni.showToast({ title: t('criticalComponent.loadEditFailed'), icon: 'none' })
}
}
function confirmDelete(item) {
const id = item?.id
if (id === undefined || id === null) return
uni.showModal({
title: t('functionCommon.confirmTitle'),
content: t('criticalComponent.confirmDeleteContent', { name: textValue(item?.name) }),
success: async (res) => {
if (!res.confirm) return
try {
await deleteCriticalComponent(String(id))
uni.showToast({ title: t('functionCommon.deleteSuccess'), icon: 'success' })
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.deleteFailed'), icon: 'none' })
}
}
})
}
function closeForm() {
formPopupRef.value?.close()
}
function toggleAutoCode() {
if (formMode.value === 'update') return
formData.isCode = !formData.isCode
if (formData.isCode) formData.code = ''
}
async function submitForm() {
if (!formData.isCode && !formData.code.trim()) {
uni.showToast({ title: t('criticalComponent.validatorCodeRequired'), icon: 'none' })
return
}
if (!formData.name.trim()) {
uni.showToast({ title: t('criticalComponent.validatorNameRequired'), icon: 'none' })
return
}
const payload = {
id: formMode.value === 'update' ? formData.id : undefined,
code: formData.isCode ? undefined : formData.code.trim(),
isCode: formData.isCode,
name: formData.name.trim(),
deviceSpec: formData.deviceSpec.trim() || undefined,
description: formData.description.trim() || undefined,
count: toNumberOrUndefined(formData.count),
remark: formData.remark.trim() || undefined
}
try {
if (formMode.value === 'create') {
await createCriticalComponent(payload)
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
} else {
await updateCriticalComponent(payload)
uni.showToast({ title: t('functionCommon.updateSuccess'), icon: 'success' })
}
closeForm()
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
}
}
function resetForm() {
formData.id = undefined
formData.code = ''
formData.isCode = true
formData.name = ''
formData.deviceSpec = ''
formData.description = ''
formData.count = ''
formData.remark = ''
}
function openDetail(item) {
const id = item?.id
if (!id && id !== 0) return
uni.navigateTo({
url: `/pages_function/pages/criticalComponent/detail?id=${encodeURIComponent(String(id))}`
})
}
function normalizeDetail(res) {
const root = res && res.data !== undefined ? res.data : res
if (root?.data && typeof root.data === 'object') return root.data
if (root && typeof root === 'object') return root
return {}
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return t('functionCommon.no')
if (value === true) return t('functionCommon.yes')
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function textValueForInput(value) {
if (value === null || value === undefined) return ''
return String(value)
}
function toNumberOrUndefined(value) {
if (value === null || value === undefined || String(value).trim() === '') return undefined
const num = Number(value)
return Number.isFinite(num) ? num : undefined
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.search-card { background: #ffffff; margin: 20rpx 24rpx; border-radius: 18rpx; padding: 20rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.04); }
.search-row { display: flex; align-items: center; gap: 16rpx; }
.search-input-wrap { flex: 1; display: flex; align-items: center; background: #f5f7fa; border-radius: 44rpx; padding: 0 20rpx; }
.search-icon { margin-right: 12rpx; font-size: 30rpx; color: #999; }
.search-input { flex: 1; height: 72rpx; font-size: 28rpx; color: #333; background: transparent; }
.search-btn { min-width: 120rpx; height: 72rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); border-radius: 14rpx; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 28rpx; font-weight: 600; }
.list-scroll { height: calc(100vh - 360rpx); }
.list-wrap { padding: 0 24rpx 30rpx; }
.type-card { background: #ffffff; border-radius: 18rpx; padding: 24rpx; margin-bottom: 18rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.05); }
.card-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 18rpx; border-bottom: 1rpx solid #edf0f3; }
.header-left { display: flex; flex-direction: column; }
.type-name { font-size: 32rpx; font-weight: 600; color: #1a3a5c; margin-bottom: 8rpx; }
.type-code { font-size: 24rpx; color: #8a9099; }
.card-body { padding-top: 16rpx; }
.row { display: flex; justify-content: space-between; align-items: center; margin-top: 12rpx; }
.label { font-size: 26rpx; color: #8a9099; }
.value { font-size: 27rpx; color: #30363d; max-width: 62%; text-align: right; }
.card-actions { margin-top: 24rpx; display: flex; justify-content: flex-end; gap: 14rpx; }
.action-btn { width: 60rpx; height: 60rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.edit-btn { background: #1a3a5c; }
.delete-btn { background: #ff4d4f; }
.add-btn { position: fixed; right: 28rpx; bottom: 140rpx; width: 96rpx; height: 96rpx; border-radius: 48rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); display: flex; align-items: center; justify-content: center; box-shadow: 0 8rpx 24rpx rgba(26, 58, 92, 0.32); z-index: 99; }
.add-icon { color: #ffffff; font-size: 56rpx; line-height: 1; }
.go-top-btn { position: fixed; right: 28rpx; bottom: 254rpx; width: 88rpx; height: 88rpx; border-radius: 44rpx; background: rgba(26, 58, 92, 0.9); display: flex; align-items: center; justify-content: center; z-index: 99; box-shadow: 0 6rpx 16rpx rgba(26, 58, 92, 0.25); }
.go-top-icon { color: #ffffff; font-size: 36rpx; font-weight: 700; }
.popup-content { width: 680rpx; max-height: 80vh; border-radius: 20rpx; overflow: hidden; }
.popup-header { padding: 24rpx; display: flex; align-items: center; justify-content: space-between; border-bottom: 1rpx solid #edf0f3; }
.popup-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; }
.close-icon { font-size: 38rpx; color: #8e95a0; }
.form-scroll { max-height: 56vh; }
.form-content { padding: 24rpx; }
.form-item { margin-bottom: 18rpx; }
.form-label { display: block; margin-bottom: 8rpx; font-size: 26rpx; color: #8a9099; }
.required-star { color: #ff4d4f; }
.form-input { height: 76rpx; padding: 0 20rpx; border-radius: 12rpx; background: #f5f7fa; font-size: 28rpx; color: #30363d; }
.code-row { display: flex; align-items: center; gap: 16rpx; }
.code-input { flex: 1; }
.auto-code-wrap { display: flex; align-items: center; gap: 8rpx; }
.auto-code-label { font-size: 24rpx; color: #8a9099; white-space: nowrap; }
.auto-code-switch { width: 64rpx; height: 36rpx; border-radius: 18rpx; background: #dcdfe6; position: relative; transition: background 0.3s; }
.auto-code-switch.active { background: #1a3a5c; }
.switch-dot { position: absolute; top: 4rpx; left: 4rpx; width: 28rpx; height: 28rpx; border-radius: 14rpx; background: #fff; transition: transform 0.3s; }
.auto-code-switch.active .switch-dot { transform: translateX(28rpx); }
.form-textarea { width: 100%; min-height: 130rpx; padding: 18rpx 20rpx; border-radius: 12rpx; background: #f5f7fa; font-size: 28rpx; color: #30363d; }
.form-footer { display: flex; gap: 14rpx; padding: 20rpx 24rpx 24rpx; }
.footer-btn { flex: 1; height: 76rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.cancel-btn { background: #edf0f4; }
.confirm-btn { background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); }
.btn-text { font-size: 28rpx; font-weight: 600; color: #ffffff; }
.cancel-btn .btn-text { color: #586070; }
.loading-text, .empty-text, .finished-text { text-align: center; padding: 28rpx 0; color: #99a1aa; font-size: 26rpx; }
</style>

@ -0,0 +1,719 @@
<template>
<view class="page-container">
<NavBar title="设备详情" />
<view class="content-section">
<view class="info-card">
<view class="card-title">基本信息</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">设备名称</text>
<text class="info-value">{{ getDetailField('deviceName') }}</text>
</view>
<view class="info-row">
<text class="info-label">设备编号</text>
<text class="info-value">{{ getDetailField('deviceCode') }}</text>
</view>
<view class="info-row">
<text class="info-label">设备状态</text>
<u-tag :text="statusLabel" :type="statusTagType" size="mini" />
</view>
<view class="info-row">
<text class="info-label">设备型号</text>
<text class="info-value">{{ getDetailField('deviceModel') }}</text>
</view>
<view class="info-row">
<text class="info-label">设备规格</text>
<text class="info-value">{{ getDetailField('deviceSpec') }}</text>
</view>
<view class="info-row">
<text class="info-label">设备类型</text>
<text class="info-value">{{ getDetailField('deviceType') }}</text>
</view>
<view class="info-row">
<text class="info-label">设备位置</text>
<text class="info-value">{{ getDetailField('deviceLocation') }}</text>
</view>
<view class="info-row">
<text class="info-label">设备负责人</text>
<text class="info-value">{{ getDetailField('deviceManagerName') }}</text>
</view>
<view class="info-row">
<text class="info-label">生产日期</text>
<text class="info-value">{{ productionDateLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">入厂日期</text>
<text class="info-value">{{ factoryEntryDateLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">创建人</text>
<text class="info-value">{{ getDetailField('creatorName') }}</text>
</view>
<view class="info-row">
<text class="info-label">创建时间</text>
<text class="info-value">{{ createTimeLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">更新时间</text>
<text class="info-value">{{ updateTimeLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">备注</text>
<text class="info-value">{{ getDetailField('remark') }}</text>
</view>
</view>
</view>
<view class="info-card">
<view class="card-title">履历</view>
<view class="tabs-box">
<u-tabs activeColor="#1a3a5c" :list="tabList" :current="currentTab" :is-scroll="false" @change="handleTabChange" />
</view>
<view>
<view v-if="currentTab === 0">
<view v-if="!inspectionGroups.length" class="empty"></view>
<view v-for="group in inspectionGroups" :key="group.key" class="history-group">
<view class="history-group-head">
<text class="history-group-time">[{{ group.time }}]</text>
<text class="history-group-operator">操作人: {{ group.operator }}</text>
</view>
<view v-for="item in group.items" :key="item.key" class="history-item">
<view class="history-item-head">
<text class="result-badge" :class="'result-' + item.resultType">{{ item.resultLabel }}</text>
<text class="history-item-name">{{ item.name }}</text>
</view>
<view class="history-item-body">
<view class="history-row">
<text class="history-label">点检方式</text>
<text class="history-value">{{ detailValue(item.method) }}</text>
</view>
<view class="history-row">
<text class="history-label">判定标准</text>
<text class="history-value">{{ detailValue(item.criteria) }}</text>
</view>
<view class="history-row">
<text class="history-label">点检时间</text>
<text class="history-value">{{ detailValue(item.taskTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">创建时间</text>
<text class="history-value">{{ detailValue(item.createTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">备注</text>
<text class="history-value">{{ detailValue(item.remark) }}</text>
</view>
<view v-if="item.images && item.images.length" class="history-images">
<image v-for="img in item.images" :key="img" class="history-image" :src="img" mode="aspectFill"
@click="previewImages(item.images, img)" />
</view>
</view>
</view>
</view>
</view>
<view v-else-if="currentTab === 1">
<view v-if="!maintainGroups.length" class="empty"></view>
<view v-for="group in maintainGroups" :key="group.key" class="history-group">
<view class="history-group-head">
<text class="history-group-time">[{{ group.time }}]</text>
<text class="history-group-operator">操作人: {{ group.operator }}</text>
</view>
<view v-for="item in group.items" :key="item.key" class="history-item">
<view class="history-item-head">
<text class="result-badge" :class="'result-' + item.resultType">{{ item.resultLabel }}</text>
<text class="history-item-name">{{ item.name }}</text>
</view>
<view class="history-item-body">
<view class="history-row">
<text class="history-label">保养方式</text>
<text class="history-value">{{ detailValue(item.method) }}</text>
</view>
<view class="history-row">
<text class="history-label">判定标准</text>
<text class="history-value">{{ detailValue(item.criteria) }}</text>
</view>
<view class="history-row">
<text class="history-label">保养时间</text>
<text class="history-value">{{ detailValue(item.taskTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">创建时间</text>
<text class="history-value">{{ detailValue(item.createTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">备注</text>
<text class="history-value">{{ detailValue(item.remark) }}</text>
</view>
<view v-if="item.images && item.images.length" class="history-images">
<image v-for="img in item.images" :key="img" class="history-image" :src="img" mode="aspectFill"
@click="previewImages(item.images, img)" />
</view>
</view>
</view>
</view>
</view>
<view v-else>
<view v-if="!repairGroups.length" class="empty"></view>
<view v-for="group in repairGroups" :key="group.key" class="repair-group">
<view class="repair-group-head">
<text class="repair-group-name">{{ group.name }}</text>
<text class="repair-group-meta">{{ group.items.length }}</text>
</view>
<view v-for="row in group.items" :key="row.key" class="repair-item">
<view class="repair-item-head">
<text class="repair-tag">{{ detailValue(row.subjectCode) }}</text>
<text class="repair-title">{{ detailValue(row.subjectName) }}</text>
</view>
<view class="repair-item-body">
<view class="history-row">
<text class="history-label">项目内容</text>
<text class="history-value">{{ detailValue(row.subjectContent) }}</text>
</view>
<view class="history-row">
<text class="history-label">维修结果</text>
<text class="history-value">
<text class="result-badge" :class="'result-' + row.resultType">{{ row.resultLabel }}</text>
</text>
</view>
<view class="history-row">
<text class="history-label">备注</text>
<text class="history-value">{{ detailValue(row.remark) }}</text>
</view>
<view class="history-row">
<text class="history-label">完成日期</text>
<text class="history-value">{{ detailValue(row.finishDateLabel) }}</text>
</view>
<view v-if="row.images && row.images.length" class="history-images">
<image v-for="img in row.images" :key="img" class="history-image" :src="img" mode="aspectFill"
@click="previewImages(row.images, img)" />
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import NavBar from '@/components/common/NavBar.vue'
import {
getEquipmentDetail,
getEquipmentInspectionByDeviceId,
getEquipmentMaintenanceByDeviceId,
getEquipmentRepairListByDeviceId
} from '@/api/mes/equipment'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
const loading = ref(false)
const deviceId = ref(undefined)
const deviceCode = ref(undefined)
const equipmentType = ref(undefined)
const detailData = ref(null)
const inspectionList = ref([])
const maintainList = ref([])
const repairList = ref([])
const tabList = ref([{ name: '点检履历' }, { name: '保养履历' }, { name: '维修履历' }])
const currentTab = ref(0)
const detailFieldCandidates = {
deviceName: ['deviceName', 'name'],
deviceCode: ['deviceCode', 'code', 'deviceNo'],
deviceStatus: ['deviceStatus', 'status'],
deviceModel: ['deviceModel', 'model'],
deviceSpec: ['deviceSpec', 'spec'],
deviceType: ['deviceTypeName', 'deviceType'],
deviceLocation: ['deviceLocation', 'location'],
deviceManagerName: ['deviceManagerName', 'managerName'],
productionDate: ['productionDate'],
factoryEntryDate: ['factoryEntryDate', 'inDate', 'entryDate'],
creatorName: ['creatorName', 'creator', 'createBy', 'createUserName'],
createTime: ['createTime'],
updateTime: ['updateTime'],
remark: ['remark', 'deviceRemark']
}
function getDetailField(field) {
const d = detailData.value
const candidates = detailFieldCandidates[field]
if (d && Array.isArray(candidates)) return detailValue(pickFirst(d, candidates))
return detailValue(d ? d[field] : undefined)
}
const statusMeta = computed(() => {
const d = detailData.value
const raw = d ? pickFirst(d, detailFieldCandidates.deviceStatus) : undefined
const label = getDictLabel(DICT_TYPE.MES_TZ_STATUS, raw, detailValue(raw))
return formatStatus(label)
})
const statusLabel = computed(() => statusMeta.value.label)
const statusType = computed(() => statusMeta.value.type)
const statusTagType = computed(() => {
if (statusType.value === 'error') return 'error'
if (statusType.value === 'warning') return 'warning'
return 'success'
})
const productionDateLabel = computed(() => {
const d = detailData.value
return formatDateOnly(d ? pickFirst(d, detailFieldCandidates.productionDate) : undefined)
})
const factoryEntryDateLabel = computed(() => {
const d = detailData.value
return formatDateOnly(d ? pickFirst(d, detailFieldCandidates.factoryEntryDate) : undefined)
})
const createTimeLabel = computed(() => {
const d = detailData.value
return formatHistoryTime(d ? pickFirst(d, detailFieldCandidates.createTime) : undefined) || '-'
})
const updateTimeLabel = computed(() => {
const d = detailData.value
return formatHistoryTime(d ? pickFirst(d, detailFieldCandidates.updateTime) : undefined) || '-'
})
const inspectionGroups = computed(() =>
buildStepGroups(inspectionList.value, {
timeFieldCandidates: ['taskTime', 'inspectionTime', 'createTime'],
nameFieldCandidates: ['inspectionItemName', 'name', 'itemName'],
resultFieldCandidates: ['inspectionResult', 'result'],
methodFieldCandidates: ['inspectionMethod', 'method'],
criteriaFieldCandidates: ['judgmentCriteria', 'criteria'],
imagesFieldCandidates: ['images'],
remarkFieldCandidates: ['remark']
})
)
const maintainGroups = computed(() =>
buildStepGroups(maintainList.value, {
timeFieldCandidates: ['taskTime', 'inspectionTime', 'createTime'],
nameFieldCandidates: ['maintainItemName', 'inspectionItemName', 'name', 'itemName'],
resultFieldCandidates: ['maintainResult', 'inspectionResult', 'result'],
methodFieldCandidates: ['inspectionMethod', 'method'],
criteriaFieldCandidates: ['judgmentCriteria', 'criteria'],
imagesFieldCandidates: ['images'],
remarkFieldCandidates: ['remark']
})
)
const repairGroups = computed(() => {
const groupsMap = new Map()
const rows = Array.isArray(repairList.value) ? repairList.value : []
for (const row of rows) {
const groupKey = String(
(row && (row.repairCode || row.repairId || row.subjectName || row.id)) ? (row.repairCode || row.repairId || row.subjectName || row.id) : '-'
)
if (!groupsMap.has(groupKey)) {
groupsMap.set(groupKey, {
key: groupKey,
name: String((row && (row.repairName || row.repairCode)) ? (row.repairName || row.repairCode) : groupKey),
items: []
})
}
const resultMeta = formatResult(row ? (row.repairResult !== undefined ? row.repairResult : row.result) : undefined)
groupsMap.get(groupKey).items.push({
key: String(row && row.id !== undefined && row.id !== null ? row.id : `${groupKey}_${Math.random()}`),
subjectCode: row ? row.subjectCode : undefined,
subjectName: row ? row.subjectName : undefined,
subjectContent: row ? row.subjectContent : undefined,
remark: row ? row.remark : undefined,
finishDateLabel: formatDateOnly(row ? row.finishDate : undefined),
resultLabel: resultMeta.label,
resultType: resultMeta.type,
images: parseImages(row ? (row.malfunctionImages || row.malfunctionUrl || row.images) : undefined)
})
}
return Array.from(groupsMap.values())
})
function handleTabChange(e) {
const idx = e && typeof e === 'object' ? e.index : e
currentTab.value = Number(idx === 0 ? 0 : idx || 0)
}
onLoad((query) => {
const rawId = query && query.id
const decoded = rawId ? decodeURIComponent(String(rawId)) : ''
deviceId.value = decoded ? decoded : undefined
equipmentType.value = query && query.type
deviceCode.value = query && query.code
fetchAll()
})
async function fetchAll() {
if (!deviceId.value && !deviceCode.value) {
uni.showToast({ title: '缺少设备ID', icon: 'none' })
return
}
loading.value = true
try {
const isScan = equipmentType.value === 'scan'
const idParam = isScan ? { id: deviceId.value } : { code: deviceCode.value }
const deviceIdParam = isScan ? { deviceId: deviceId.value } : { code: deviceCode.value }
const [detailRes, inspectionRes, maintainRes, repairRes] = await Promise.all([
getEquipmentDetail(deviceId.value, idParam),
getEquipmentInspectionByDeviceId(deviceId.value, deviceIdParam),
getEquipmentMaintenanceByDeviceId(deviceId.value, deviceIdParam),
getEquipmentRepairListByDeviceId(deviceId.value, deviceIdParam)
])
detailData.value = normalizeDetail(detailRes)
inspectionList.value = normalizeList(inspectionRes)
maintainList.value = normalizeList(maintainRes)
repairList.value = normalizeList(repairRes)
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
function normalizeDetail(res) {
const data = res && res.data !== undefined ? res.data : res
if (data && !Array.isArray(data) && data.data && !Array.isArray(data.data)) return data.data
if (data && !Array.isArray(data)) return data
return null
}
function normalizeList(res) {
const data = res && res.data !== undefined ? res.data : res
if (Array.isArray(data)) return data
if (data && Array.isArray(data.data)) return data.data
if (data && data.data && Array.isArray(data.data.list)) return data.data.list
if (data && data.data && Array.isArray(data.data.rows)) return data.data.rows
if (data && data.data && Array.isArray(data.data.records)) return data.data.records
if (data && Array.isArray(data.list)) return data.list
if (data && Array.isArray(data.rows)) return data.rows
if (data && Array.isArray(data.records)) return data.records
return []
}
function detailValue(v) {
if (v === 0) return '0'
if (v === false) return '否'
if (v === true) return '是'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s ? s : '-'
}
function formatStatus(v) {
const raw = v === null || v === undefined ? '' : String(v).trim()
const upper = raw.toUpperCase()
if (!raw) return { label: '-', type: 'normal' }
if (raw === '1' || raw === '0' || upper === 'OK' || raw.includes('正常') || raw.includes('运行')) return { label: raw, type: 'normal' }
if (raw === '2' || upper === 'NG' || raw.includes('停') || raw.includes('禁') || raw.includes('坏') || raw.includes('修'))
return { label: raw, type: 'error' }
return { label: raw, type: 'warning' }
}
function formatResult(v) {
const raw = v === null || v === undefined ? '' : String(v).trim()
const upper = raw.toUpperCase()
if (!raw) return { label: '-', type: 'info' }
if (raw === '0') return { label: '待检测', type: 'info' }
if (raw === '1' || upper === 'OK') return { label: '通过', type: 'success' }
if (raw === '2' || upper === 'NG') return { label: '不通过', type: 'danger' }
return { label: raw, type: 'info' }
}
function formatHistoryTime(value) {
if (!value) return ''
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d, hh, mm, ss] = value
const pad = (n) => String(n).padStart(2, '0')
if (hh !== undefined) return `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}:${pad(ss)}`
return `${y}-${pad(m)}-${pad(d)}`
}
const s = String(value).trim()
if (!s) return ''
const num = Number(s)
if (Number.isFinite(num)) {
const ms = s.length === 10 ? num * 1000 : num
const d = new Date(ms)
if (!Number.isNaN(d.getTime())) return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
}
const d = new Date(s)
if (!Number.isNaN(d.getTime())) return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
return s
}
function formatDateOnly(value) {
const t = formatHistoryTime(value)
if (!t) return '-'
return String(t).split(' ')[0]
}
function parseImages(value) {
if (!value) return []
if (Array.isArray(value)) return value.map(String).filter(Boolean)
const cleaned = String(value).replace(/[`'"]/g, '').trim()
return cleaned
.split(',')
.map((v) => v.trim())
.filter(Boolean)
}
function pickFirst(obj, keys) {
for (const k of keys) {
if (obj && obj[k] !== undefined && obj[k] !== null && String(obj[k]).trim() !== '') return obj[k]
}
return undefined
}
function buildStepGroups(rows, options) {
const groupsMap = new Map()
const list = Array.isArray(rows) ? rows : []
for (const row of list) {
const time = formatHistoryTime(pickFirst(row, options.timeFieldCandidates) || (row ? row.createTime : undefined))
const operator = detailValue(row ? (row.operator || row.creatorName || row.creator) : undefined)
const managementId = row && row.managementId !== undefined && row.managementId !== null ? row.managementId : ''
const groupKey = `${managementId}__${time}__${operator}`
const name = pickFirst(row, options.nameFieldCandidates) || '-'
const resultMeta = formatResult(pickFirst(row, options.resultFieldCandidates))
const item = {
key: String(row && row.id !== undefined && row.id !== null ? row.id : `${groupKey}_${String(name)}`),
name: detailValue(name),
resultLabel: resultMeta.label,
resultType: resultMeta.type,
method: pickFirst(row, options.methodFieldCandidates),
criteria: pickFirst(row, options.criteriaFieldCandidates),
remark: pickFirst(row, options.remarkFieldCandidates),
images: parseImages(pickFirst(row, options.imagesFieldCandidates)),
taskTimeLabel: formatHistoryTime(row ? (row.taskTime || row.inspectionTime) : undefined),
createTimeLabel: formatHistoryTime(row ? row.createTime : undefined)
}
if (!groupsMap.has(groupKey)) {
groupsMap.set(groupKey, { key: groupKey, time: time || '-', operator, items: [item] })
} else {
groupsMap.get(groupKey).items.push(item)
}
}
return Array.from(groupsMap.values())
}
function previewImages(list, current) {
if (!list || !list.length) return
uni.previewImage({ urls: list, current })
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f0f2f5;
}
.content-section {
padding: 0 30rpx 30rpx;
}
.info-card {
background: #ffffff;
border-radius: 20rpx;
padding: 30rpx;
margin-top: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 24rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #f0f2f5;
}
.info-list {
display: flex;
flex-direction: column;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f7fa;
&:last-child {
border-bottom: none;
}
}
.info-label {
font-size: 28rpx;
color: #999999;
}
.info-value {
font-size: 28rpx;
color: #333333;
}
.tabs-box {
margin-bottom: 24rpx;
}
.empty {
padding: 30rpx 0;
text-align: center;
color: #999;
font-size: 26rpx;
}
.history-group {
margin-bottom: 24rpx;
}
.history-group-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.history-group-time {
font-size: 24rpx;
color: #1a3a5c;
}
.history-group-operator {
font-size: 24rpx;
color: #666;
}
.history-item {
border: 1rpx solid #eef1f5;
border-radius: 12rpx;
padding: 16rpx;
margin-bottom: 12rpx;
}
.history-item-head {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
}
.history-item-name {
font-size: 28rpx;
color: #333;
font-weight: 600;
}
.history-item-body {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.history-row {
display: flex;
justify-content: space-between;
gap: 20rpx;
}
.history-label {
color: #999;
font-size: 24rpx;
}
.history-value {
color: #333;
font-size: 24rpx;
text-align: right;
}
.history-images {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 10rpx;
}
.history-image {
width: 120rpx;
height: 120rpx;
border-radius: 10rpx;
}
.result-badge {
font-size: 22rpx;
padding: 4rpx 10rpx;
border-radius: 8rpx;
}
.result-info {
background: rgba(144, 147, 153, 0.15);
color: #606266;
}
.result-success {
background: rgba(24, 188, 55, 0.15);
color: #18bc37;
}
.result-danger {
background: rgba(255, 77, 79, 0.15);
color: #ff4d4f;
}
.repair-group {
margin-bottom: 20rpx;
}
.repair-group-head {
display: flex;
justify-content: space-between;
margin-bottom: 12rpx;
}
.repair-group-name {
font-size: 28rpx;
color: #1a3a5c;
font-weight: 600;
}
.repair-group-meta {
font-size: 24rpx;
color: #999;
}
.repair-item {
border: 1rpx solid #eef1f5;
border-radius: 12rpx;
padding: 16rpx;
margin-bottom: 12rpx;
}
.repair-item-head {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
}
.repair-tag {
font-size: 22rpx;
color: #666;
background: #f5f7fa;
border-radius: 8rpx;
padding: 4rpx 10rpx;
}
.repair-title {
font-size: 28rpx;
color: #333;
font-weight: 600;
}
</style>

@ -0,0 +1,374 @@
<template>
<view class="page-container">
<NavBar title="设备查询" subTitle="请选择查询方式" />
<view class="content-section">
<view class="scan-section">
<view class="scan-area" @click="startScan">
<view class="scan-icon">
<text class="icon-text">📷</text>
</view>
<text class="scan-title">自动扫描</text>
<text class="scan-desc">点击启动扫描设备编码</text>
</view>
<view v-if="isScanning" class="scanning-overlay">
<view class="scanning-animation">
<view class="scan-line"></view>
<view class="scan-corners">
<view class="corner corner-tl"></view>
<view class="corner corner-tr"></view>
<view class="corner corner-bl"></view>
<view class="corner corner-br"></view>
</view>
</view>
<text class="scanning-text">正在打开扫码...</text>
</view>
</view>
<view class="divider">
<view class="divider-line"></view>
<text class="divider-text"></text>
<view class="divider-line"></view>
</view>
<view class="input-section">
<view class="input-label">手动输入设备编码</view>
<view class="input-wrapper">
<input
v-model="equipmentCode"
class="code-input"
type="text"
placeholder="请输入设备编码"
placeholder-class="input-placeholder"
/>
</view>
<view class="confirm-btn" :class="{ active: equipmentCode.length > 0 }" @click="confirmInput">
<text class="btn-text">确定</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import NavBar from '@/components/common/NavBar.vue'
const equipmentCode = ref('');
const isScanning = ref(false);
const SCAN_TYPE = 'EQUIPMENT';
const MODULE_NAME = '设备';
function parseScanResult(res) {
const raw = String(res?.result || '').trim();
if (!raw) return {};
const splitIndex = raw.indexOf('-');
if (splitIndex <= 0 || splitIndex >= raw.length - 1) return {};
return {
type: raw.slice(0, splitIndex),
id: raw.slice(splitIndex + 1)
};
}
function startScan() {
if (isScanning.value) return;
isScanning.value = true;
const finish = () => {
isScanning.value = false;
}
uni.scanCode({
onlyFromCamera: true,
scanType: ['qrCode', 'barCode'],
success: (res) => {
const { type, id } = parseScanResult(res);
if (!type || !id) {
uni.showToast({ title: '未获取到扫码结果', icon: 'none' })
return
}
if (type !== SCAN_TYPE) {
uni.showToast({ title: `二维码类型不匹配,请扫描${MODULE_NAME}二维码`, icon: 'none' })
return
}
navigateToDetail(id)
},
fail: (err) => {
const msg = String(err?.errMsg || '')
if (msg.includes('cancel')) {
uni.showToast({ title: '已取消扫码', icon: 'none' })
return
}
if (msg.toLowerCase().includes('not support') || msg.toLowerCase().includes('not supported')) {
uni.showToast({ title: '当前平台不支持扫码', icon: 'none' })
return
}
uni.showToast({ title: '扫码失败', icon: 'none' })
},
complete: finish
})
}
function confirmInput() {
if (equipmentCode.value.trim().length === 0) {
uni.showToast({
title: '请输入设备编码',
icon: 'none'
});
return;
}
navigateToDetail(equipmentCode.value.trim(), 'input');
}
function navigateToDetail(code, type = 'scan') {
let url
if (type === 'scan') {
url = `/pages_function/pages/equipment/detail?id=${encodeURIComponent(code)}&type=${type}`
} else {
url = `/pages_function/pages/equipment/detail?code=${code}&type=${type}`
}
uni.navigateTo({
url: url
});
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f0f2f5;
}
.content-section {
padding: 40rpx 30rpx;
}
.scan-section {
position: relative;
background: #ffffff;
border-radius: 24rpx;
padding: 60rpx 40rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.scan-area {
display: flex;
flex-direction: column;
align-items: center;
&:active {
opacity: 0.8;
}
}
.scan-icon {
width: 160rpx;
height: 160rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #3d7ab5 100%);
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30rpx;
.icon-text {
font-size: 72rpx;
}
}
.scan-title {
font-size: 34rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 12rpx;
}
.scan-desc {
font-size: 26rpx;
color: #999999;
}
.scanning-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(26, 58, 92, 0.95);
border-radius: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
}
.scanning-animation {
width: 300rpx;
height: 300rpx;
position: relative;
margin-bottom: 40rpx;
}
.scan-line {
position: absolute;
top: 0;
left: 20rpx;
right: 20rpx;
height: 4rpx;
background: linear-gradient(90deg, transparent, #ff8c00, transparent);
animation: scanMove 1.5s ease-in-out infinite;
}
@keyframes scanMove {
0% {
top: 20rpx;
}
50% {
top: 260rpx;
}
100% {
top: 20rpx;
}
}
.scan-corners {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.corner {
position: absolute;
width: 40rpx;
height: 40rpx;
border-color: #ff8c00;
border-style: solid;
border-width: 0;
}
.corner-tl {
top: 0;
left: 0;
border-top-width: 6rpx;
border-left-width: 6rpx;
border-top-left-radius: 12rpx;
}
.corner-tr {
top: 0;
right: 0;
border-top-width: 6rpx;
border-right-width: 6rpx;
border-top-right-radius: 12rpx;
}
.corner-bl {
bottom: 0;
left: 0;
border-bottom-width: 6rpx;
border-left-width: 6rpx;
border-bottom-left-radius: 12rpx;
}
.corner-br {
bottom: 0;
right: 0;
border-bottom-width: 6rpx;
border-right-width: 6rpx;
border-bottom-right-radius: 12rpx;
}
.scanning-text {
font-size: 32rpx;
color: #ffffff;
margin-bottom: 30rpx;
}
.divider {
display: flex;
align-items: center;
margin-bottom: 30rpx;
.divider-line {
flex: 1;
height: 2rpx;
background: #e8eaed;
}
.divider-text {
padding: 0 30rpx;
font-size: 28rpx;
color: #999999;
}
}
.input-section {
background: #ffffff;
border-radius: 24rpx;
padding: 40rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.input-label {
font-size: 30rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 24rpx;
}
.input-wrapper {
margin-bottom: 30rpx;
}
.code-input {
width: 100%;
height: 96rpx;
background: #f5f7fa;
border-radius: 16rpx;
padding: 0 30rpx;
font-size: 30rpx;
color: #333333;
border: 2rpx solid transparent;
&:focus {
border-color: #1a3a5c;
background: #ffffff;
}
}
.input-placeholder {
color: #c0c4cc;
}
.confirm-btn {
height: 96rpx;
background: #c0c4cc;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&.active {
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
&:active {
opacity: 0.8;
transform: scale(0.98);
}
}
.btn-text {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
}
}
</style>

@ -0,0 +1,162 @@
<template>
<view class="page-container">
<view class="fixed-header">
<NavBar :title="t('equipmentCategory.detailTitle')" />
</view>
<scroll-view scroll-y class="detail-scroll">
<view class="content-section">
<!-- 基础信息 -->
<view class="info-card">
<view class="card-title">{{ t('equipmentCategory.basicInfo') }}</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">{{ t('equipmentCategory.code') }}</text>
<text class="info-value">{{ fieldValue('code') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentCategory.name') }}</text>
<text class="info-value">{{ fieldValue('name') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentCategory.parentName') }}</text>
<text class="info-value">{{ parentNameText }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentCategory.sort') }}</text>
<text class="info-value">{{ fieldValue('sort') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentCategory.createTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData?.createTime) }}</text>
</view>
<view class="info-row remark-row">
<text class="info-label">{{ t('equipmentCategory.remark') }}</text>
<text class="info-value remark-value">{{ fieldValue('remark') }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getDeviceType, getDeviceTypeTree } from '@/api/mes/deviceType'
const { t } = useI18n()
const detailId = ref(undefined)
const detailData = ref(null)
const allCategories = ref([])
const parentNameText = computed(() => {
const parentId = detailData.value?.parentId
if (!parentId || parentId === 0) return t('equipmentCategory.rootCategory')
const parent = allCategories.value.find(c => c.id === parentId)
return parent ? parent.name : t('equipmentCategory.rootCategory')
})
onLoad(async (query) => {
const id = query?.id ? decodeURIComponent(String(query.id)) : ''
detailId.value = id || undefined
await fetchCategoryList()
await fetchDetail()
})
async function fetchCategoryList() {
try {
const res = await getDeviceTypeTree({})
const root = res && res.data !== undefined ? res.data : res
const treeData = Array.isArray(root) ? root : (Array.isArray(root?.data) ? root.data : [])
allCategories.value = flattenTree(treeData)
} catch (e) {}
}
function flattenTree(nodes) {
const result = []
nodes.forEach(node => {
result.push(node)
if (node.children && node.children.length) {
result.push(...flattenTree(node.children))
}
})
return result
}
async function fetchDetail() {
if (!detailId.value) {
uni.showToast({ title: t('equipmentCategory.noId'), icon: 'none' })
return
}
try {
const res = await getDeviceType(detailId.value)
detailData.value = normalizeDetail(res)
} catch (e) {
uni.showToast({ title: t('equipmentCategory.loadFailed'), icon: 'none' })
}
}
function normalizeDetail(res) {
const root = res && res.data !== undefined ? res.data : res
if (root?.data && typeof root.data === 'object') return root.data
if (root && typeof root === 'object') return root
return {}
}
function fieldValue(field) {
return textValue(detailData.value ? detailData.value[field] : undefined)
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return t('functionCommon.no')
if (value === true) return t('functionCommon.yes')
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function formatDateTime(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = value
const pad = (n) => String(n).padStart(2, '0')
return `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}:${pad(ss)}`
}
const text = String(value).trim()
if (!text) return '-'
const numeric = Number(text)
if (Number.isFinite(numeric)) {
const timestamp = text.length === 10 ? numeric * 1000 : numeric
const date = new Date(timestamp)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
}
const date = new Date(text)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
return text
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.fixed-header { position: sticky; top: 0; z-index: 10; }
.detail-scroll { height: calc(100vh - 120rpx); }
.content-section { padding: 0 24rpx 24rpx; }
.info-card { margin-top: 20rpx; background: #ffffff; border-radius: 20rpx; padding: 28rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); }
.card-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; margin-bottom: 18rpx; }
.info-list { background: #ffffff; }
.info-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 18rpx 0; border-bottom: 1rpx solid #edf0f3; }
.info-row:last-child { border-bottom: none; }
.info-label { font-size: 27rpx; color: #8a9099; width: 220rpx; }
.info-value { flex: 1; text-align: right; font-size: 28rpx; color: #30363d; line-height: 1.45; }
.remark-row { border-bottom: none; }
.remark-value { white-space: pre-wrap; }
</style>

@ -0,0 +1,418 @@
<template>
<view class="page-container">
<NavBar :title="t('equipmentCategory.moduleName')" :subTitle="t('equipmentCategory.subTitle')" />
<!-- 搜索区域 -->
<view class="search-card">
<view class="search-row">
<view class="search-input-wrap">
<text class="iconfont icon-search search-icon"></text>
<input v-model="searchKeyword" class="search-input" :placeholder="t('equipmentCategory.searchPlaceholder')" @confirm="handleSearch" />
</view>
<view class="search-btn" @click="handleSearch">{{ t('functionCommon.search') }}</view>
</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 filteredList" :key="item.id" class="type-card" @click="openDetail(item)">
<view class="card-header">
<view class="header-left">
<text class="type-name">{{ textValue(item.name) }}</text>
<text class="type-code">{{ t('equipmentCategory.code') }}: {{ textValue(item.code) }}</text>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('equipmentCategory.parentName') }}</text>
<text class="value">{{ getParentName(item.parentId) }}</text>
</view>
<view class="row">
<text class="label">{{ t('equipmentCategory.sort') }}</text>
<text class="value">{{ textValue(item.sort) }}</text>
</view>
<view class="row">
<text class="label">{{ t('equipmentCategory.remark') }}</text>
<text class="value">{{ textValue(item.remark) }}</text>
</view>
</view>
<view class="card-actions">
<view class="action-btn edit-btn" @click.stop="openEdit(item)">
<uni-icons type="compose" size="18" color="#ffffff"></uni-icons>
</view>
<view class="action-btn delete-btn" @click.stop="confirmDelete(item)">
<uni-icons type="trash" size="18" color="#ffffff"></uni-icons>
</view>
</view>
</view>
<view v-if="loading" class="loading-text">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!filteredList.length" class="empty-text">{{ t('equipmentCategory.empty') }}</view>
<view v-else-if="finished" class="finished-text">{{ t('functionCommon.noMore') }}</view>
</view>
</scroll-view>
<!-- 新增悬浮按钮 -->
<view class="add-btn" @click="openCreate">
<text class="add-icon">+</text>
</view>
<!-- 返回顶部 -->
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<text class="go-top-icon"></text>
</view>
<!-- 新增/编辑弹框 -->
<uni-popup ref="formPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ formMode === 'create' ? t('equipmentCategory.createTitle') : t('equipmentCategory.editTitle') }}</text>
<view class="popup-close" @click="closeForm">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="form-scroll">
<view class="form-content">
<view class="form-item">
<text class="form-label">{{ t('equipmentCategory.code') }} <text class="required-star">*</text></text>
<input v-model="formData.code" class="form-input" type="text" :placeholder="t('equipmentCategory.placeholderCode')" :disabled="formMode === 'update'" />
</view>
<view class="form-item">
<text class="form-label">{{ t('equipmentCategory.name') }} <text class="required-star">*</text></text>
<input v-model="formData.name" class="form-input" type="text" :placeholder="t('equipmentCategory.placeholderName')" />
</view>
<view class="form-item">
<text class="form-label">{{ t('equipmentCategory.parentName') }}</text>
<picker :range="parentCategoryLabels" @change="onParentChange">
<view class="form-picker">
<text :class="['picker-text', formData.parentId === undefined || formData.parentId === 0 ? 'placeholder' : '']">{{ selectedParentLabel }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">{{ t('equipmentCategory.sort') }}</text>
<input v-model="formData.sort" class="form-input" type="number" :placeholder="t('equipmentCategory.placeholderSort')" />
</view>
<view class="form-item">
<text class="form-label">{{ t('equipmentCategory.remark') }}</text>
<textarea v-model="formData.remark" class="form-textarea" :placeholder="t('equipmentCategory.placeholderRemark')" :maxlength="200" />
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeForm">
<text class="btn-text">{{ t('functionCommon.cancel') }}</text>
</view>
<view class="footer-btn confirm-btn" @click="submitForm">
<text class="btn-text">{{ t('functionCommon.save') }}</text>
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getDeviceTypeTree, getDeviceType, createDeviceType, updateDeviceType, deleteDeviceType } from '@/api/mes/deviceType'
const { t } = useI18n()
const formPopupRef = ref(null)
const searchKeyword = ref('')
const list = ref([])
const allCategories = ref([])
const loading = ref(false)
const finished = ref(false)
const scrollTop = ref(0)
const showGoTop = ref(false)
const formMode = ref('create')
const formData = reactive({
id: undefined,
code: '',
name: '',
parentId: 0,
sort: '',
remark: ''
})
onLoad(async () => {
await fetchList()
})
async function fetchList() {
loading.value = true
try {
const res = await getDeviceTypeTree({
code: searchKeyword.value.trim() || undefined,
name: searchKeyword.value.trim() || undefined
})
const root = res && res.data !== undefined ? res.data : res
const treeData = Array.isArray(root) ? root : (Array.isArray(root?.data) ? root.data : [])
allCategories.value = flattenTree(buildTree(treeData))
list.value = allCategories.value
finished.value = true
} catch (e) {
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
} finally {
loading.value = false
}
}
function buildTree(data) {
const map = {}
const roots = []
data.forEach(item => { map[item.id] = { ...item, children: [] } })
data.forEach(item => {
if (item.parentId && map[item.parentId]) {
map[item.parentId].children.push(map[item.id])
} else {
roots.push(map[item.id])
}
})
return roots
}
function flattenTree(nodes, depth = 0) {
const result = []
nodes.forEach(node => {
result.push({ ...node, _depth: depth })
if (node.children && node.children.length) {
result.push(...flattenTree(node.children, depth + 1))
}
})
return result
}
const filteredList = computed(() => {
if (!searchKeyword.value.trim()) return list.value
const kw = searchKeyword.value.trim().toLowerCase()
return list.value.filter(item =>
(item.name && item.name.toLowerCase().includes(kw)) ||
(item.code && item.code.toLowerCase().includes(kw))
)
})
function getParentName(parentId) {
if (!parentId || parentId === 0) return t('equipmentCategory.rootCategory')
const parent = allCategories.value.find(c => c.id === parentId)
return parent ? parent.name : t('equipmentCategory.rootCategory')
}
const parentCategoryOptions = computed(() => {
const root = [{ label: t('equipmentCategory.rootCategory'), value: 0 }]
allCategories.value.forEach(item => {
root.push({ label: item.name || '', value: item.id })
})
return root
})
const parentCategoryLabels = computed(() => parentCategoryOptions.value.map(o => o.label))
const selectedParentLabel = computed(() => {
const opt = parentCategoryOptions.value.find(o => o.value === formData.parentId)
return opt ? opt.label : t('equipmentCategory.placeholderParent')
})
function onParentChange(e) {
const idx = e.detail.value
formData.parentId = parentCategoryOptions.value[idx]?.value ?? 0
}
async function handleSearch() {
await fetchList()
}
function onScroll(e) {
const top = e?.detail?.scrollTop || 0
showGoTop.value = top > 600
}
function goTop() {
scrollTop.value = 0
}
function loadMore() {}
function openCreate() {
formMode.value = 'create'
resetForm()
formPopupRef.value?.open()
}
async function openEdit(item) {
const id = item?.id
if (id === undefined || id === null) {
uni.showToast({ title: t('equipmentCategory.noId'), icon: 'none' })
return
}
try {
const res = await getDeviceType(id)
const detail = normalizeDetail(res)
formMode.value = 'update'
formData.id = detail?.id
formData.code = textValueForInput(detail?.code)
formData.name = textValueForInput(detail?.name)
formData.parentId = detail?.parentId ?? 0
formData.sort = textValueForInput(detail?.sort)
formData.remark = textValueForInput(detail?.remark)
formPopupRef.value?.open()
} catch (e) {
uni.showToast({ title: t('equipmentCategory.loadEditFailed'), icon: 'none' })
}
}
function confirmDelete(item) {
const id = item?.id
if (id === undefined || id === null) return
uni.showModal({
title: t('functionCommon.confirmTitle'),
content: t('equipmentCategory.confirmDeleteContent', { name: textValue(item?.name) }),
success: async (res) => {
if (!res.confirm) return
try {
await deleteDeviceType(id)
uni.showToast({ title: t('functionCommon.deleteSuccess'), icon: 'success' })
await fetchList()
} catch (e) {
uni.showToast({ title: t('functionCommon.deleteFailed'), icon: 'none' })
}
}
})
}
function closeForm() {
formPopupRef.value?.close()
}
async function submitForm() {
if (!formData.code.trim()) {
uni.showToast({ title: t('equipmentCategory.validatorCodeRequired'), icon: 'none' })
return
}
if (!formData.name.trim()) {
uni.showToast({ title: t('equipmentCategory.validatorNameRequired'), icon: 'none' })
return
}
const payload = {
id: formMode.value === 'update' ? formData.id : undefined,
code: formData.code.trim(),
name: formData.name.trim(),
parentId: formData.parentId || 0,
sort: toNumberOrUndefined(formData.sort),
remark: formData.remark.trim() || undefined
}
try {
if (formMode.value === 'create') {
await createDeviceType(payload)
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
} else {
await updateDeviceType(payload)
uni.showToast({ title: t('functionCommon.updateSuccess'), icon: 'success' })
}
closeForm()
await fetchList()
} catch (e) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
}
}
function resetForm() {
formData.id = undefined
formData.code = ''
formData.name = ''
formData.parentId = 0
formData.sort = ''
formData.remark = ''
}
function openDetail(item) {
const id = item?.id
if (!id && id !== 0) return
uni.navigateTo({
url: `/pages_function/pages/equipmentCategory/detail?id=${encodeURIComponent(String(id))}`
})
}
function normalizeDetail(res) {
const root = res && res.data !== undefined ? res.data : res
if (root?.data && typeof root.data === 'object') return root.data
if (root && typeof root === 'object') return root
return {}
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return t('functionCommon.no')
if (value === true) return t('functionCommon.yes')
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function textValueForInput(value) {
if (value === null || value === undefined) return ''
return String(value)
}
function toNumberOrUndefined(value) {
if (value === null || value === undefined || String(value).trim() === '') return undefined
const num = Number(value)
return Number.isFinite(num) ? num : undefined
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.search-card { background: #ffffff; margin: 20rpx 24rpx; border-radius: 18rpx; padding: 20rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.04); }
.search-row { display: flex; align-items: center; gap: 16rpx; }
.search-input-wrap { flex: 1; display: flex; align-items: center; background: #f5f7fa; border-radius: 44rpx; padding: 0 20rpx; }
.search-icon { margin-right: 12rpx; font-size: 30rpx; color: #999; }
.search-input { flex: 1; height: 72rpx; font-size: 28rpx; color: #333; background: transparent; }
.search-btn { min-width: 120rpx; height: 72rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); border-radius: 14rpx; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 28rpx; font-weight: 600; }
.list-scroll { height: calc(100vh - 360rpx); }
.list-wrap { padding: 0 24rpx 30rpx; }
.type-card { background: #ffffff; border-radius: 18rpx; padding: 24rpx; margin-bottom: 18rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.05); }
.card-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 18rpx; border-bottom: 1rpx solid #edf0f3; }
.header-left { display: flex; flex-direction: column; }
.type-name { font-size: 32rpx; font-weight: 600; color: #1a3a5c; margin-bottom: 8rpx; }
.type-code { font-size: 24rpx; color: #8a9099; }
.card-body { padding-top: 16rpx; }
.row { display: flex; justify-content: space-between; align-items: center; margin-top: 12rpx; }
.label { font-size: 26rpx; color: #8a9099; }
.value { font-size: 27rpx; color: #30363d; max-width: 62%; text-align: right; }
.card-actions { margin-top: 24rpx; display: flex; justify-content: flex-end; gap: 14rpx; }
.action-btn { width: 60rpx; height: 60rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.edit-btn { background: #1a3a5c; }
.delete-btn { background: #ff4d4f; }
.add-btn { position: fixed; right: 28rpx; bottom: 140rpx; width: 96rpx; height: 96rpx; border-radius: 48rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); display: flex; align-items: center; justify-content: center; box-shadow: 0 8rpx 24rpx rgba(26, 58, 92, 0.32); z-index: 99; }
.add-icon { color: #ffffff; font-size: 56rpx; line-height: 1; }
.go-top-btn { position: fixed; right: 28rpx; bottom: 254rpx; width: 88rpx; height: 88rpx; border-radius: 44rpx; background: rgba(26, 58, 92, 0.9); display: flex; align-items: center; justify-content: center; z-index: 99; box-shadow: 0 6rpx 16rpx rgba(26, 58, 92, 0.25); }
.go-top-icon { color: #ffffff; font-size: 36rpx; font-weight: 700; }
.popup-content { width: 680rpx; max-height: 80vh; border-radius: 20rpx; overflow: hidden; }
.popup-header { padding: 24rpx; display: flex; align-items: center; justify-content: space-between; border-bottom: 1rpx solid #edf0f3; }
.popup-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; }
.close-icon { font-size: 38rpx; color: #8e95a0; }
.form-scroll { max-height: 56vh; }
.form-content { padding: 24rpx; }
.form-item { margin-bottom: 18rpx; }
.form-label { display: block; margin-bottom: 8rpx; font-size: 26rpx; color: #8a9099; }
.required-star { color: #ff4d4f; }
.form-input { height: 76rpx; padding: 0 20rpx; border-radius: 12rpx; background: #f5f7fa; font-size: 28rpx; color: #30363d; }
.form-picker { height: 76rpx; padding: 0 20rpx; border-radius: 12rpx; background: #f5f7fa; display: flex; align-items: center; justify-content: space-between; }
.picker-text { font-size: 28rpx; color: #30363d; }
.picker-text.placeholder { color: #999; }
.picker-arrow { font-size: 30rpx; color: #999; }
.form-textarea { width: 100%; min-height: 130rpx; padding: 18rpx 20rpx; border-radius: 12rpx; background: #f5f7fa; font-size: 28rpx; color: #30363d; }
.form-footer { display: flex; gap: 14rpx; padding: 20rpx 24rpx 24rpx; }
.footer-btn { flex: 1; height: 76rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.cancel-btn { background: #edf0f4; }
.confirm-btn { background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); }
.btn-text { font-size: 28rpx; font-weight: 600; color: #ffffff; }
.cancel-btn .btn-text { color: #586070; }
.loading-text, .empty-text, .finished-text { text-align: center; padding: 28rpx 0; color: #99a1aa; font-size: 26rpx; }
</style>

@ -0,0 +1,380 @@
<template>
<view class="page-container">
<view class="fixed-header">
<NavBar :title="t('equipmentLedger.detailTitle')" />
</view>
<scroll-view scroll-y class="detail-scroll">
<view class="content-section">
<!-- 基础信息 -->
<view class="info-card">
<view class="card-title">{{ t('equipmentLedger.basicInfo') }}</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">{{ t('equipmentLedger.deviceCode') }}</text>
<text class="info-value">{{ fieldValue('deviceCode') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentLedger.deviceName') }}</text>
<text class="info-value">{{ fieldValue('deviceName') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentLedger.deviceStatus') }}</text>
<text :class="['info-value', getStatusClass(detailData?.deviceStatus)]">{{ getStatusText(detailData?.deviceStatus) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentLedger.deviceType') }}</text>
<text class="info-value">{{ fieldValue('deviceTypeName') || getDeviceTypeNameById(detailData?.deviceType) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentLedger.deviceSpec') }}</text>
<text class="info-value">{{ fieldValue('deviceSpec') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentLedger.isScheduled') }}</text>
<text class="info-value">{{ scheduledText }}</text>
</view>
<view v-if="detailData?.isScheduled === 1 || detailData?.isSchedueld === 1" class="info-row">
<text class="info-label">{{ t('equipmentLedger.ratedCapacity') }}</text>
<text class="info-value">{{ fieldValue('ratedCapacity') }}</text>
</view>
<view v-if="detailData?.isScheduled === 1 || detailData?.isSchedueld === 1" class="info-row">
<text class="info-label">{{ t('equipmentLedger.dailyAverageValue') }}</text>
<text class="info-value">{{ fieldValue('dailyAverageValue') }}</text>
</view>
<view v-if="detailData?.isScheduled === 1 || detailData?.isSchedueld === 1" class="info-row">
<text class="info-label">{{ t('equipmentLedger.dataCollectionCapacity') }}</text>
<text class="info-value">{{ fieldValue('dataCollectionCapacity') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentLedger.productionDate') }}</text>
<text class="info-value">{{ formatDateValue(detailData?.productionDate) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentLedger.factoryEntryDate') }}</text>
<text class="info-value">{{ formatDateValue(detailData?.factoryEntryDate) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentLedger.deviceLocation') }}</text>
<text class="info-value">{{ fieldValue('deviceLocation') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentLedger.deviceManagerName') }}</text>
<text class="info-value">{{ fieldValue('deviceManagerName') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentLedger.workshop') }}</text>
<text class="info-value">{{ fieldValue('workshopName') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentLedger.creatorName') }}</text>
<text class="info-value">{{ fieldValue('creatorName') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('equipmentLedger.createTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData?.createTime) }}</text>
</view>
<view class="info-row remark-row">
<text class="info-label">{{ t('equipmentLedger.remark') }}</text>
<text class="info-value remark-value">{{ fieldValue('remark') }}</text>
</view>
</view>
</view>
<!-- 历史记录Tabs -->
<view class="info-card">
<view class="tab-bar">
<view
v-for="tab in tabs"
:key="tab.key"
:class="['tab-item', activeTab === tab.key ? 'active' : '']"
@click="switchTab(tab.key)"
>
<text class="tab-text">{{ tab.label }}</text>
</view>
</view>
<!-- 点检历史 -->
<view v-if="activeTab === 'check'" class="tab-content">
<view v-if="!inspectionList.length" class="empty-tip">{{ t('equipmentLedger.noHistoryData') }}</view>
<view v-for="(item, idx) in inspectionList" :key="idx" class="history-item">
<view class="history-header">
<text class="history-time">{{ formatDateTime(item.inspectionTime) }}</text>
<text :class="['history-result', getResultClass(item.inspectionResult)]">{{ getResultText(item.inspectionResult) }}</text>
</view>
<view class="history-body">
<text class="history-name">{{ item.inspectionItemName || item.name || '-' }}</text>
<text class="history-operator">{{ t('equipmentLedger.operator') }}: {{ item.operatorName || item.inspectorName || '-' }}</text>
</view>
</view>
</view>
<!-- 保养历史 -->
<view v-if="activeTab === 'maintain'" class="tab-content">
<view v-if="!maintainList.length" class="empty-tip">{{ t('equipmentLedger.noHistoryData') }}</view>
<view v-for="(item, idx) in maintainList" :key="idx" class="history-item">
<view class="history-header">
<text class="history-time">{{ formatDateTime(item.maintainTime || item.inspectionTime) }}</text>
<text :class="['history-result', getResultClass(item.maintainResult || item.inspectionResult)]">{{ getResultText(item.maintainResult || item.inspectionResult) }}</text>
</view>
<view class="history-body">
<text class="history-name">{{ item.maintainItemName || item.inspectionItemName || item.name || '-' }}</text>
<text class="history-operator">{{ t('equipmentLedger.operator') }}: {{ item.operatorName || item.inspectorName || '-' }}</text>
</view>
</view>
</view>
<!-- 报修历史 -->
<view v-if="activeTab === 'repair'" class="tab-content">
<view v-if="!repairList.length" class="empty-tip">{{ t('equipmentLedger.noHistoryData') }}</view>
<view v-for="(item, idx) in repairList" :key="idx" class="history-item">
<view class="history-header">
<text class="history-time">{{ formatDateTime(item.createTime) }}</text>
<text :class="['history-result', getRepairStatusClass(item.status)]">{{ getRepairStatusText(item.status) }}</text>
</view>
<view class="history-body">
<text class="history-name">{{ item.repairNo || item.description || '-' }}</text>
<text class="history-operator">{{ t('equipmentLedger.operator') }}: {{ item.creatorName || '-' }}</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getDeviceLedger } from '@/api/mes/deviceLedger'
import { getDeviceTypeTree } from '@/api/mes/deviceType'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
import request from '@/utils/request'
const { t } = useI18n()
const detailId = ref(undefined)
const detailData = ref(null)
const deviceTypeList = ref([])
const activeTab = ref('check')
const inspectionList = ref([])
const maintainList = ref([])
const repairList = ref([])
const tabs = computed(() => [
{ key: 'check', label: t('equipmentLedger.checkHistory') },
{ key: 'maintain', label: t('equipmentLedger.maintainHistory') },
{ key: 'repair', label: t('equipmentLedger.repairHistory') }
])
const scheduledText = computed(() => {
const val = detailData.value?.isSchedueld ?? detailData.value?.isScheduled
return Number(val) === 1 ? t('equipmentLedger.yes') : t('equipmentLedger.no')
})
onLoad(async (query) => {
const id = query?.id ? decodeURIComponent(String(query.id)) : ''
detailId.value = id || undefined
await initAllDict()
await fetchDeviceTypeList()
await fetchDetail()
})
async function fetchDeviceTypeList() {
try {
const res = await getDeviceTypeTree({})
const root = res && res.data !== undefined ? res.data : res
const treeData = Array.isArray(root) ? root : (Array.isArray(root?.data) ? root.data : [])
deviceTypeList.value = flattenTree(treeData)
} catch (e) {}
}
function flattenTree(nodes) {
const result = []
nodes.forEach(node => {
result.push(node)
if (node.children && node.children.length) {
result.push(...flattenTree(node.children))
}
})
return result
}
function getDeviceTypeNameById(id) {
if (!id) return '-'
const found = deviceTypeList.value.find(item => String(item.id) === String(id))
return found ? found.name : String(id)
}
async function fetchDetail() {
if (!detailId.value) {
uni.showToast({ title: t('equipmentLedger.noId'), icon: 'none' })
return
}
try {
const res = await getDeviceLedger(detailId.value)
detailData.value = normalizeDetail(res)
await fetchInspectionHistory()
await fetchMaintainHistory()
await fetchRepairHistory()
} catch (e) {
uni.showToast({ title: t('equipmentLedger.loadFailed'), icon: 'none' })
}
}
function normalizeDetail(res) {
const root = res && res.data !== undefined ? res.data : res
if (root?.data && typeof root.data === 'object') return root.data
if (root && typeof root === 'object') return root
return {}
}
async function fetchInspectionHistory() {
try {
const res = await request({ url: '/admin-api/mes/ticket-management/getInspectionByDeviceId', method: 'get', params: { deviceId: detailId.value } })
const data = res && res.data !== undefined ? res.data : res
inspectionList.value = Array.isArray(data) ? data : []
} catch (e) { inspectionList.value = [] }
}
async function fetchMaintainHistory() {
try {
const res = await request({ url: '/admin-api/mes/ticket-management/getMaintenanceByDeviceId', method: 'get', params: { deviceId: detailId.value } })
const data = res && res.data !== undefined ? res.data : res
maintainList.value = Array.isArray(data) ? data : []
} catch (e) { maintainList.value = [] }
}
async function fetchRepairHistory() {
try {
const res = await request({ url: '/admin-api/mes/dv-repair/getRepairListByDeviceId', method: 'get', params: { deviceId: detailId.value } })
const data = res && res.data !== undefined ? res.data : res
repairList.value = Array.isArray(data) ? data : []
} catch (e) { repairList.value = [] }
}
function switchTab(key) {
activeTab.value = key
}
function getStatusText(status) {
return getDictLabel(DICT_TYPE.MES_TZ_STATUS, status, textValue(status))
}
function getStatusClass(status) {
const s = String(status)
if (s === '0' || s === '1') return 'text-success'
return 'text-danger'
}
function getResultText(result) {
if (result === 'PASS' || result === 'pass' || result === 'OK' || result === 'ok' || result === 1 || result === '1') return t('equipmentLedger.resultPass')
if (result === 'FAIL' || result === 'fail' || result === 'NG' || result === 'ng' || result === 0 || result === '0') return t('equipmentLedger.resultFail')
return textValue(result)
}
function getResultClass(result) {
if (result === 'PASS' || result === 'pass' || result === 'OK' || result === 'ok' || result === 1 || result === '1') return 'text-success'
if (result === 'FAIL' || result === 'fail' || result === 'NG' || result === 'ng' || result === 0 || result === '0') return 'text-danger'
return ''
}
function getRepairStatusText(status) {
if (status === 0 || status === '0') return t('equipmentLedger.repairPending')
if (status === 1 || status === '1') return t('equipmentLedger.repairProcessing')
if (status === 2 || status === '2') return t('equipmentLedger.repairCompleted')
return textValue(status)
}
function getRepairStatusClass(status) {
if (status === 0 || status === '0') return 'text-warning'
if (status === 1 || status === '1') return 'text-primary'
if (status === 2 || status === '2') return 'text-success'
return ''
}
function fieldValue(field) {
return textValue(detailData.value ? detailData.value[field] : undefined)
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return t('functionCommon.no')
if (value === true) return t('functionCommon.yes')
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function formatDateValue(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d] = value
const pad = (n) => String(n).padStart(2, '0')
return `${y}-${pad(m)}-${pad(d)}`
}
const text = String(value).trim()
if (!text) return '-'
return text.split(' ')[0]
}
function formatDateTime(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = value
const pad = (n) => String(n).padStart(2, '0')
return `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}:${pad(ss)}`
}
const text = String(value).trim()
if (!text) return '-'
const numeric = Number(text)
if (Number.isFinite(numeric)) {
const timestamp = text.length === 10 ? numeric * 1000 : numeric
const date = new Date(timestamp)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
}
const date = new Date(text)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
return text
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.fixed-header { position: sticky; top: 0; z-index: 10; }
.detail-scroll { height: calc(100vh - 120rpx); }
.content-section { padding: 0 24rpx 24rpx; }
.info-card { margin-top: 20rpx; background: #ffffff; border-radius: 20rpx; padding: 28rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); }
.card-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; margin-bottom: 18rpx; }
.info-list { background: #ffffff; }
.info-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 18rpx 0; border-bottom: 1rpx solid #edf0f3; }
.info-label { font-size: 27rpx; color: #8a9099; width: 220rpx; }
.info-value { flex: 1; text-align: right; font-size: 28rpx; color: #30363d; line-height: 1.45; }
.text-success { color: #52c41a; }
.text-danger { color: #ff4d4f; }
.text-warning { color: #faad14; }
.text-primary { color: #1a3a5c; }
.remark-row { border-bottom: none; }
.remark-value { white-space: pre-wrap; }
.tab-bar { display: flex; border-bottom: 2rpx solid #edf0f3; margin-bottom: 20rpx; }
.tab-item { flex: 1; text-align: center; padding: 16rpx 0; position: relative; }
.tab-text { font-size: 28rpx; color: #8a9099; }
.tab-item.active .tab-text { color: #1a3a5c; font-weight: 700; }
.tab-item.active::after { content: ''; position: absolute; bottom: -2rpx; left: 30%; right: 30%; height: 4rpx; background: #1a3a5c; border-radius: 2rpx; }
.tab-content { min-height: 200rpx; }
.empty-tip { text-align: center; padding: 40rpx 0; color: #99a1aa; font-size: 26rpx; }
.history-item { padding: 20rpx 0; border-bottom: 1rpx solid #f0f2f5; }
.history-item:last-child { border-bottom: none; }
.history-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10rpx; }
.history-time { font-size: 24rpx; color: #8a9099; }
.history-result { font-size: 26rpx; font-weight: 600; }
.history-body { display: flex; justify-content: space-between; align-items: center; }
.history-name { font-size: 28rpx; color: #30363d; }
.history-operator { font-size: 24rpx; color: #8a9099; }
</style>

@ -0,0 +1,613 @@
<template>
<view class="page-container">
<NavBar :title="t('equipmentLedger.moduleName')" :subTitle="t('equipmentLedger.subTitle')" />
<!-- 搜索区域 -->
<view class="search-card">
<view class="search-row">
<view class="search-input-wrap">
<text class="iconfont icon-search search-icon"></text>
<input v-model="searchKeyword" class="search-input" :placeholder="t('equipmentLedger.searchPlaceholder')" @confirm="handleSearch" />
</view>
<view class="search-btn" @click="handleSearch">{{ t('functionCommon.search') }}</view>
</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="type-card" @click="openDetail(item)">
<view class="card-header">
<view class="header-left">
<text class="type-name">{{ textValue(item.deviceName) }}</text>
<text class="type-code">{{ t('equipmentLedger.deviceCode') }}: {{ textValue(item.deviceCode) }}</text>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('equipmentLedger.deviceType') }}</text>
<text class="value">{{ textValue(item.deviceTypeName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('equipmentLedger.deviceStatus') }}</text>
<text :class="['value', getStatusClass(item.deviceStatus)]">{{ getStatusText(item.deviceStatus) }}</text>
</view>
<view class="row">
<text class="label">{{ t('equipmentLedger.deviceSpec') }}</text>
<text class="value">{{ textValue(item.deviceSpec) }}</text>
</view>
<view class="row">
<text class="label">{{ t('equipmentLedger.deviceLocation') }}</text>
<text class="value">{{ textValue(item.deviceLocation) }}</text>
</view>
</view>
<view class="card-actions">
<view class="action-btn edit-btn" @click.stop="openEdit(item)">
<uni-icons type="compose" size="18" color="#ffffff"></uni-icons>
</view>
<view class="action-btn delete-btn" @click.stop="confirmDelete(item)">
<uni-icons type="trash" size="18" color="#ffffff"></uni-icons>
</view>
</view>
</view>
<view v-if="loading && pageNo === 1" class="loading-text">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!list.length" class="empty-text">{{ t('equipmentLedger.empty') }}</view>
<view v-else-if="loadingMore" class="loading-text">{{ t('functionCommon.loadingMore') }}</view>
<view v-else-if="finished" class="finished-text">{{ t('functionCommon.noMore') }}</view>
</view>
</scroll-view>
<!-- 新增悬浮按钮 -->
<view class="add-btn" @click="openCreate">
<text class="add-icon">+</text>
</view>
<!-- 返回顶部 -->
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<text class="go-top-icon"></text>
</view>
<!-- 新增/编辑弹框 -->
<uni-popup ref="formPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ formMode === 'create' ? t('equipmentLedger.createTitle') : t('equipmentLedger.editTitle') }}</text>
<view class="popup-close" @click="closeForm">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="form-scroll">
<view class="form-content">
<!-- 设备编码 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.deviceCode') }} <text class="required-star">*</text></text>
<view class="code-row">
<input v-model="formData.deviceCode" class="form-input code-input" type="text" :placeholder="t('equipmentLedger.placeholderDeviceCode')" :disabled="formData.isCode || formMode === 'update'" />
<view class="auto-code-wrap" @click="toggleAutoCode">
<text class="auto-code-label">{{ t('equipmentLedger.autoCode') }}</text>
<view :class="['auto-code-switch', formData.isCode ? 'active' : '']">
<text class="switch-dot"></text>
</view>
</view>
</view>
</view>
<!-- 设备名称 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.deviceName') }} <text class="required-star">*</text></text>
<input v-model="formData.deviceName" class="form-input" type="text" :placeholder="t('equipmentLedger.placeholderDeviceName')" />
</view>
<!-- 设备类型 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.deviceType') }} <text class="required-star">*</text></text>
<picker :range="deviceTypeLabels" @change="onDeviceTypeChange">
<view class="form-picker">
<text :class="['picker-text', !formData.deviceType ? 'placeholder' : '']">{{ selectedDeviceTypeLabel }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<!-- 设备规格 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.deviceSpec') }}</text>
<input v-model="formData.deviceSpec" class="form-input" type="text" :placeholder="t('equipmentLedger.placeholderDeviceSpec')" />
</view>
<!-- 是否排产 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.isScheduled') }}</text>
<view class="switch-row" @click="toggleScheduled">
<text :class="['switch-text', formData.isScheduled === 1 ? 'active' : '']">{{ formData.isScheduled === 1 ? t('equipmentLedger.yes') : t('equipmentLedger.no') }}</text>
<view :class="['auto-code-switch', formData.isScheduled === 1 ? 'active' : '']">
<text class="switch-dot"></text>
</view>
</view>
</view>
<!-- 额定产能 -->
<view v-if="formData.isScheduled === 1" class="form-item">
<text class="form-label">{{ t('equipmentLedger.ratedCapacity') }} <text class="required-star">*</text></text>
<input v-model="formData.ratedCapacity" class="form-input" type="digit" :placeholder="t('equipmentLedger.placeholderRatedCapacity')" />
</view>
<!-- 每日报工平均值 -->
<view v-if="formData.isScheduled === 1" class="form-item">
<text class="form-label">{{ t('equipmentLedger.dailyAverageValue') }} <text class="required-star">*</text></text>
<input v-model="formData.dailyAverageValue" class="form-input" type="digit" :placeholder="t('equipmentLedger.placeholderDailyAverageValue')" />
</view>
<!-- 数据采集产能 -->
<view v-if="formData.isScheduled === 1" class="form-item">
<text class="form-label">{{ t('equipmentLedger.dataCollectionCapacity') }} <text class="required-star">*</text></text>
<input v-model="formData.dataCollectionCapacity" class="form-input" type="digit" :placeholder="t('equipmentLedger.placeholderDataCollectionCapacity')" />
</view>
<!-- 生产日期 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.productionDate') }} <text class="required-star">*</text></text>
<picker mode="date" :value="formData.productionDate" @change="onProductionDateChange">
<view class="form-picker">
<text :class="['picker-text', !formData.productionDate ? 'placeholder' : '']">{{ formData.productionDate || t('equipmentLedger.placeholderProductionDate') }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<!-- 入厂日期 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.factoryEntryDate') }} <text class="required-star">*</text></text>
<picker mode="date" :value="formData.factoryEntryDate" @change="onFactoryEntryDateChange">
<view class="form-picker">
<text :class="['picker-text', !formData.factoryEntryDate ? 'placeholder' : '']">{{ formData.factoryEntryDate || t('equipmentLedger.placeholderFactoryEntryDate') }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<!-- 设备位置 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.deviceLocation') }}</text>
<input v-model="formData.deviceLocation" class="form-input" type="text" :placeholder="t('equipmentLedger.placeholderDeviceLocation')" />
</view>
<!-- 备注 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.remark') }}</text>
<textarea v-model="formData.remark" class="form-textarea" :placeholder="t('equipmentLedger.placeholderRemark')" :maxlength="200" />
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeForm">
<text class="btn-text">{{ t('functionCommon.cancel') }}</text>
</view>
<view class="footer-btn confirm-btn" @click="submitForm">
<text class="btn-text">{{ t('functionCommon.save') }}</text>
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getDeviceLedgerPage, getDeviceLedger, createDeviceLedger, updateDeviceLedger, deleteDeviceLedger } from '@/api/mes/deviceLedger'
import { getDeviceTypeTree } from '@/api/mes/deviceType'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const { t } = useI18n()
const formPopupRef = ref(null)
const searchKeyword = ref('')
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const finished = ref(false)
const pageNo = ref(1)
const pageSize = ref(10)
const total = ref(0)
const scrollTop = ref(0)
const showGoTop = ref(false)
const formMode = ref('create')
const deviceTypeList = ref([])
const formData = reactive({
id: undefined,
deviceCode: '',
isCode: false,
deviceName: '',
deviceType: '',
deviceSpec: '',
isScheduled: 0,
ratedCapacity: '',
dailyAverageValue: '',
dataCollectionCapacity: '',
productionDate: '',
factoryEntryDate: '',
deviceLocation: '',
remark: ''
})
onLoad(async () => {
await initAllDict()
await fetchDeviceTypeList()
await fetchList(true)
})
async function fetchDeviceTypeList() {
try {
const res = await getDeviceTypeTree({})
const root = res && res.data !== undefined ? res.data : res
const treeData = Array.isArray(root) ? root : (Array.isArray(root?.data) ? root.data : [])
deviceTypeList.value = flattenTree(treeData)
} catch (e) {}
}
function flattenTree(nodes) {
const result = []
nodes.forEach(node => {
result.push(node)
if (node.children && node.children.length) {
result.push(...flattenTree(node.children))
}
})
return result
}
const deviceTypeLabels = computed(() => deviceTypeList.value.map(item => item.name || ''))
const selectedDeviceTypeLabel = computed(() => {
if (!formData.deviceType) return t('equipmentLedger.placeholderDeviceType')
const found = deviceTypeList.value.find(item => String(item.id) === String(formData.deviceType))
return found ? found.name : String(formData.deviceType)
})
function onDeviceTypeChange(e) {
const idx = e.detail.value
const item = deviceTypeList.value[idx]
formData.deviceType = item?.id ?? ''
}
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,
deviceCode: searchKeyword.value.trim() || undefined,
deviceName: searchKeyword.value.trim() || undefined
}
const res = await getDeviceLedgerPage(params)
const page = normalizePageData(res)
total.value = page.total
if (reset) {
list.value = page.list
} else {
list.value = [...list.value, ...page.list]
}
const loadedCount = list.value.length
finished.value = loadedCount >= total.value || 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 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 || root?.data?.records || []
const candidateTotal = root?.total ?? root?.data?.total ?? (Array.isArray(candidateList) ? candidateList.length : 0)
return { list: Array.isArray(candidateList) ? candidateList : [], total: Number(candidateTotal || 0) }
}
function getStatusText(status) {
return getDictLabel(DICT_TYPE.MES_TZ_STATUS, status, textValue(status))
}
function getStatusClass(status) {
const s = String(status)
if (s === '0' || s === '1') return 'text-success'
return 'text-danger'
}
async function handleSearch() {
await fetchList(true)
}
function onScroll(e) {
const top = e?.detail?.scrollTop || 0
showGoTop.value = top > 600
}
function goTop() {
scrollTop.value = 0
}
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
}
function openCreate() {
formMode.value = 'create'
resetForm()
formPopupRef.value?.open()
}
async function openEdit(item) {
const id = item?.id
if (id === undefined || id === null) {
uni.showToast({ title: t('equipmentLedger.noId'), icon: 'none' })
return
}
try {
const res = await getDeviceLedger(id)
const detail = normalizeDetail(res)
formMode.value = 'update'
formData.id = detail?.id
formData.deviceCode = textValueForInput(detail?.deviceCode)
formData.isCode = Boolean(detail?.isCode)
formData.deviceName = textValueForInput(detail?.deviceName)
formData.deviceType = detail?.deviceType ?? ''
formData.deviceSpec = textValueForInput(detail?.deviceSpec)
formData.isScheduled = detail?.isSchedueld ?? detail?.isScheduled ?? 0
formData.ratedCapacity = textValueForInput(detail?.ratedCapacity)
formData.dailyAverageValue = textValueForInput(detail?.dailyAverageValue)
formData.dataCollectionCapacity = textValueForInput(detail?.dataCollectionCapacity)
formData.productionDate = formatDateForInput(detail?.productionDate)
formData.factoryEntryDate = formatDateForInput(detail?.factoryEntryDate)
formData.deviceLocation = textValueForInput(detail?.deviceLocation)
formData.remark = textValueForInput(detail?.remark)
formPopupRef.value?.open()
} catch (e) {
uni.showToast({ title: t('equipmentLedger.loadEditFailed'), icon: 'none' })
}
}
function confirmDelete(item) {
const id = item?.id
if (id === undefined || id === null) return
uni.showModal({
title: t('functionCommon.confirmTitle'),
content: t('equipmentLedger.confirmDeleteContent', { name: textValue(item?.deviceName) }),
success: async (res) => {
if (!res.confirm) return
try {
await deleteDeviceLedger(String(id))
uni.showToast({ title: t('functionCommon.deleteSuccess'), icon: 'success' })
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.deleteFailed'), icon: 'none' })
}
}
})
}
function closeForm() {
formPopupRef.value?.close()
}
function toggleAutoCode() {
if (formMode.value === 'update') return
formData.isCode = !formData.isCode
if (formData.isCode) formData.deviceCode = ''
}
function toggleScheduled() {
formData.isScheduled = formData.isScheduled === 1 ? 0 : 1
if (formData.isScheduled !== 1) {
formData.ratedCapacity = ''
formData.dailyAverageValue = ''
formData.dataCollectionCapacity = ''
}
}
function onProductionDateChange(e) {
formData.productionDate = e.detail.value
}
function onFactoryEntryDateChange(e) {
formData.factoryEntryDate = e.detail.value
}
async function submitForm() {
if (!formData.isCode && !formData.deviceCode.trim()) {
uni.showToast({ title: t('equipmentLedger.validatorDeviceCodeRequired'), icon: 'none' })
return
}
if (!formData.deviceName.trim()) {
uni.showToast({ title: t('equipmentLedger.validatorDeviceNameRequired'), icon: 'none' })
return
}
if (!formData.deviceType) {
uni.showToast({ title: t('equipmentLedger.validatorDeviceTypeRequired'), icon: 'none' })
return
}
if (!formData.productionDate) {
uni.showToast({ title: t('equipmentLedger.validatorProductionDateRequired'), icon: 'none' })
return
}
if (!formData.factoryEntryDate) {
uni.showToast({ title: t('equipmentLedger.validatorFactoryEntryDateRequired'), icon: 'none' })
return
}
if (formData.isScheduled === 1) {
if (!formData.ratedCapacity && formData.ratedCapacity !== 0) {
uni.showToast({ title: t('equipmentLedger.validatorRatedCapacityRequired'), icon: 'none' })
return
}
if (!formData.dailyAverageValue && formData.dailyAverageValue !== 0) {
uni.showToast({ title: t('equipmentLedger.validatorDailyAverageValueRequired'), icon: 'none' })
return
}
if (!formData.dataCollectionCapacity && formData.dataCollectionCapacity !== 0) {
uni.showToast({ title: t('equipmentLedger.validatorDataCollectionCapacityRequired'), icon: 'none' })
return
}
}
const payload = {
id: formMode.value === 'update' ? formData.id : undefined,
deviceCode: formData.isCode ? undefined : formData.deviceCode.trim(),
isCode: formData.isCode,
deviceName: formData.deviceName.trim(),
deviceType: formData.deviceType,
deviceSpec: formData.deviceSpec.trim() || undefined,
isScheduled: formData.isScheduled,
ratedCapacity: formData.isScheduled === 1 ? toNumberOrUndefined(formData.ratedCapacity) : undefined,
dailyAverageValue: formData.isScheduled === 1 ? toNumberOrUndefined(formData.dailyAverageValue) : undefined,
dataCollectionCapacity: formData.isScheduled === 1 ? toNumberOrUndefined(formData.dataCollectionCapacity) : undefined,
productionDate: formData.productionDate,
factoryEntryDate: formData.factoryEntryDate,
deviceLocation: formData.deviceLocation.trim() || undefined,
remark: formData.remark.trim() || undefined
}
try {
if (formMode.value === 'create') {
await createDeviceLedger(payload)
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
} else {
await updateDeviceLedger(payload)
uni.showToast({ title: t('functionCommon.updateSuccess'), icon: 'success' })
}
closeForm()
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
}
}
function resetForm() {
formData.id = undefined
formData.deviceCode = ''
formData.isCode = false
formData.deviceName = ''
formData.deviceType = ''
formData.deviceSpec = ''
formData.isScheduled = 0
formData.ratedCapacity = ''
formData.dailyAverageValue = ''
formData.dataCollectionCapacity = ''
formData.productionDate = ''
formData.factoryEntryDate = ''
formData.deviceLocation = ''
formData.remark = ''
}
function openDetail(item) {
const id = item?.id
if (!id && id !== 0) return
uni.navigateTo({
url: `/pages_function/pages/equipmentLedger/detail?id=${encodeURIComponent(String(id))}`
})
}
function normalizeDetail(res) {
const root = res && res.data !== undefined ? res.data : res
if (root?.data && typeof root.data === 'object') return root.data
if (root && typeof root === 'object') return root
return {}
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return t('functionCommon.no')
if (value === true) return t('functionCommon.yes')
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function textValueForInput(value) {
if (value === null || value === undefined) return ''
return String(value)
}
function formatDateForInput(value) {
if (!value) return ''
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d] = value
const pad = (n) => String(n).padStart(2, '0')
return `${y}-${pad(m)}-${pad(d)}`
}
const text = String(value).trim()
if (!text) return ''
return text.split(' ')[0]
}
function toNumberOrUndefined(value) {
if (value === null || value === undefined || String(value).trim() === '') return undefined
const num = Number(value)
return Number.isFinite(num) ? num : undefined
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.search-card { background: #ffffff; margin: 20rpx 24rpx; border-radius: 18rpx; padding: 20rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.04); }
.search-row { display: flex; align-items: center; gap: 16rpx; }
.search-input-wrap { flex: 1; display: flex; align-items: center; background: #f5f7fa; border-radius: 44rpx; padding: 0 20rpx; }
.search-icon { margin-right: 12rpx; font-size: 30rpx; color: #999; }
.search-input { flex: 1; height: 72rpx; font-size: 28rpx; color: #333; background: transparent; }
.search-btn { min-width: 120rpx; height: 72rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); border-radius: 14rpx; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 28rpx; font-weight: 600; }
.list-scroll { height: calc(100vh - 360rpx); }
.list-wrap { padding: 0 24rpx 30rpx; }
.type-card { background: #ffffff; border-radius: 18rpx; padding: 24rpx; margin-bottom: 18rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.05); }
.card-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 18rpx; border-bottom: 1rpx solid #edf0f3; }
.header-left { display: flex; flex-direction: column; }
.type-name { font-size: 32rpx; font-weight: 600; color: #1a3a5c; margin-bottom: 8rpx; }
.type-code { font-size: 24rpx; color: #8a9099; }
.card-body { padding-top: 16rpx; }
.row { display: flex; justify-content: space-between; align-items: center; margin-top: 12rpx; }
.label { font-size: 26rpx; color: #8a9099; }
.value { font-size: 27rpx; color: #30363d; max-width: 62%; text-align: right; }
.text-success { color: #52c41a; }
.text-danger { color: #ff4d4f; }
.card-actions { margin-top: 24rpx; display: flex; justify-content: flex-end; gap: 14rpx; }
.action-btn { width: 60rpx; height: 60rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.edit-btn { background: #1a3a5c; }
.delete-btn { background: #ff4d4f; }
.add-btn { position: fixed; right: 28rpx; bottom: 140rpx; width: 96rpx; height: 96rpx; border-radius: 48rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); display: flex; align-items: center; justify-content: center; box-shadow: 0 8rpx 24rpx rgba(26, 58, 92, 0.32); z-index: 99; }
.add-icon { color: #ffffff; font-size: 56rpx; line-height: 1; }
.go-top-btn { position: fixed; right: 28rpx; bottom: 254rpx; width: 88rpx; height: 88rpx; border-radius: 44rpx; background: rgba(26, 58, 92, 0.9); display: flex; align-items: center; justify-content: center; z-index: 99; box-shadow: 0 6rpx 16rpx rgba(26, 58, 92, 0.25); }
.go-top-icon { color: #ffffff; font-size: 36rpx; font-weight: 700; }
.popup-content { width: 680rpx; max-height: 80vh; border-radius: 20rpx; overflow: hidden; }
.popup-header { padding: 24rpx; display: flex; align-items: center; justify-content: space-between; border-bottom: 1rpx solid #edf0f3; }
.popup-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; }
.close-icon { font-size: 38rpx; color: #8e95a0; }
.form-scroll { max-height: 56vh; }
.form-content { padding: 24rpx; }
.form-item { margin-bottom: 18rpx; }
.form-label { display: block; margin-bottom: 8rpx; font-size: 26rpx; color: #8a9099; }
.required-star { color: #ff4d4f; }
.form-input { height: 76rpx; padding: 0 20rpx; border-radius: 12rpx; background: #f5f7fa; font-size: 28rpx; color: #30363d; }
.code-row { display: flex; align-items: center; gap: 16rpx; }
.code-input { flex: 1; }
.auto-code-wrap { display: flex; align-items: center; gap: 8rpx; }
.auto-code-label { font-size: 24rpx; color: #8a9099; white-space: nowrap; }
.auto-code-switch { width: 64rpx; height: 36rpx; border-radius: 18rpx; background: #dcdfe6; position: relative; transition: background 0.3s; }
.auto-code-switch.active { background: #1a3a5c; }
.switch-dot { position: absolute; top: 4rpx; left: 4rpx; width: 28rpx; height: 28rpx; border-radius: 14rpx; background: #fff; transition: transform 0.3s; }
.auto-code-switch.active .switch-dot { transform: translateX(28rpx); }
.switch-row { display: flex; align-items: center; gap: 16rpx; }
.switch-text { font-size: 28rpx; color: #8a9099; }
.switch-text.active { color: #1a3a5c; font-weight: 600; }
.form-picker { height: 76rpx; padding: 0 20rpx; border-radius: 12rpx; background: #f5f7fa; display: flex; align-items: center; justify-content: space-between; }
.picker-text { font-size: 28rpx; color: #30363d; }
.picker-text.placeholder { color: #999; }
.picker-arrow { font-size: 30rpx; color: #999; }
.form-textarea { width: 100%; min-height: 130rpx; padding: 18rpx 20rpx; border-radius: 12rpx; background: #f5f7fa; font-size: 28rpx; color: #30363d; }
.form-footer { display: flex; gap: 14rpx; padding: 20rpx 24rpx 24rpx; }
.footer-btn { flex: 1; height: 76rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.cancel-btn { background: #edf0f4; }
.confirm-btn { background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); }
.btn-text { font-size: 28rpx; font-weight: 600; color: #ffffff; }
.cancel-btn .btn-text { color: #586070; }
.loading-text, .empty-text, .finished-text { text-align: center; padding: 28rpx 0; color: #99a1aa; font-size: 26rpx; }
</style>

@ -0,0 +1,551 @@
<template>
<view class="page-container">
<view class="header-section">
<view class="back-btn" @click="goBack">
<text class="back-icon"></text>
<text class="back-text">返回</text>
</view>
<text class="header-title">检验类型</text>
</view>
<view class="search-section">
<view class="search-wrapper">
<view class="search-icon">
<text class="iconfont icon-search"></text>
</view>
<input
v-model="searchKeyword"
class="search-input"
type="text"
placeholder="请输入检验类型编码或名称"
placeholder-class="input-placeholder"
@input="handleSearch"
/>
<view v-if="searchKeyword" class="clear-btn" @click="clearSearch">
<text class="clear-icon">×</text>
</view>
</view>
</view>
<scroll-view scroll-y class="content-scroll">
<view class="inspection-list">
<view
v-for="(item, index) in inspectionList"
:key="index"
class="inspection-card"
>
<view class="card-header">
<view class="header-left">
<text class="inspection-name">{{ item.name }}</text>
<text class="inspection-code">编码: {{ item.code }}</text>
</view>
<view class="card-actions">
<view class="action-btn edit-btn" @click.stop="handleEdit(item)">
<text class="action-icon"></text>
</view>
<view class="action-btn delete-btn" @click.stop="handleDelete(item)">
<text class="action-icon">🗑</text>
</view>
</view>
</view>
<view class="card-body">
<view class="card-row">
<text class="card-label">备注</text>
<text class="card-value">{{ item.remark || '-' }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="add-btn" @click="showAddForm">
<text class="add-icon">+</text>
</view>
<uni-popup ref="addPopup" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">新增检验类型</text>
<view class="popup-close" @click="hideAddForm">
<text class="close-icon">×</text>
</view>
</view>
<view class="form-content">
<view class="form-item">
<text class="form-label">检验类型编码 <text class="required">*</text></text>
<input
v-model="formData.code"
class="form-input"
type="text"
placeholder="请输入检验类型编码"
/>
</view>
<view class="form-item">
<text class="form-label">检验类型名称 <text class="required">*</text></text>
<input
v-model="formData.name"
class="form-input"
type="text"
placeholder="请输入检验类型名称"
/>
</view>
<view class="form-item">
<text class="form-label">备注</text>
<textarea
v-model="formData.remark"
class="form-textarea"
placeholder="请输入备注"
:maxlength="200"
/>
</view>
</view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="hideAddForm">
<text class="btn-text">取消</text>
</view>
<view class="footer-btn confirm-btn" @click="handleSave">
<text class="btn-text">保存</text>
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue';
const addPopup = ref(null);
const searchKeyword = ref('');
const formData = reactive({
code: '',
name: '',
remark: ''
});
const inspectionList = reactive([
{ code: 'IT001', name: '来料检验', remark: '原材料入库前检验' },
{ code: 'IT002', name: '过程检验', remark: '生产过程中检验' },
{ code: 'IT003', name: '成品检验', remark: '产品完工后检验' },
{ code: 'IT004', name: '出货检验', remark: '产品出货前检验' },
{ code: 'IT005', name: '退货检验', remark: '退货产品检验' }
]);
function goBack() {
uni.navigateBack();
}
function handleSearch() {
console.log('搜索:', searchKeyword.value);
}
function clearSearch() {
searchKeyword.value = '';
}
function handleEdit(item) {
uni.showToast({
title: `编辑${item.name}`,
icon: 'none'
});
}
function handleDelete(item) {
uni.showModal({
title: '确认删除',
content: `确定要删除"${item.name}"吗?`,
success: () => {
uni.showToast({
title: '删除成功',
icon: 'success'
});
}
});
}
function showAddForm() {
addPopup.value.open();
}
function hideAddForm() {
addPopup.value.close();
resetForm();
}
function resetForm() {
formData.code = '';
formData.name = '';
formData.remark = '';
}
function handleSave() {
if (!formData.code || !formData.name) {
uni.showToast({
title: '请填写必填项',
icon: 'none'
});
return;
}
uni.showToast({
title: '保存成功',
icon: 'success'
});
hideAddForm();
resetForm();
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f0f2f5;
}
.header-section {
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
padding: 60rpx 40rpx 40rpx 30rpx;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.back-btn {
display: flex;
align-items: center;
position: absolute;
left: 30rpx;
top: 50%;
transform: translateY(-50%);
&:active {
opacity: 0.7;
}
.back-icon {
font-size: 48rpx;
color: #ffffff;
margin-right: 8rpx;
}
.back-text {
font-size: 28rpx;
color: #ffffff;
}
}
.header-title {
font-size: 34rpx;
font-weight: bold;
color: #ffffff;
}
.search-section {
background: #ffffff;
padding: 24rpx 30rpx;
margin-bottom: 20rpx;
}
.search-wrapper {
display: flex;
align-items: center;
background: #f5f7fa;
border-radius: 48rpx;
padding: 0 24rpx;
}
.search-icon {
margin-right: 20rpx;
.iconfont {
font-size: 36rpx;
color: #666666;
}
}
.search-input {
flex: 1;
height: 72rpx;
font-size: 28rpx;
color: #333333;
background: transparent;
}
.input-placeholder {
color: #999999;
}
.clear-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.7;
}
.clear-icon {
font-size: 36rpx;
color: #999999;
}
}
.content-scroll {
flex: 1;
height: calc(100vh - 260rpx);
}
.inspection-list {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 24rpx;
}
.inspection-card {
width: 100%;
background: #ffffff;
border-radius: 20rpx;
padding: 28rpx;
margin-bottom: 20rpx;
box-sizing: border-box;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #f0f2f5;
}
.header-left {
display: flex;
flex-direction: column;
}
.inspection-name {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 8rpx;
}
.inspection-code {
font-size: 24rpx;
color: #999999;
}
.card-actions {
display: flex;
gap: 16rpx;
}
.action-btn {
width: 64rpx;
height: 64rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.7;
transform: scale(0.95);
}
.action-icon {
font-size: 32rpx;
color: #ffffff;
}
}
.edit-btn {
background: #1a3a5c;
}
.delete-btn {
background: #ff4d4f;
}
.card-body {
display: flex;
flex-direction: column;
}
.card-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.card-label {
font-size: 26rpx;
color: #999999;
}
.card-value {
font-size: 28rpx;
color: #333333;
flex: 1;
text-align: right;
}
.add-btn {
position: fixed;
bottom: 120rpx;
right: 30rpx;
width: 100rpx;
height: 100rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(26, 58, 92, 0.3);
z-index: 999;
&:active {
transform: scale(0.95);
}
.add-icon {
font-size: 60rpx;
color: #ffffff;
font-weight: bold;
}
}
.popup-content {
width: 600rpx;
background: #ffffff;
border-radius: 20rpx;
overflow: hidden;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
}
.popup-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.7;
}
.close-icon {
font-size: 36rpx;
color: #ffffff;
}
}
.form-content {
padding: 32rpx;
}
.form-item {
margin-bottom: 24rpx;
}
.form-label {
display: block;
font-size: 28rpx;
color: #333333;
margin-bottom: 12rpx;
}
.required {
color: #ff4d4f;
margin-left: 4rpx;
}
.form-input {
width: 100%;
height: 80rpx;
padding: 0 24rpx;
font-size: 28rpx;
background: #f5f7fa;
border-radius: 12rpx;
border: 2rpx solid #e8eaed;
box-sizing: border-box;
}
.form-textarea {
width: 100%;
min-height: 120rpx;
padding: 16rpx 24rpx;
font-size: 28rpx;
background: #f5f7fa;
border-radius: 12rpx;
border: 2rpx solid #e8eaed;
box-sizing: border-box;
resize: none;
}
.form-footer {
display: flex;
gap: 16rpx;
margin-top: 32rpx;
}
.footer-btn {
flex: 1;
height: 88rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.8;
}
.btn-text {
font-size: 28rpx;
font-weight: 500;
}
}
.cancel-btn {
background: #f5f7fa;
.btn-text {
color: #666666;
}
}
.confirm-btn {
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
.btn-text {
color: #ffffff;
}
}
</style>

@ -0,0 +1,820 @@
<template>
<view class="page-container">
<view class="header-section">
<view class="back-btn" @click="goBack">
<text class="back-icon"></text>
<text class="back-text">返回</text>
</view>
<text class="header-title">检验项库</text>
</view>
<view class="search-section">
<view class="search-wrapper">
<view class="search-icon">
<text class="iconfont icon-search"></text>
</view>
<input
v-model="searchKeyword"
class="search-input"
type="text"
placeholder="请输入检验项名称"
placeholder-class="input-placeholder"
@input="handleSearch"
/>
<view v-if="searchKeyword" class="clear-btn" @click="clearSearch">
<text class="clear-icon">×</text>
</view>
</view>
</view>
<scroll-view scroll-y class="content-scroll">
<view class="item-list">
<view
v-for="(item, index) in itemList"
:key="index"
class="item-card"
>
<view class="card-header">
<view class="header-left">
<text class="item-name">{{ item.name }}</text>
<text class="item-type">{{ item.zjTypeName }}</text>
</view>
<view class="card-actions">
<view class="action-btn edit-btn" @click.stop="handleEdit(item)">
<text class="action-icon"></text>
</view>
<view class="action-btn delete-btn" @click.stop="handleDelete(item)">
<text class="action-icon">🗑</text>
</view>
</view>
</view>
<view class="card-body">
<view class="card-row">
<text class="card-label">作业方式</text>
<text class="card-value">{{ item.tool || '-' }}</text>
</view>
<view class="card-row">
<text class="card-label">标准值</text>
<text class="card-value">{{ item.standardVal || '-' }} {{ item.unitName || '' }}</text>
</view>
<view class="card-row">
<text class="card-label">上限值</text>
<text class="card-value">{{ item.upperVal || '-' }} {{ item.unitName || '' }}</text>
</view>
<view class="card-row">
<text class="card-label">下限值</text>
<text class="card-value">{{ item.lowerVal || '-' }} {{ item.unitName || '' }}</text>
</view>
<view class="card-row">
<text class="card-label">备注</text>
<text class="card-value">{{ item.remark || '-' }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="add-btn" @click="showAddForm">
<text class="add-icon">+</text>
</view>
<uni-popup ref="addPopup" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">新增检验项</text>
<view class="popup-close" @click="hideAddForm">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="form-scroll">
<view class="form-content">
<view class="form-item">
<text class="form-label">检验项名称 <text class="required">*</text></text>
<input
v-model="formData.name"
class="form-input"
type="text"
placeholder="请输入检验项名称"
/>
</view>
<view class="form-item">
<text class="form-label">检验类型 <text class="required">*</text></text>
<view class="form-picker" @click="showTypePicker">
<text :class="{ 'placeholder-text': !formData.zjTypeName }">
{{ formData.zjTypeName || '请选择检验类型' }}
</text>
<text class="picker-arrow"></text>
</view>
</view>
<view class="form-item">
<text class="form-label">作业方式</text>
<input
v-model="formData.tool"
class="form-input"
type="text"
placeholder="请输入作业方式"
/>
</view>
<view class="form-row">
<view class="form-item half">
<text class="form-label">标准值</text>
<input
v-model="formData.standardVal"
class="form-input"
type="text"
placeholder="请输入"
/>
</view>
<view class="form-item half">
<text class="form-label">单位</text>
<input
v-model="formData.unitName"
class="form-input"
type="text"
placeholder="请输入"
/>
</view>
</view>
<view class="form-row">
<view class="form-item half">
<text class="form-label">上限值</text>
<input
v-model="formData.upperVal"
class="form-input"
type="text"
placeholder="请输入"
/>
</view>
<view class="form-item half">
<text class="form-label">下限值</text>
<input
v-model="formData.lowerVal"
class="form-input"
type="text"
placeholder="请输入"
/>
</view>
</view>
<view class="form-item">
<text class="form-label">备注</text>
<textarea
v-model="formData.remark"
class="form-textarea"
placeholder="请输入备注"
:maxlength="200"
/>
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="hideAddForm">
<text class="btn-text">取消</text>
</view>
<view class="footer-btn confirm-btn" @click="handleSave">
<text class="btn-text">保存</text>
</view>
</view>
</view>
</uni-popup>
<uni-popup ref="typePicker" type="bottom" background-color="#fff">
<view class="picker-content">
<view class="picker-header">
<text class="picker-title">选择检验类型</text>
<view class="picker-close" @click="hideTypePicker">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="picker-list">
<view
v-for="(item, index) in typeList"
:key="index"
class="picker-item"
@click="selectType(item)"
>
<text class="picker-text">{{ item.name }}</text>
<text v-if="formData.zjTypeName === item.name" class="picker-check"></text>
</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue';
const addPopup = ref(null);
const typePicker = ref(null);
const searchKeyword = ref('');
const formData = reactive({
name: '',
zjTypeName: '',
tool: '',
standardVal: '',
upperVal: '',
lowerVal: '',
unitName: '',
remark: ''
});
const typeList = reactive([
{ name: '来料检验' },
{ name: '过程检验' },
{ name: '成品检验' },
{ name: '出货检验' },
{ name: '退货检验' }
]);
const itemList = reactive([
{
name: '外观检查',
zjTypeName: '来料检验',
tool: '目视',
standardVal: '合格',
upperVal: '',
lowerVal: '',
unitName: '',
remark: '检查产品外观是否有缺陷'
},
{
name: '尺寸测量',
zjTypeName: '来料检验',
tool: '卡尺',
standardVal: '100',
upperVal: '102',
lowerVal: '98',
unitName: 'mm',
remark: '测量产品关键尺寸'
},
{
name: '重量检测',
zjTypeName: '过程检验',
tool: '电子秤',
standardVal: '500',
upperVal: '510',
lowerVal: '490',
unitName: 'g',
remark: '检测产品重量'
},
{
name: '硬度测试',
zjTypeName: '成品检验',
tool: '硬度计',
standardVal: '60',
upperVal: '65',
lowerVal: '55',
unitName: 'HRC',
remark: '测试产品硬度'
},
{
name: '电气性能',
zjTypeName: '出货检验',
tool: '万用表',
standardVal: '220',
upperVal: '230',
lowerVal: '210',
unitName: 'V',
remark: '检测电气性能'
}
]);
function goBack() {
uni.navigateBack();
}
function handleSearch() {
console.log('搜索:', searchKeyword.value);
}
function clearSearch() {
searchKeyword.value = '';
}
function handleEdit(item) {
uni.showToast({
title: `编辑${item.name}`,
icon: 'none'
});
}
function handleDelete(item) {
uni.showModal({
title: '确认删除',
content: `确定要删除"${item.name}"吗?`,
success: () => {
uni.showToast({
title: '删除成功',
icon: 'success'
});
}
});
}
function showAddForm() {
addPopup.value.open();
}
function hideAddForm() {
addPopup.value.close();
resetForm();
}
function resetForm() {
formData.name = '';
formData.zjTypeName = '';
formData.tool = '';
formData.standardVal = '';
formData.upperVal = '';
formData.lowerVal = '';
formData.unitName = '';
formData.remark = '';
}
function showTypePicker() {
typePicker.value.open();
}
function hideTypePicker() {
typePicker.value.close();
}
function selectType(item) {
formData.zjTypeName = item.name;
hideTypePicker();
}
function handleSave() {
if (!formData.name || !formData.zjTypeName) {
uni.showToast({
title: '请填写必填项',
icon: 'none'
});
return;
}
uni.showToast({
title: '保存成功',
icon: 'success'
});
hideAddForm();
resetForm();
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f0f2f5;
}
.header-section {
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
padding: 60rpx 40rpx 40rpx 30rpx;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.back-btn {
display: flex;
align-items: center;
position: absolute;
left: 30rpx;
top: 50%;
transform: translateY(-50%);
&:active {
opacity: 0.7;
}
.back-icon {
font-size: 48rpx;
color: #ffffff;
margin-right: 8rpx;
}
.back-text {
font-size: 28rpx;
color: #ffffff;
}
}
.header-title {
font-size: 34rpx;
font-weight: bold;
color: #ffffff;
}
.search-section {
background: #ffffff;
padding: 24rpx 30rpx;
margin-bottom: 20rpx;
}
.search-wrapper {
display: flex;
align-items: center;
background: #f5f7fa;
border-radius: 48rpx;
padding: 0 24rpx;
}
.search-icon {
margin-right: 20rpx;
.iconfont {
font-size: 36rpx;
color: #666666;
}
}
.search-input {
flex: 1;
height: 72rpx;
font-size: 28rpx;
color: #333333;
background: transparent;
}
.input-placeholder {
color: #999999;
}
.clear-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.7;
}
.clear-icon {
font-size: 36rpx;
color: #999999;
}
}
.content-scroll {
flex: 1;
height: calc(100vh - 260rpx);
}
.item-list {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 24rpx;
}
.item-card {
width: 100%;
background: #ffffff;
border-radius: 20rpx;
padding: 28rpx;
margin-bottom: 20rpx;
box-sizing: border-box;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #f0f2f5;
}
.header-left {
display: flex;
flex-direction: column;
}
.item-name {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 8rpx;
}
.item-type {
font-size: 24rpx;
color: #999999;
}
.card-actions {
display: flex;
gap: 16rpx;
}
.action-btn {
width: 64rpx;
height: 64rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.7;
transform: scale(0.95);
}
.action-icon {
font-size: 32rpx;
color: #ffffff;
}
}
.edit-btn {
background: #1a3a5c;
}
.delete-btn {
background: #ff4d4f;
}
.card-body {
display: flex;
flex-direction: column;
}
.card-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.card-label {
font-size: 26rpx;
color: #999999;
}
.card-value {
font-size: 28rpx;
color: #333333;
flex: 1;
text-align: right;
}
.add-btn {
position: fixed;
bottom: 120rpx;
right: 30rpx;
width: 100rpx;
height: 100rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(26, 58, 92, 0.3);
z-index: 999;
&:active {
transform: scale(0.95);
}
.add-icon {
font-size: 60rpx;
color: #ffffff;
font-weight: bold;
}
}
.popup-content {
width: 650rpx;
max-height: 80vh;
background: #ffffff;
border-radius: 20rpx;
overflow: hidden;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
}
.popup-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.7;
}
.close-icon {
font-size: 36rpx;
color: #ffffff;
}
}
.form-scroll {
max-height: 55vh;
}
.form-content {
padding: 32rpx;
}
.form-item {
margin-bottom: 24rpx;
}
.form-row {
display: flex;
gap: 20rpx;
.form-item {
flex: 1;
}
}
.form-label {
display: block;
font-size: 28rpx;
color: #333333;
margin-bottom: 12rpx;
}
.required {
color: #ff4d4f;
margin-left: 4rpx;
}
.form-input {
width: 100%;
height: 80rpx;
padding: 0 24rpx;
font-size: 28rpx;
background: #f5f7fa;
border-radius: 12rpx;
border: 2rpx solid #e8eaed;
box-sizing: border-box;
}
.form-picker {
width: 100%;
height: 80rpx;
padding: 0 24rpx;
font-size: 28rpx;
background: #f5f7fa;
border-radius: 12rpx;
border: 2rpx solid #e8eaed;
box-sizing: border-box;
display: flex;
justify-content: space-between;
align-items: center;
.placeholder-text {
color: #999999;
}
.picker-arrow {
font-size: 32rpx;
color: #999999;
}
}
.form-textarea {
width: 100%;
min-height: 120rpx;
padding: 16rpx 24rpx;
font-size: 28rpx;
background: #f5f7fa;
border-radius: 12rpx;
border: 2rpx solid #e8eaed;
box-sizing: border-box;
resize: none;
}
.form-footer {
display: flex;
gap: 16rpx;
padding: 0 32rpx 32rpx;
}
.footer-btn {
flex: 1;
height: 88rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.8;
}
.btn-text {
font-size: 28rpx;
font-weight: 500;
}
}
.cancel-btn {
background: #f5f7fa;
.btn-text {
color: #666666;
}
}
.confirm-btn {
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
.btn-text {
color: #ffffff;
}
}
.picker-content {
background: #ffffff;
border-radius: 20rpx 20rpx 0 0;
}
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 2rpx solid #f0f2f5;
}
.picker-title {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
}
.picker-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.7;
}
.close-icon {
font-size: 36rpx;
color: #999999;
}
}
.picker-list {
max-height: 600rpx;
}
.picker-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 2rpx solid #f0f2f5;
&:last-child {
border-bottom: none;
}
&:active {
background: #f5f7fa;
}
}
.picker-text {
font-size: 28rpx;
color: #333333;
}
.picker-check {
font-size: 32rpx;
color: #1a3a5c;
}
</style>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,143 @@
<template>
<view class="page-container">
<NavBar title="关键件详情" />
<view class="content-section">
<view class="info-card">
<view class="card-title">基本信息</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">编码</text>
<text class="info-value">{{ detailValue(detailData?.code) }}</text>
</view>
<view class="info-row">
<text class="info-label">名称</text>
<text class="info-value">{{ detailValue(detailData?.name) }}</text>
</view>
<view class="info-row">
<text class="info-label">描述</text>
<text class="info-value">{{ detailValue(detailData?.description) }}</text>
</view>
<view class="info-row">
<text class="info-label">数量</text>
<text class="info-value">{{ detailValue(detailData?.count) }}</text>
</view>
<view class="info-row">
<text class="info-label">备注</text>
<text class="info-value">{{ detailValue(detailData?.remark) }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import request from '@/utils/request'
import NavBar from '@/components/common/NavBar.vue'
const keypartId = ref(undefined)
const keypartCode = ref(undefined)
const keypartType = ref(undefined)
const loading = ref(false)
const detailData = ref(null)
onLoad((query) => {
const rawId = query && query.id
const decoded = rawId ? decodeURIComponent(String(rawId)) : ''
keypartId.value = decoded ? decoded : undefined
keypartType.value = query && query.type
keypartCode.value = query && query.code
fetchDetail()
})
async function fetchDetail() {
if (!keypartId.value && !keypartCode.value) {
uni.showToast({ title: '缺少关键件ID', icon: 'none' })
return
}
loading.value = true
try {
const isScan = keypartType.value === 'scan'
const params = isScan ? { id: keypartId.value } : { code: keypartCode.value }
const res = await request({
url: '/admin-api/mes/critical-component/get',
method: 'get',
params: params
})
detailData.value = res && res.data !== undefined ? res.data : res
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
function detailValue(v) {
if (v === 0) return '0'
if (v === false) return '否'
if (v === true) return '是'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s ? s : '-'
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f0f2f5;
}
.content-section {
padding: 0 30rpx 30rpx;
margin-top: 40rpx;
}
.info-card {
background: #ffffff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 24rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #f0f2f5;
}
.info-list {
display: flex;
flex-direction: column;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f7fa;
&:last-child {
border-bottom: none;
}
}
.info-label {
font-size: 28rpx;
color: #999999;
}
.info-value {
font-size: 28rpx;
color: #333333;
}
</style>

@ -0,0 +1,374 @@
<template>
<view class="page-container">
<NavBar title="关键件查询" subTitle="请选择查询方式" />
<view class="content-section">
<view class="scan-section">
<view class="scan-area" @click="startScan">
<view class="scan-icon">
<text class="icon-text">📷</text>
</view>
<text class="scan-title">自动扫描</text>
<text class="scan-desc">点击启动扫描关键件编码</text>
</view>
<view v-if="isScanning" class="scanning-overlay">
<view class="scanning-animation">
<view class="scan-line"></view>
<view class="scan-corners">
<view class="corner corner-tl"></view>
<view class="corner corner-tr"></view>
<view class="corner corner-bl"></view>
<view class="corner corner-br"></view>
</view>
</view>
<text class="scanning-text">正在打开扫码...</text>
</view>
</view>
<view class="divider">
<view class="divider-line"></view>
<text class="divider-text"></text>
<view class="divider-line"></view>
</view>
<view class="input-section">
<view class="input-label">手动输入关键件ID</view>
<view class="input-wrapper">
<input
v-model="keypartCode"
class="code-input"
type="text"
placeholder="请输入关键件ID"
placeholder-class="input-placeholder"
/>
</view>
<view class="confirm-btn" :class="{ active: keypartCode.length > 0 }" @click="confirmInput">
<text class="btn-text">确定</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import NavBar from '@/components/common/NavBar.vue'
const keypartCode = ref('');
const isScanning = ref(false);
const SCAN_TYPE = 'KEY_PART';
const MODULE_NAME = '关键件';
function parseScanResult(res) {
const raw = String(res?.result || '').trim();
if (!raw) return {};
const splitIndex = raw.indexOf('-');
if (splitIndex <= 0 || splitIndex >= raw.length - 1) return {};
return {
type: raw.slice(0, splitIndex),
id: raw.slice(splitIndex + 1)
};
}
function startScan() {
if (isScanning.value) return;
isScanning.value = true;
const finish = () => {
isScanning.value = false;
}
uni.scanCode({
onlyFromCamera: true,
scanType: ['qrCode', 'barCode'],
success: (res) => {
const { type, id } = parseScanResult(res);
if (!type || !id) {
uni.showToast({ title: '未获取到扫码结果', icon: 'none' })
return
}
if (type !== SCAN_TYPE) {
uni.showToast({ title: `二维码类型不匹配,请扫描${MODULE_NAME}二维码`, icon: 'none' })
return
}
navigateToDetail(id)
},
fail: (err) => {
const msg = String(err?.errMsg || '')
if (msg.includes('cancel')) {
uni.showToast({ title: '已取消扫码', icon: 'none' })
return
}
if (msg.toLowerCase().includes('not support') || msg.toLowerCase().includes('not supported')) {
uni.showToast({ title: '当前平台不支持扫码', icon: 'none' })
return
}
uni.showToast({ title: '扫码失败', icon: 'none' })
},
complete: finish
})
}
function confirmInput() {
if (keypartCode.value.trim().length === 0) {
uni.showToast({
title: '请输入关键件ID',
icon: 'none'
});
return;
}
navigateToDetail(keypartCode.value.trim(), 'input');
}
function navigateToDetail(id, type = 'scan') {
let url
if (type === 'scan') {
url = `/pages_function/pages/keypart/detail?id=${encodeURIComponent(id)}&type=${type}`
} else {
url = `/pages_function/pages/keypart/detail?code=${id}&type=${type}`
}
uni.navigateTo({
url: url
});
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f0f2f5;
}
.content-section {
padding: 40rpx 30rpx;
}
.scan-section {
position: relative;
background: #ffffff;
border-radius: 24rpx;
padding: 60rpx 40rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.scan-area {
display: flex;
flex-direction: column;
align-items: center;
&:active {
opacity: 0.8;
}
}
.scan-icon {
width: 160rpx;
height: 160rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #3d7ab5 100%);
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30rpx;
.icon-text {
font-size: 72rpx;
}
}
.scan-title {
font-size: 34rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 12rpx;
}
.scan-desc {
font-size: 26rpx;
color: #999999;
}
.scanning-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(26, 58, 92, 0.95);
border-radius: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
}
.scanning-animation {
width: 300rpx;
height: 300rpx;
position: relative;
margin-bottom: 40rpx;
}
.scan-line {
position: absolute;
top: 0;
left: 20rpx;
right: 20rpx;
height: 4rpx;
background: linear-gradient(90deg, transparent, #ff8c00, transparent);
animation: scanMove 1.5s ease-in-out infinite;
}
@keyframes scanMove {
0% {
top: 20rpx;
}
50% {
top: 260rpx;
}
100% {
top: 20rpx;
}
}
.scan-corners {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.corner {
position: absolute;
width: 40rpx;
height: 40rpx;
border-color: #ff8c00;
border-style: solid;
border-width: 0;
}
.corner-tl {
top: 0;
left: 0;
border-top-width: 6rpx;
border-left-width: 6rpx;
border-top-left-radius: 12rpx;
}
.corner-tr {
top: 0;
right: 0;
border-top-width: 6rpx;
border-right-width: 6rpx;
border-top-right-radius: 12rpx;
}
.corner-bl {
bottom: 0;
left: 0;
border-bottom-width: 6rpx;
border-left-width: 6rpx;
border-bottom-left-radius: 12rpx;
}
.corner-br {
bottom: 0;
right: 0;
border-bottom-width: 6rpx;
border-right-width: 6rpx;
border-bottom-right-radius: 12rpx;
}
.scanning-text {
font-size: 32rpx;
color: #ffffff;
margin-bottom: 30rpx;
}
.divider {
display: flex;
align-items: center;
margin-bottom: 30rpx;
.divider-line {
flex: 1;
height: 2rpx;
background: #e8eaed;
}
.divider-text {
padding: 0 30rpx;
font-size: 28rpx;
color: #999999;
}
}
.input-section {
background: #ffffff;
border-radius: 24rpx;
padding: 40rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.input-label {
font-size: 30rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 24rpx;
}
.input-wrapper {
margin-bottom: 30rpx;
}
.code-input {
width: 100%;
height: 96rpx;
background: #f5f7fa;
border-radius: 16rpx;
padding: 0 30rpx;
font-size: 30rpx;
color: #333333;
border: 2rpx solid transparent;
&:focus {
border-color: #1a3a5c;
background: #ffffff;
}
}
.input-placeholder {
color: #c0c4cc;
}
.confirm-btn {
height: 96rpx;
background: #c0c4cc;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&.active {
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
&:active {
opacity: 0.8;
transform: scale(0.98);
}
}
.btn-text {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
}
}
</style>

@ -0,0 +1,155 @@
<template>
<view class="page-container">
<view class="fixed-header">
<NavBar :title="t('materialCategory.detailTitle')" />
</view>
<scroll-view scroll-y class="detail-scroll">
<view class="content-section">
<!-- 基础信息 -->
<view class="info-card">
<view class="card-title">{{ t('materialCategory.basicInfo') }}</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">{{ t('materialCategory.code') }}</text>
<text class="info-value">{{ fieldValue('code') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialCategory.name') }}</text>
<text class="info-value">{{ fieldValue('name') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialCategory.parentName') }}</text>
<text class="info-value">{{ parentNameText }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialCategory.sort') }}</text>
<text class="info-value">{{ fieldValue('sort') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialCategory.status') }}</text>
<text :class="['info-value', String(detailData?.status) === '0' ? 'text-success' : 'text-danger']">{{ statusText }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialCategory.createTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData?.createTime) }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getProductCategory, getProductCategoryList } from '@/api/erp/productCategory'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const { t } = useI18n()
const detailId = ref(undefined)
const detailData = ref(null)
const allCategories = ref([])
const parentNameText = computed(() => {
if (!detailData.value?.parentId || detailData.value.parentId === 0) return t('materialCategory.rootCategory')
const parent = allCategories.value.find(item => item.id === detailData.value.parentId)
return parent ? (parent.name || '-') : '-'
})
const statusText = computed(() => {
return getDictLabel(DICT_TYPE.COMMON_STATUS, detailData.value?.status, textValue(detailData.value?.status))
})
onLoad(async (query) => {
const id = query?.id ? decodeURIComponent(String(query.id)) : ''
detailId.value = id || undefined
await initAllDict()
await fetchDetail()
})
async function fetchDetail() {
if (!detailId.value) {
uni.showToast({ title: t('materialCategory.noId'), icon: 'none' })
return
}
try {
const res = await getProductCategory(detailId.value)
detailData.value = normalizeDetail(res)
const listRes = await getProductCategoryList()
allCategories.value = normalizeListData(listRes)
} catch (e) {
uni.showToast({ title: t('materialCategory.loadFailed'), icon: 'none' })
}
}
function normalizeDetail(res) {
const root = res && res.data !== undefined ? res.data : res
if (root?.data && typeof root.data === 'object') return root.data
if (root && typeof root === 'object') return root
return {}
}
function normalizeListData(res) {
const root = res && res.data !== undefined ? res.data : res
if (Array.isArray(root)) return root
if (root?.data && Array.isArray(root.data)) return root.data
return []
}
function fieldValue(field) {
return textValue(detailData.value ? detailData.value[field] : undefined)
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return t('functionCommon.no')
if (value === true) return t('functionCommon.yes')
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function formatDateTime(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = value
const pad = (n) => String(n).padStart(2, '0')
return `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}:${pad(ss)}`
}
const text = String(value).trim()
if (!text) return '-'
const numeric = Number(text)
if (Number.isFinite(numeric)) {
const timestamp = text.length === 10 ? numeric * 1000 : numeric
const date = new Date(timestamp)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
}
const date = new Date(text)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
return text
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.fixed-header { position: sticky; top: 0; z-index: 10; }
.detail-scroll { height: calc(100vh - 120rpx); }
.content-section { padding: 0 24rpx 24rpx; }
.info-card { margin-top: 20rpx; background: #ffffff; border-radius: 20rpx; padding: 28rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); }
.card-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; margin-bottom: 18rpx; }
.info-list { background: #ffffff; }
.info-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 18rpx 0; border-bottom: 1rpx solid #edf0f3; }
.info-row:last-child { border-bottom: none; }
.info-label { font-size: 27rpx; color: #8a9099; width: 220rpx; }
.info-value { flex: 1; text-align: right; font-size: 28rpx; color: #30363d; line-height: 1.45; }
.text-success { color: #52c41a; }
.text-danger { color: #ff4d4f; }
</style>

@ -0,0 +1,420 @@
<template>
<view class="page-container">
<NavBar :title="t('materialCategory.moduleName')" :subTitle="t('materialCategory.subTitle')" />
<!-- 搜索区域 -->
<view class="search-card">
<view class="search-row">
<view class="search-input-wrap">
<text class="iconfont icon-search search-icon"></text>
<input v-model="searchKeyword" class="search-input" :placeholder="t('materialCategory.searchPlaceholder')" @confirm="handleSearch" />
</view>
<view class="search-btn" @click="handleSearch">{{ t('functionCommon.search') }}</view>
</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 filteredList" :key="item.id" class="type-card" @click="openDetail(item)">
<view class="card-header">
<view class="header-left">
<text class="type-name">{{ textValue(item.name) }}</text>
<text class="type-code">{{ t('materialCategory.code') }}: {{ textValue(item.code) }}</text>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('materialCategory.parentName') }}</text>
<text class="value">{{ getParentName(item.parentId) }}</text>
</view>
<view class="row">
<text class="label">{{ t('materialCategory.sort') }}</text>
<text class="value">{{ textValue(item.sort) }}</text>
</view>
<view class="row">
<text class="label">{{ t('materialCategory.status') }}</text>
<text :class="['value', String(item.status) === '0' ? 'text-success' : 'text-danger']">{{ statusLabel(item.status) }}</text>
</view>
</view>
<view class="card-actions">
<view class="action-btn edit-btn" @click.stop="openEdit(item)">
<uni-icons type="compose" size="18" color="#ffffff"></uni-icons>
</view>
<view class="action-btn delete-btn" @click.stop="confirmDelete(item)">
<uni-icons type="trash" size="18" color="#ffffff"></uni-icons>
</view>
</view>
</view>
<view v-if="loading" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!filteredList.length" class="hint">{{ t('materialCategory.empty') }}</view>
</view>
</scroll-view>
<!-- 返回顶部 -->
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<uni-icons type="arrow-up" size="20" color="#1a3a5c"></uni-icons>
</view>
<!-- 新增按钮 -->
<view class="add-btn" @click="openCreate">
<text class="add-icon">+</text>
</view>
<!-- 新增/编辑弹框 -->
<uni-popup ref="formPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ formMode === 'create' ? t('materialCategory.createTitle') : t('materialCategory.editTitle') }}</text>
<view class="popup-close" @click="closeForm">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="form-scroll">
<view class="form-content">
<view class="form-item">
<text class="form-label">{{ t('materialCategory.parentName') }} <text class="required-star">*</text></text>
<picker mode="selector" :range="parentCategoryOptions" range-key="label" @change="onParentChange">
<view :class="['form-picker', formData.parentId === undefined ? 'placeholder' : '']">
{{ formData.parentId !== undefined ? getParentName(formData.parentId) : t('materialCategory.placeholderParent') }}
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">{{ t('materialCategory.code') }} <text class="required-star">*</text></text>
<input v-model="formData.code" class="form-input" :placeholder="t('materialCategory.placeholderCode')" :disabled="formMode === 'update'" />
</view>
<view class="form-item">
<text class="form-label">{{ t('materialCategory.name') }} <text class="required-star">*</text></text>
<input v-model="formData.name" class="form-input" :placeholder="t('materialCategory.placeholderName')" />
</view>
<view class="form-item">
<text class="form-label">{{ t('materialCategory.sort') }} <text class="required-star">*</text></text>
<input v-model="formData.sort" class="form-input" type="number" :placeholder="t('materialCategory.placeholderSort')" />
</view>
<view class="form-item">
<text class="form-label">{{ t('materialCategory.status') }} <text class="required-star">*</text></text>
<picker mode="selector" :range="statusOptions" range-key="label" @change="onStatusChange">
<view :class="['form-picker', formData.status === undefined ? 'placeholder' : '']">
{{ formData.status !== undefined ? statusLabel(formData.status) : t('materialCategory.placeholderStatus') }}
</view>
</picker>
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeForm"><text class="btn-text">{{ t('functionCommon.cancel') }}</text></view>
<view class="footer-btn confirm-btn" @click="submitForm"><text class="btn-text">{{ t('functionCommon.save') }}</text></view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getProductCategoryList, getProductCategorySimpleList, createProductCategory, updateProductCategory, deleteProductCategory } from '@/api/erp/productCategory'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const { t } = useI18n()
const formPopupRef = ref(null)
const searchKeyword = ref('')
const list = ref([])
const allCategories = ref([])
const loading = ref(false)
const scrollTop = ref(0)
const showGoTop = ref(false)
const formMode = ref('create')
const formData = reactive({
id: undefined,
parentId: undefined,
code: '',
name: '',
sort: 0,
status: 0
})
const statusOptions = computed(() => [
{ label: t('materialCategory.statusEnable'), value: 0 },
{ label: t('materialCategory.statusDisable'), value: 1 }
])
const parentCategoryOptions = computed(() => {
const root = [{ label: t('materialCategory.rootCategory'), value: 0 }]
allCategories.value.forEach(item => {
root.push({ label: item.name || '', value: item.id })
})
return root
})
const filteredList = computed(() => {
if (!searchKeyword.value.trim()) return list.value
const keyword = searchKeyword.value.trim().toLowerCase()
return list.value.filter(item =>
(item.name && item.name.toLowerCase().includes(keyword)) ||
(item.code && item.code.toLowerCase().includes(keyword))
)
})
onLoad(async () => {
await initAllDict()
await fetchList()
})
async function fetchList() {
loading.value = true
try {
const res = await getProductCategoryList()
const data = normalizeData(res)
allCategories.value = Array.isArray(data) ? data : []
list.value = flattenTree(buildTree(allCategories.value))
} catch (e) {
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
} finally {
loading.value = false
}
}
function buildTree(data) {
const map = {}
const roots = []
data.forEach(item => { map[item.id] = { ...item, children: [] } })
data.forEach(item => {
if (item.parentId && map[item.parentId]) {
map[item.parentId].children.push(map[item.id])
} else {
roots.push(map[item.id])
}
})
return roots
}
function flattenTree(nodes, depth = 0) {
const result = []
nodes.forEach(node => {
result.push({ ...node, _depth: depth })
if (node.children && node.children.length) {
result.push(...flattenTree(node.children, depth + 1))
}
})
return result
}
function normalizeData(res) {
const root = res && res.data !== undefined ? res.data : res
if (Array.isArray(root)) return root
if (root?.data && Array.isArray(root.data)) return root.data
return []
}
function getParentName(parentId) {
if (!parentId || parentId === 0) return t('materialCategory.rootCategory')
const parent = allCategories.value.find(item => item.id === parentId)
return parent ? (parent.name || '-') : '-'
}
function statusLabel(value) {
return getDictLabel(DICT_TYPE.COMMON_STATUS, value, textValue(value))
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return t('functionCommon.no')
if (value === true) return t('functionCommon.yes')
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function handleSearch() {}
function loadMore() {}
function onScroll(e) {
showGoTop.value = (e?.detail?.scrollTop || 0) > 600
}
function goTop() {
scrollTop.value = 0
}
function openDetail(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
uni.navigateTo({
url: `/pages_function/pages/materialCategory/detail?id=${encodeURIComponent(String(item.id))}`
})
}
function openCreate() {
formMode.value = 'create'
resetForm()
formPopupRef.value?.open()
}
async function openEdit(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdEdit'), icon: 'none' })
return
}
try {
const { getProductCategory } = await import('@/api/erp/productCategory')
const res = await getProductCategory(item.id)
const detail = normalizeDetail(res)
formMode.value = 'update'
formData.id = detail?.id
formData.parentId = detail?.parentId ?? 0
formData.code = String(detail?.code ?? '')
formData.name = String(detail?.name ?? '')
formData.sort = detail?.sort ?? 0
formData.status = detail?.status ?? 0
formPopupRef.value?.open()
} catch (e) {
uni.showToast({ title: t('materialCategory.loadEditFailed'), icon: 'none' })
}
}
function confirmDelete(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdDelete'), icon: 'none' })
return
}
uni.showModal({
title: t('functionCommon.confirmDelete'),
content: t('materialCategory.confirmDeleteContent', { name: textValue(item?.name) }),
success: async (res) => {
if (!res.confirm) return
try {
await deleteProductCategory(item.id)
uni.showToast({ title: t('functionCommon.deleteSuccess'), icon: 'success' })
await fetchList()
} catch (e) {
uni.showToast({ title: t('functionCommon.deleteFailed'), icon: 'none' })
}
}
})
}
function onParentChange(e) {
const idx = e.detail.value
formData.parentId = parentCategoryOptions.value[idx]?.value ?? 0
}
function onStatusChange(e) {
const idx = e.detail.value
formData.status = statusOptions.value[idx]?.value ?? 0
}
function closeForm() {
formPopupRef.value?.close()
}
async function submitForm() {
if (!formData.code.trim()) {
uni.showToast({ title: t('materialCategory.validatorCodeRequired'), icon: 'none' })
return
}
if (!formData.name.trim()) {
uni.showToast({ title: t('materialCategory.validatorNameRequired'), icon: 'none' })
return
}
if (formData.sort === undefined || formData.sort === null || String(formData.sort).trim() === '') {
uni.showToast({ title: t('materialCategory.validatorSortRequired'), icon: 'none' })
return
}
const payload = {
id: formMode.value === 'update' ? formData.id : undefined,
parentId: formData.parentId ?? 0,
code: formData.code.trim(),
name: formData.name.trim(),
sort: Number(formData.sort),
status: formData.status
}
try {
if (formMode.value === 'create') {
await createProductCategory(payload)
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
} else {
await updateProductCategory(payload)
uni.showToast({ title: t('functionCommon.updateSuccess'), icon: 'success' })
}
closeForm()
await fetchList()
} catch (e) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
}
}
function resetForm() {
formData.id = undefined
formData.parentId = 0
formData.code = ''
formData.name = ''
formData.sort = 0
formData.status = 0
}
function normalizeDetail(res) {
const root = res && res.data !== undefined ? res.data : res
if (root?.data && typeof root.data === 'object') return root.data
if (root && typeof root === 'object') return root
return {}
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.search-card { background: #ffffff; margin: 20rpx 24rpx; border-radius: 18rpx; padding: 20rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.04); }
.search-row { display: flex; align-items: center; gap: 16rpx; }
.search-input-wrap { flex: 1; display: flex; align-items: center; background: #f5f7fa; border-radius: 44rpx; padding: 0 20rpx; }
.search-icon { margin-right: 12rpx; }
.search-input { flex: 1; height: 72rpx; font-size: 28rpx; color: #333; background: transparent; }
.search-btn { min-width: 120rpx; height: 72rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); border-radius: 14rpx; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 28rpx; font-weight: 600; }
.list-scroll { height: calc(100vh - 300rpx); }
.list-wrap { padding: 0 24rpx 30rpx; }
.type-card { background: #ffffff; border-radius: 18rpx; padding: 24rpx; margin-bottom: 18rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.05); }
.card-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 18rpx; border-bottom: 1rpx solid #edf0f3; }
.header-left { display: flex; flex-direction: column; }
.type-name { font-size: 32rpx; font-weight: 600; color: #1a3a5c; margin-bottom: 8rpx; }
.type-code { font-size: 24rpx; color: #8a9099; }
.card-body { padding-top: 16rpx; }
.row { display: flex; justify-content: space-between; align-items: center; margin-top: 12rpx; }
.label { font-size: 26rpx; color: #8a9099; }
.value { font-size: 27rpx; color: #30363d; max-width: 62%; text-align: right; }
.text-success { color: #52c41a; }
.text-danger { color: #ff4d4f; }
.card-actions { margin-top: 24rpx; display: flex; justify-content: flex-end; gap: 14rpx; }
.action-btn { width: 60rpx; height: 60rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.edit-btn { background: #1a3a5c; }
.delete-btn { background: #ff4d4f; }
.hint { text-align: center; color: #909399; padding: 24rpx 0; }
.go-top-btn { position: fixed; right: 28rpx; bottom: 254rpx; width: 88rpx; height: 88rpx; border-radius: 44rpx; background: rgba(26, 58, 92, 0.9); display: flex; align-items: center; justify-content: center; z-index: 99; box-shadow: 0 6rpx 16rpx rgba(26, 58, 92, 0.25); }
.add-btn { position: fixed; right: 28rpx; bottom: 140rpx; width: 96rpx; height: 96rpx; border-radius: 48rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); display: flex; align-items: center; justify-content: center; box-shadow: 0 8rpx 24rpx rgba(26, 58, 92, 0.32); z-index: 99; }
.add-icon { color: #ffffff; font-size: 56rpx; line-height: 1; }
.popup-content { width: 680rpx; max-height: 80vh; border-radius: 20rpx; overflow: hidden; }
.popup-header { padding: 24rpx; display: flex; align-items: center; justify-content: space-between; border-bottom: 1rpx solid #edf0f3; position: relative; }
.popup-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; }
.popup-close { position: absolute; right: 24rpx; top: 20rpx; width: 48rpx; height: 48rpx; display: flex; align-items: center; justify-content: center; }
.close-icon { font-size: 42rpx; color: #9aa0a6; line-height: 1; }
.form-scroll { max-height: 56vh; }
.form-content { padding: 22rpx 28rpx; }
.form-item { margin-bottom: 18rpx; }
.form-label { display: block; font-size: 25rpx; color: #5f6b7a; margin-bottom: 10rpx; }
.required-star { color: #e34d59; }
.form-input { height: 76rpx; padding: 0 20rpx; border-radius: 12rpx; background: #f5f7fa; font-size: 28rpx; color: #30363d; }
.form-picker { height: 76rpx; padding: 0 20rpx; border-radius: 12rpx; background: #f5f7fa; font-size: 28rpx; color: #30363d; display: flex; align-items: center; }
.form-picker.placeholder { color: #999; }
.form-footer { height: 88rpx; display: flex; border-top: 1rpx solid #edf0f3; }
.footer-btn { flex: 1; display: flex; align-items: center; justify-content: center; }
.btn-text { font-size: 28rpx; }
.cancel-btn { background: #edf0f4; }
.cancel-btn .btn-text { color: #586070; }
.confirm-btn { background: #1a3a5c; }
.confirm-btn .btn-text { color: #fff; }
</style>

@ -0,0 +1,153 @@
<template>
<view class="page-container">
<view class="fixed-header">
<NavBar :title="t('materialInfo.detailTitle')" />
</view>
<scroll-view scroll-y class="detail-scroll">
<view class="content-section">
<!-- 基础信息 -->
<view class="info-card">
<view class="card-title">{{ t('materialInfo.basicInfo') }}</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">{{ t('materialInfo.barCode') }}</text>
<text class="info-value">{{ fieldValue('barCode') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInfo.name') }}</text>
<text class="info-value">{{ fieldValue('name') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInfo.category') }}</text>
<text class="info-value">{{ fieldValue('subCategoryName') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInfo.unit') }}</text>
<text class="info-value">{{ fieldValue('unitName') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInfo.standard') }}</text>
<text class="info-value">{{ fieldValue('standard') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInfo.expiryDay') }}</text>
<text class="info-value">{{ fieldValue('expiryDay') }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInfo.status') }}</text>
<text :class="['info-value', String(detailData?.status) === '0' ? 'text-success' : 'text-danger']">{{ statusText }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('materialInfo.createTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData?.createTime) }}</text>
</view>
<view class="info-row remark-row">
<text class="info-label">{{ t('materialInfo.remark') }}</text>
<text class="info-value remark-value">{{ fieldValue('remark') }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getProduct } from '@/api/erp/productInfo'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const { t } = useI18n()
const detailId = ref(undefined)
const detailData = ref(null)
const statusText = computed(() => {
return getDictLabel(DICT_TYPE.COMMON_STATUS, detailData.value?.status, textValue(detailData.value?.status))
})
onLoad(async (query) => {
const id = query?.id ? decodeURIComponent(String(query.id)) : ''
detailId.value = id || undefined
await initAllDict()
await fetchDetail()
})
async function fetchDetail() {
if (!detailId.value) {
uni.showToast({ title: t('materialInfo.noId'), icon: 'none' })
return
}
try {
const res = await getProduct(detailId.value)
detailData.value = normalizeDetail(res)
} catch (e) {
uni.showToast({ title: t('materialInfo.loadFailed'), icon: 'none' })
}
}
function normalizeDetail(res) {
const root = res && res.data !== undefined ? res.data : res
if (root?.data && typeof root.data === 'object') return root.data
if (root && typeof root === 'object') return root
return {}
}
function fieldValue(field) {
return textValue(detailData.value ? detailData.value[field] : undefined)
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return t('functionCommon.no')
if (value === true) return t('functionCommon.yes')
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function formatDateTime(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = value
const pad = (n) => String(n).padStart(2, '0')
return `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}:${pad(ss)}`
}
const text = String(value).trim()
if (!text) return '-'
const numeric = Number(text)
if (Number.isFinite(numeric)) {
const timestamp = text.length === 10 ? numeric * 1000 : numeric
const date = new Date(timestamp)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
}
const date = new Date(text)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
return text
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.fixed-header { position: sticky; top: 0; z-index: 10; }
.detail-scroll { height: calc(100vh - 120rpx); }
.content-section { padding: 0 24rpx 24rpx; }
.info-card { margin-top: 20rpx; background: #ffffff; border-radius: 20rpx; padding: 28rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); }
.card-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; margin-bottom: 18rpx; }
.info-list { background: #ffffff; }
.info-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 18rpx 0; border-bottom: 1rpx solid #edf0f3; }
.info-row:last-child { border-bottom: none; }
.info-label { font-size: 27rpx; color: #8a9099; width: 220rpx; }
.info-value { flex: 1; text-align: right; font-size: 28rpx; color: #30363d; line-height: 1.45; }
.remark-row { border-bottom: none; }
.remark-value { white-space: pre-wrap; }
.text-success { color: #52c41a; }
.text-danger { color: #ff4d4f; }
</style>

@ -0,0 +1,522 @@
<template>
<view class="page-container">
<NavBar :title="t('materialInfo.moduleName')" :subTitle="t('materialInfo.subTitle')" />
<!-- 搜索区域 -->
<view class="search-card">
<view class="search-row">
<view class="search-input-wrap">
<text class="iconfont icon-search search-icon"></text>
<input v-model="searchKeyword" class="search-input" :placeholder="t('materialInfo.searchPlaceholder')" @confirm="handleSearch" />
</view>
<view class="search-btn" @click="handleSearch">{{ t('functionCommon.search') }}</view>
</view>
<!-- 分类Tab -->
<view class="tab-wrap">
<view v-for="item in parentCategoryList" :key="item.id" :class="['tab-item', activeCategoryId === item.id ? 'tab-active' : '']" @click="switchCategory(item)">
<text class="tab-text">{{ item.name }}</text>
</view>
</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="type-card" @click="openDetail(item)">
<view class="card-header">
<view class="header-left">
<text class="type-name">{{ textValue(item.name) }}</text>
<text class="type-code">{{ textValue(item.barCode) }}</text>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('materialInfo.standard') }}</text>
<text class="value">{{ textValue(item.standard) }}</text>
</view>
<view class="row">
<text class="label">{{ t('materialInfo.category') }}</text>
<text class="value">{{ textValue(item.subCategoryName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('materialInfo.unit') }}</text>
<text class="value">{{ textValue(item.unitName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('materialInfo.status') }}</text>
<text :class="['value', String(item.status) === '0' ? 'text-success' : 'text-danger']">{{ statusLabel(item.status) }}</text>
</view>
</view>
<view class="card-actions">
<view class="action-btn edit-btn" @click.stop="openEdit(item)">
<uni-icons type="compose" size="18" color="#ffffff"></uni-icons>
</view>
<view class="action-btn delete-btn" @click.stop="confirmDelete(item)">
<uni-icons type="trash" size="18" color="#ffffff"></uni-icons>
</view>
</view>
</view>
<view v-if="loading && pageNo === 1" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!list.length" class="hint">{{ t('materialInfo.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="#1a3a5c"></uni-icons>
</view>
<!-- 新增按钮 -->
<view class="add-btn" @click="openCreate">
<text class="add-icon">+</text>
</view>
<!-- 新增/编辑弹框 -->
<uni-popup ref="formPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ formMode === 'create' ? t('materialInfo.createTitle') : t('materialInfo.editTitle') }}</text>
<view class="popup-close" @click="closeForm">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="form-scroll">
<view class="form-content">
<view class="form-item">
<text class="form-label">{{ t('materialInfo.barCode') }} <text v-if="!formData.isCode" class="required-star">*</text></text>
<view class="code-row">
<input v-model="formData.barCode" class="form-input code-input" :placeholder="t('materialInfo.placeholderBarCode')" :disabled="formData.isCode || formMode === 'update'" />
<view class="code-switch" @click="toggleAutoCode">
<text :class="['switch-text', formData.isCode ? 'switch-active' : '']">{{ t('materialInfo.autoCode') }}</text>
</view>
</view>
</view>
<view class="form-item">
<text class="form-label">{{ t('materialInfo.name') }} <text class="required-star">*</text></text>
<input v-model="formData.name" class="form-input" :placeholder="t('materialInfo.placeholderName')" />
</view>
<view class="form-item">
<text class="form-label">{{ t('materialInfo.category') }} <text class="required-star">*</text></text>
<picker mode="selector" :range="categoryPickerOptions" range-key="label" @change="onCategoryChange">
<view :class="['form-picker', !formData.categoryId ? 'placeholder' : '']">
{{ formData.categoryId ? getCategoryName(formData.categoryId) : t('materialInfo.placeholderCategory') }}
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">{{ t('materialInfo.unit') }} <text class="required-star">*</text></text>
<picker mode="selector" :range="unitOptions" range-key="label" @change="onUnitChange">
<view :class="['form-picker', !formData.unitId ? 'placeholder' : '']">
{{ formData.unitId ? getUnitName(formData.unitId) : t('materialInfo.placeholderUnit') }}
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">{{ t('materialInfo.standard') }}</text>
<input v-model="formData.standard" class="form-input" :placeholder="t('materialInfo.placeholderStandard')" />
</view>
<view class="form-item">
<text class="form-label">{{ t('materialInfo.expiryDay') }}</text>
<input v-model="formData.expiryDay" class="form-input" type="number" :placeholder="t('materialInfo.placeholderExpiryDay')" />
</view>
<view class="form-item">
<text class="form-label">{{ t('materialInfo.status') }} <text class="required-star">*</text></text>
<picker mode="selector" :range="statusOptions" range-key="label" @change="onStatusChange">
<view :class="['form-picker', formData.status === undefined ? 'placeholder' : '']">
{{ formData.status !== undefined ? statusLabel(formData.status) : t('materialInfo.placeholderStatus') }}
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">{{ t('materialInfo.remark') }}</text>
<textarea v-model="formData.remark" class="form-textarea" :placeholder="t('materialInfo.placeholderRemark')" />
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeForm"><text class="btn-text">{{ t('functionCommon.cancel') }}</text></view>
<view class="footer-btn confirm-btn" @click="submitForm"><text class="btn-text">{{ t('functionCommon.save') }}</text></view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getProductPage, getProduct, createProduct, updateProduct, deleteProduct, getProductUnitSimpleList } from '@/api/erp/productInfo'
import { getProductCategorySimpleList } from '@/api/erp/productCategory'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const { t } = useI18n()
const formPopupRef = ref(null)
const searchKeyword = ref('')
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const finished = ref(false)
const pageNo = ref(1)
const pageSize = ref(10)
const total = ref(0)
const scrollTop = ref(0)
const showGoTop = ref(false)
const formMode = ref('create')
const activeCategoryId = ref(undefined)
const parentCategoryList = ref([])
const categoryList = ref([])
const unitList = ref([])
const formData = reactive({
id: undefined,
name: '',
barCode: '',
isCode: false,
categoryId: undefined,
unitId: undefined,
status: 0,
standard: '',
remark: '',
expiryDay: undefined
})
const statusOptions = computed(() => [
{ label: t('materialInfo.statusEnable'), value: 0 },
{ label: t('materialInfo.statusDisable'), value: 1 }
])
const categoryPickerOptions = computed(() => {
return categoryList.value.map(item => ({ label: item.name || '', value: item.id }))
})
const unitOptions = computed(() => {
return unitList.value.map(item => ({ label: item.name || '', value: item.id }))
})
onLoad(async () => {
await initAllDict()
await loadCategories()
await fetchList(true)
})
async function loadCategories() {
try {
const res = await getProductCategorySimpleList()
const data = normalizeListData(res)
categoryList.value = Array.isArray(data) ? data : []
const parents = []
data.forEach(item => {
if (!item.parentId || item.parentId === 0) {
parents.push(item)
}
})
parents.sort((a, b) => (a.sort || 0) - (b.sort || 0))
parentCategoryList.value = parents
if (parents.length > 0) {
activeCategoryId.value = parents[0].id
}
} catch (e) {}
try {
const res = await getProductUnitSimpleList()
unitList.value = normalizeListData(res)
} catch (e) {}
}
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,
name: searchKeyword.value.trim() || undefined,
categoryId: activeCategoryId.value
}
const res = await getProductPage(params)
const page = normalizePageData(res)
total.value = page.total
if (reset) list.value = page.list
else list.value = [...list.value, ...page.list]
finished.value = list.value.length >= total.value || 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 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 || root?.data?.records || []
const candidateTotal = root?.total ?? root?.data?.total ?? (Array.isArray(candidateList) ? candidateList.length : 0)
return { list: Array.isArray(candidateList) ? candidateList : [], total: Number(candidateTotal || 0) }
}
function normalizeListData(res) {
const root = res && res.data !== undefined ? res.data : res
if (Array.isArray(root)) return root
if (root?.data && Array.isArray(root.data)) return root.data
return []
}
function statusLabel(value) {
return getDictLabel(DICT_TYPE.COMMON_STATUS, value, textValue(value))
}
function getCategoryName(id) {
const item = categoryList.value.find(c => c.id === id)
return item ? (item.name || '-') : '-'
}
function getUnitName(id) {
const item = unitList.value.find(u => u.id === id)
return item ? (item.name || '-') : '-'
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return t('functionCommon.no')
if (value === true) return t('functionCommon.yes')
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function handleSearch() { fetchList(true) }
function switchCategory(item) {
activeCategoryId.value = item.id
fetchList(true)
}
function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
fetchList(false)
}
function onScroll(e) { showGoTop.value = (e?.detail?.scrollTop || 0) > 600 }
function goTop() { scrollTop.value = 0 }
function openDetail(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
uni.navigateTo({
url: `/pages_function/pages/materialInfo/detail?id=${encodeURIComponent(String(item.id))}`
})
}
function openCreate() {
formMode.value = 'create'
resetForm()
formPopupRef.value?.open()
}
async function openEdit(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdEdit'), icon: 'none' })
return
}
try {
const res = await getProduct(item.id)
const detail = normalizeDetail(res)
formMode.value = 'update'
formData.id = detail?.id
formData.name = String(detail?.name ?? '')
formData.barCode = String(detail?.barCode ?? '')
formData.isCode = Boolean(detail?.isCode)
formData.categoryId = detail?.categoryId
formData.unitId = detail?.unitId
formData.status = detail?.status ?? 0
formData.standard = String(detail?.standard ?? '')
formData.remark = String(detail?.remark ?? '')
formData.expiryDay = detail?.expiryDay
formPopupRef.value?.open()
} catch (e) {
uni.showToast({ title: t('materialInfo.loadEditFailed'), icon: 'none' })
}
}
function confirmDelete(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdDelete'), icon: 'none' })
return
}
uni.showModal({
title: t('functionCommon.confirmDelete'),
content: t('materialInfo.confirmDeleteContent', { name: textValue(item?.name) }),
success: async (res) => {
if (!res.confirm) return
try {
await deleteProduct(item.id)
uni.showToast({ title: t('functionCommon.deleteSuccess'), icon: 'success' })
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.deleteFailed'), icon: 'none' })
}
}
})
}
function toggleAutoCode() {
if (formMode.value === 'update') return
formData.isCode = !formData.isCode
if (formData.isCode) formData.barCode = ''
}
function onCategoryChange(e) {
const idx = e.detail.value
formData.categoryId = categoryPickerOptions.value[idx]?.value
}
function onUnitChange(e) {
const idx = e.detail.value
formData.unitId = unitOptions.value[idx]?.value
}
function onStatusChange(e) {
const idx = e.detail.value
formData.status = statusOptions.value[idx]?.value ?? 0
}
function closeForm() { formPopupRef.value?.close() }
async function submitForm() {
if (!formData.isCode && !formData.barCode.trim()) {
uni.showToast({ title: t('materialInfo.validatorBarCodeRequired'), icon: 'none' })
return
}
if (!formData.name.trim()) {
uni.showToast({ title: t('materialInfo.validatorNameRequired'), icon: 'none' })
return
}
if (!formData.categoryId) {
uni.showToast({ title: t('materialInfo.validatorCategoryRequired'), icon: 'none' })
return
}
if (!formData.unitId) {
uni.showToast({ title: t('materialInfo.validatorUnitRequired'), icon: 'none' })
return
}
const payload = {
id: formMode.value === 'update' ? formData.id : undefined,
name: formData.name.trim(),
barCode: formData.isCode ? undefined : formData.barCode.trim(),
isCode: formData.isCode,
categoryId: formData.categoryId,
unitId: formData.unitId,
status: formData.status,
standard: formData.standard.trim() || undefined,
remark: formData.remark.trim() || undefined,
expiryDay: formData.expiryDay !== undefined && formData.expiryDay !== '' ? Number(formData.expiryDay) : undefined
}
try {
if (formMode.value === 'create') {
await createProduct(payload)
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
} else {
await updateProduct(payload)
uni.showToast({ title: t('functionCommon.updateSuccess'), icon: 'success' })
}
closeForm()
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
}
}
function resetForm() {
formData.id = undefined
formData.name = ''
formData.barCode = ''
formData.isCode = false
formData.categoryId = undefined
formData.unitId = undefined
formData.status = 0
formData.standard = ''
formData.remark = ''
formData.expiryDay = undefined
}
function normalizeDetail(res) {
const root = res && res.data !== undefined ? res.data : res
if (root?.data && typeof root.data === 'object') return root.data
if (root && typeof root === 'object') return root
return {}
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.search-card { background: #ffffff; margin: 20rpx 24rpx; border-radius: 18rpx; padding: 20rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.04); }
.search-row { display: flex; align-items: center; gap: 16rpx; }
.search-input-wrap { flex: 1; display: flex; align-items: center; background: #f5f7fa; border-radius: 44rpx; padding: 0 20rpx; }
.search-icon { margin-right: 12rpx; }
.search-input { flex: 1; height: 72rpx; font-size: 28rpx; color: #333; background: transparent; }
.search-btn { min-width: 120rpx; height: 72rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); border-radius: 14rpx; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 28rpx; font-weight: 600; }
.tab-wrap { margin-top: 16rpx; display: flex; flex-wrap: wrap; gap: 12rpx; }
.tab-item { padding: 10rpx 24rpx; border-radius: 24rpx; background: #f5f7fa; }
.tab-active { background: #1a3a5c; }
.tab-active .tab-text { color: #fff; }
.tab-text { font-size: 24rpx; color: #5f6b7a; }
.list-scroll { height: calc(100vh - 380rpx); }
.list-wrap { padding: 0 24rpx 30rpx; }
.type-card { background: #ffffff; border-radius: 18rpx; padding: 24rpx; margin-bottom: 18rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.05); }
.card-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 18rpx; border-bottom: 1rpx solid #edf0f3; }
.header-left { display: flex; flex-direction: column; }
.type-name { font-size: 32rpx; font-weight: 600; color: #1a3a5c; margin-bottom: 8rpx; }
.type-code { font-size: 24rpx; color: #8a9099; }
.card-body { padding-top: 16rpx; }
.row { display: flex; justify-content: space-between; align-items: center; margin-top: 12rpx; }
.label { font-size: 26rpx; color: #8a9099; }
.value { font-size: 27rpx; color: #30363d; max-width: 62%; text-align: right; }
.text-success { color: #52c41a; }
.text-danger { color: #ff4d4f; }
.card-actions { margin-top: 24rpx; display: flex; justify-content: flex-end; gap: 14rpx; }
.action-btn { width: 60rpx; height: 60rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.edit-btn { background: #1a3a5c; }
.delete-btn { background: #ff4d4f; }
.hint { text-align: center; color: #909399; padding: 24rpx 0; }
.go-top-btn { position: fixed; right: 28rpx; bottom: 254rpx; width: 88rpx; height: 88rpx; border-radius: 44rpx; background: rgba(26, 58, 92, 0.9); display: flex; align-items: center; justify-content: center; z-index: 99; box-shadow: 0 6rpx 16rpx rgba(26, 58, 92, 0.25); }
.add-btn { position: fixed; right: 28rpx; bottom: 140rpx; width: 96rpx; height: 96rpx; border-radius: 48rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); display: flex; align-items: center; justify-content: center; box-shadow: 0 8rpx 24rpx rgba(26, 58, 92, 0.32); z-index: 99; }
.add-icon { color: #ffffff; font-size: 56rpx; line-height: 1; }
.popup-content { width: 680rpx; max-height: 80vh; border-radius: 20rpx; overflow: hidden; }
.popup-header { padding: 24rpx; display: flex; align-items: center; justify-content: space-between; border-bottom: 1rpx solid #edf0f3; position: relative; }
.popup-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; }
.popup-close { position: absolute; right: 24rpx; top: 20rpx; width: 48rpx; height: 48rpx; display: flex; align-items: center; justify-content: center; }
.close-icon { font-size: 42rpx; color: #9aa0a6; line-height: 1; }
.form-scroll { max-height: 56vh; }
.form-content { padding: 22rpx 28rpx; }
.form-item { margin-bottom: 18rpx; }
.form-label { display: block; font-size: 25rpx; color: #5f6b7a; margin-bottom: 10rpx; }
.required-star { color: #e34d59; }
.form-input { height: 76rpx; padding: 0 20rpx; border-radius: 12rpx; background: #f5f7fa; font-size: 28rpx; color: #30363d; }
.form-textarea { width: 100%; min-height: 130rpx; padding: 18rpx 20rpx; border-radius: 12rpx; background: #f5f7fa; font-size: 28rpx; color: #30363d; box-sizing: border-box; }
.form-picker { height: 76rpx; padding: 0 20rpx; border-radius: 12rpx; background: #f5f7fa; font-size: 28rpx; color: #30363d; display: flex; align-items: center; }
.form-picker.placeholder { color: #999; }
.code-row { display: flex; align-items: center; gap: 12rpx; }
.code-input { flex: 1; }
.code-switch { padding: 10rpx 16rpx; background: #f5f7fa; border-radius: 8rpx; }
.switch-text { font-size: 22rpx; color: #999; }
.switch-active { color: #1a3a5c; font-weight: 600; }
.form-footer { height: 88rpx; display: flex; border-top: 1rpx solid #edf0f3; }
.footer-btn { flex: 1; display: flex; align-items: center; justify-content: center; }
.btn-text { font-size: 28rpx; }
.cancel-btn { background: #edf0f4; }
.cancel-btn .btn-text { color: #586070; }
.confirm-btn { background: #1a3a5c; }
.confirm-btn .btn-text { color: #fff; }
</style>

@ -0,0 +1,730 @@
<template>
<view class="page-container">
<NavBar title="模具详情" />
<view class="content-section">
<view class="info-card">
<view class="card-title">基本信息</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">模具名称</text>
<text class="info-value">{{ getDetailField('name') }}</text>
</view>
<view class="info-row">
<text class="info-label">模具编号</text>
<text class="info-value">{{ getDetailField('code') }}</text>
</view>
<view class="info-row">
<text class="info-label">模具状态</text>
<u-tag :text="statusLabel" :type="statusTagType" size="mini" />
</view>
<view class="info-row">
<text class="info-label">模具型号</text>
<text class="info-value">{{ getDetailField('brandName') }}</text>
</view>
<view class="info-row">
<text class="info-label">模具规格</text>
<text class="info-value">{{ getDetailField('moldType') }}</text>
</view>
<view class="info-row">
<text class="info-label">工序</text>
<text class="info-value">{{ getDetailField('orgType') }}</text>
</view>
<view class="info-row">
<text class="info-label">模穴数</text>
<text class="info-value">{{ getDetailField('moldSize') }}</text>
</view>
<view class="info-row">
<text class="info-label">使用次数/</text>
<text class="info-value">{{ getDetailField('useTime') }}</text>
</view>
<view class="info-row">
<text class="info-label">使用设备</text>
<text class="info-value">{{ machineLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">入库日期</text>
<text class="info-value">{{ inTimeLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">是否启用</text>
<text class="info-value">{{ enableLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">备注</text>
<text class="info-value">{{ getDetailField('remark') }}</text>
</view>
</view>
</view>
<view class="info-card">
<view class="card-title">履历</view>
<view class="tabs-box">
<u-tabs activeColor="#1a3a5c" :list="tabList" :current="currentTab" :is-scroll="false"
@change="handleTabChange" />
</view>
<view>
<view v-if="currentTab === 0">
<view v-if="!inspectionGroups.length" class="empty"></view>
<view v-for="group in inspectionGroups" :key="group.key" class="history-group">
<view class="history-group-head">
<text class="history-group-time">[{{ group.time }}]</text>
<text class="history-group-operator">操作人: {{ group.operator }}</text>
</view>
<view v-for="item in group.items" :key="item.key" class="history-item">
<view class="history-item-head">
<text class="result-badge" :class="'result-' + item.resultType">{{ item.resultLabel }}</text>
<text class="history-item-name">{{ item.name }}</text>
</view>
<view class="history-item-body">
<view class="history-row">
<text class="history-label">点检方式</text>
<text class="history-value">{{ detailValue(item.method) }}</text>
</view>
<view class="history-row">
<text class="history-label">判定标准</text>
<text class="history-value">{{ detailValue(item.criteria) }}</text>
</view>
<view class="history-row">
<text class="history-label">点检时间</text>
<text class="history-value">{{ detailValue(item.taskTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">创建时间</text>
<text class="history-value">{{ detailValue(item.createTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">备注</text>
<text class="history-value">{{ detailValue(item.remark) }}</text>
</view>
<view v-if="item.images && item.images.length" class="history-images">
<image v-for="img in item.images" :key="img" class="history-image" :src="img" mode="aspectFill"
@click="previewImages(item.images, img)" />
</view>
</view>
</view>
</view>
</view>
<view v-else-if="currentTab === 1">
<view v-if="!maintainGroups.length" class="empty"></view>
<view v-for="group in maintainGroups" :key="group.key" class="history-group">
<view class="history-group-head">
<text class="history-group-time">[{{ group.time }}]</text>
<text class="history-group-operator">操作人: {{ group.operator }}</text>
</view>
<view v-for="item in group.items" :key="item.key" class="history-item">
<view class="history-item-head">
<text class="result-badge" :class="'result-' + item.resultType">{{ item.resultLabel }}</text>
<text class="history-item-name">{{ item.name }}</text>
</view>
<view class="history-item-body">
<view class="history-row">
<text class="history-label">保养方式</text>
<text class="history-value">{{ detailValue(item.method) }}</text>
</view>
<view class="history-row">
<text class="history-label">判定标准</text>
<text class="history-value">{{ detailValue(item.criteria) }}</text>
</view>
<view class="history-row">
<text class="history-label">保养时间</text>
<text class="history-value">{{ detailValue(item.taskTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">创建时间</text>
<text class="history-value">{{ detailValue(item.createTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">备注</text>
<text class="history-value">{{ detailValue(item.remark) }}</text>
</view>
<view v-if="item.images && item.images.length" class="history-images">
<image v-for="img in item.images" :key="img" class="history-image" :src="img" mode="aspectFill"
@click="previewImages(item.images, img)" />
</view>
</view>
</view>
</view>
</view>
<view v-else>
<view v-if="!repairGroups.length" class="empty"></view>
<view v-for="group in repairGroups" :key="group.key" class="repair-group">
<view class="repair-group-head">
<text class="repair-group-name">{{ group.name }}</text>
<text class="repair-group-meta">{{ group.items.length }}</text>
</view>
<view v-for="row in group.items" :key="row.key" class="repair-item">
<view class="repair-item-head">
<text class="repair-tag">{{ detailValue(row.subjectCode) }}</text>
<text class="repair-title">{{ detailValue(row.subjectName) }}</text>
</view>
<view class="repair-item-body">
<view class="history-row">
<text class="history-label">项目内容</text>
<text class="history-value">{{ detailValue(row.subjectContent) }}</text>
</view>
<view class="history-row">
<text class="history-label">维修结果</text>
<text class="history-value">
<text class="result-badge" :class="'result-' + row.resultType">{{ row.resultLabel }}</text>
</text>
</view>
<view class="history-row">
<text class="history-label">备注</text>
<text class="history-value">{{ detailValue(row.remark) }}</text>
</view>
<view class="history-row">
<text class="history-label">完成日期</text>
<text class="history-value">{{ detailValue(row.finishDateLabel) }}</text>
</view>
<view v-if="row.images && row.images.length" class="history-images">
<image v-for="img in row.images" :key="img" class="history-image" :src="img" mode="aspectFill"
@click="previewImages(row.images, img)" />
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import NavBar from '@/components/common/NavBar.vue'
import {
getMoldDetail,
getMoldInspectionByMoldId,
getMoldMaintenanceByMoldId,
getMoldRepairListByMoldId
} from '@/api/mes/mold'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const loading = ref(false)
const moldId = ref(undefined)
const moldCode = ref(undefined)
const moldType = ref(undefined)
const detailData = ref(null)
const inspectionList = ref([])
const maintainList = ref([])
const repairList = ref([])
const tabList = ref([{ name: '点检履历' }, { name: '保养履历' }, { name: '维修履历' }])
const currentTab = ref(0)
function get(obj, key) {
if (!obj) return undefined
return obj[key]
}
function getDetailField(field) {
const d = detailData.value
return detailValue(d ? d[field] : undefined)
}
const statusMeta = computed(() => {
const d = detailData.value
const raw = d ? d.status : undefined
const label = getDictLabel(DICT_TYPE.ERP_MOLD_STATUS, raw, detailValue(raw))
return formatStatus(label)
})
const statusLabel = computed(() => statusMeta.value.label)
const statusType = computed(() => statusMeta.value.type)
const statusTagType = computed(() => {
if (statusType.value === 'error') return 'error'
if (statusType.value === 'warning') return 'warning'
return 'success'
})
const machineLabel = computed(() => {
const d = detailData.value
const v = d ? d.machineName || d.machineId : undefined
return detailValue(v)
})
const inTimeLabel = computed(() => {
const d = detailData.value
return formatDateOnly(d ? d.inTime : undefined)
})
const enableLabel = computed(() => {
const d = detailData.value
return formatEnable(d ? d.isEnable : undefined)
})
const inspectionGroups = computed(() =>
buildStepGroups(inspectionList.value, {
timeFieldCandidates: ['taskTime', 'inspectionTime', 'createTime'],
nameFieldCandidates: ['inspectionItemName', 'name', 'itemName'],
resultFieldCandidates: ['inspectionResult', 'result'],
methodFieldCandidates: ['inspectionMethod', 'method'],
criteriaFieldCandidates: ['judgmentCriteria', 'criteria'],
imagesFieldCandidates: ['images'],
remarkFieldCandidates: ['remark']
})
)
const maintainGroups = computed(() =>
buildStepGroups(maintainList.value, {
timeFieldCandidates: ['taskTime', 'inspectionTime', 'createTime'],
nameFieldCandidates: ['maintainItemName', 'inspectionItemName', 'name', 'itemName'],
resultFieldCandidates: ['maintainResult', 'inspectionResult', 'result'],
methodFieldCandidates: ['inspectionMethod', 'method'],
criteriaFieldCandidates: ['judgmentCriteria', 'criteria'],
imagesFieldCandidates: ['images'],
remarkFieldCandidates: ['remark']
})
)
const repairGroups = computed(() => {
const groupsMap = new Map()
const rows = Array.isArray(repairList.value) ? repairList.value : []
for (const row of rows) {
const groupKey = String(
(row && (row.repairCode || row.repairId || row.subjectName || row.id)) ? (row.repairCode || row.repairId || row.subjectName || row.id) : '-'
)
if (!groupsMap.has(groupKey)) {
groupsMap.set(groupKey, {
key: groupKey,
name: String((row && (row.repairName || row.repairCode)) ? (row.repairName || row.repairCode) : groupKey),
items: []
})
}
const resultMeta = formatResult(row ? (row.repairResult !== undefined ? row.repairResult : row.result) : undefined)
groupsMap.get(groupKey).items.push({
key: String(row && row.id !== undefined && row.id !== null ? row.id : `${groupKey}_${Math.random()}`),
subjectCode: row ? row.subjectCode : undefined,
subjectName: row ? row.subjectName : undefined,
subjectContent: row ? row.subjectContent : undefined,
remark: row ? row.remark : undefined,
finishDateLabel: formatDateOnly(row ? row.finishDate : undefined),
resultLabel: resultMeta.label,
resultType: resultMeta.type,
images: parseImages(row ? (row.malfunctionUrl || row.malfunctionImages) : undefined)
})
}
return Array.from(groupsMap.values())
})
function handleTabChange(e) {
const idx = e && typeof e === 'object' ? e.index : e
currentTab.value = Number(idx === 0 ? 0 : idx || 0)
}
onLoad((query) => {
const rawId = query && query.id
const decoded = rawId ? decodeURIComponent(String(rawId)) : ''
moldId.value = decoded ? decoded : undefined
moldType.value = query && query.type
moldCode.value = query && query.code
fetchAll()
})
async function fetchAll() {
if (!moldId.value && !moldCode.value) {
uni.showToast({ title: '缺少模具ID', icon: 'none' })
return
}
loading.value = true
try {
const isScan = moldType.value === 'scan'
const idParam = isScan ? { id: moldId.value } : { code: moldCode.value }
const moldIdParam = isScan ? { moldId: moldId.value } : { code: moldCode.value }
const [detailRes, inspectionRes, maintainRes, repairRes] = await Promise.all([
getMoldDetail(moldId.value, idParam),
getMoldInspectionByMoldId(moldId.value, moldIdParam),
getMoldMaintenanceByMoldId(moldId.value, moldIdParam),
getMoldRepairListByMoldId(moldId.value, moldIdParam)
])
detailData.value = detailRes.data
inspectionList.value = normalizeList(inspectionRes)
maintainList.value = normalizeList(maintainRes)
repairList.value = normalizeList(repairRes)
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
function normalizeList(res) {
const data = res && res.data !== undefined ? res.data : res
if (Array.isArray(data)) return data
if (data && Array.isArray(data.data)) return data.data
if (data && data.data && Array.isArray(data.data.list)) return data.data.list
if (data && data.data && Array.isArray(data.data.rows)) return data.data.rows
if (data && data.data && Array.isArray(data.data.records)) return data.data.records
if (data && Array.isArray(data.list)) return data.list
if (data && Array.isArray(data.rows)) return data.rows
if (data && Array.isArray(data.records)) return data.records
return []
}
function detailValue(v) {
if (v === 0) return '0'
if (v === false) return '否'
if (v === true) return '是'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s ? s : '-'
}
function formatEnable(v) {
if (v === true || v === 'true' || v === 1 || v === '1') return '是'
if (v === false || v === 'false' || v === 0 || v === '0') return '否'
return detailValue(v)
}
function formatStatus(v) {
const raw = v === null || v === undefined ? '' : String(v).trim()
const upper = raw.toUpperCase()
if (!raw) return { label: '-', type: 'normal' }
if (raw === '1' || upper === 'OK' || raw.includes('正常')) return { label: raw, type: 'normal' }
if (raw === '2' || upper === 'NG' || raw.includes('停') || raw.includes('禁') || raw.includes('坏') || raw.includes('修'))
return { label: raw, type: 'error' }
return { label: raw, type: 'warning' }
}
function formatResult(v) {
const raw = v === null || v === undefined ? '' : String(v).trim()
const upper = raw.toUpperCase()
if (!raw) return { label: '-', type: 'info' }
if (raw === '0') return { label: '待检测', type: 'info' }
if (raw === '1' || upper === 'OK') return { label: '通过', type: 'success' }
if (raw === '2' || upper === 'NG') return { label: '不通过', type: 'danger' }
return { label: raw, type: 'info' }
}
function formatHistoryTime(value) {
if (!value) return ''
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d, hh, mm, ss] = value
const pad = (n) => String(n).padStart(2, '0')
if (hh !== undefined) return `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}:${pad(ss)}`
return `${y}-${pad(m)}-${pad(d)}`
}
const s = String(value).trim()
if (!s) return ''
const num = Number(s)
if (Number.isFinite(num)) {
const ms = s.length === 10 ? num * 1000 : num
const d = new Date(ms)
if (!Number.isNaN(d.getTime())) return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
}
const d = new Date(s)
if (!Number.isNaN(d.getTime())) return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
return s
}
function formatDateOnly(value) {
const t = formatHistoryTime(value)
if (!t) return '-'
return String(t).split(' ')[0]
}
function parseImages(value) {
if (!value) return []
if (Array.isArray(value)) return value.map(String).filter(Boolean)
const cleaned = String(value).replace(/[`'"]/g, '').trim()
return cleaned
.split(',')
.map((v) => v.trim())
.filter(Boolean)
}
function pickFirst(obj, keys) {
for (const k of keys) {
if (obj && obj[k] !== undefined && obj[k] !== null && String(obj[k]).trim() !== '') return obj[k]
}
return undefined
}
function buildStepGroups(rows, options) {
const groupsMap = new Map()
const list = Array.isArray(rows) ? rows : []
for (const row of list) {
const time = formatHistoryTime(pickFirst(row, options.timeFieldCandidates) || (row ? row.createTime : undefined))
const operator = detailValue(row ? (row.operator || row.creatorName || row.creator) : undefined)
const managementId = row && row.managementId !== undefined && row.managementId !== null ? row.managementId : ''
const groupKey = `${managementId}__${time}__${operator}`
const name = pickFirst(row, options.nameFieldCandidates) || '-'
const resultMeta = formatResult(pickFirst(row, options.resultFieldCandidates))
const item = {
key: String(row && row.id !== undefined && row.id !== null ? row.id : `${groupKey}_${String(name)}`),
name: detailValue(name),
resultLabel: resultMeta.label,
resultType: resultMeta.type,
method: pickFirst(row, options.methodFieldCandidates),
criteria: pickFirst(row, options.criteriaFieldCandidates),
remark: pickFirst(row, options.remarkFieldCandidates),
images: parseImages(pickFirst(row, options.imagesFieldCandidates)),
taskTimeLabel: formatHistoryTime(row ? (row.taskTime || row.inspectionTime) : undefined),
createTimeLabel: formatHistoryTime(row ? row.createTime : undefined)
}
if (!groupsMap.has(groupKey)) {
groupsMap.set(groupKey, { key: groupKey, time: time || '-', operator, items: [item] })
} else {
groupsMap.get(groupKey).items.push(item)
}
}
return Array.from(groupsMap.values())
}
function previewImages(list, current) {
if (!list || !list.length) return
uni.previewImage({ urls: list, current })
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f0f2f5;
}
.status-tag {
margin-top: 20rpx;
display: flex;
justify-content: flex-end;
}
.content-section {
padding: 0 30rpx 30rpx;
}
.info-card {
background: #ffffff;
border-radius: 20rpx;
padding: 30rpx;
margin-top: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 24rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #f0f2f5;
}
.info-list {
display: flex;
flex-direction: column;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f7fa;
&:last-child {
border-bottom: none;
}
}
.info-label {
font-size: 28rpx;
color: #999999;
}
.info-value {
font-size: 28rpx;
color: #333333;
&.highlight {
font-weight: 600;
color: #1a3a5c;
}
&.warning {
color: #ff8c00;
font-weight: 500;
}
}
.tabs-box {
margin-bottom: 24rpx;
}
.empty {
padding: 32rpx 0;
text-align: center;
font-size: 28rpx;
color: #999999;
}
.history-group {
background: #f8fafc;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.history-group-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.history-group-time {
font-size: 24rpx;
color: #1a3a5c;
font-weight: 600;
}
.history-group-operator {
font-size: 24rpx;
color: #666666;
}
.history-item {
background: #ffffff;
border-radius: 14rpx;
padding: 18rpx;
margin-bottom: 16rpx;
}
.history-item:last-child {
margin-bottom: 0;
}
.history-item-head {
display: flex;
align-items: center;
margin-bottom: 14rpx;
}
.history-item-name {
font-size: 28rpx;
color: #333333;
font-weight: 600;
}
.result-badge {
padding: 6rpx 14rpx;
border-radius: 999rpx;
font-size: 22rpx;
margin-right: 14rpx;
}
.result-success {
background: rgba(24, 188, 55, 0.12);
color: #18bc37;
}
.result-danger {
background: rgba(255, 77, 79, 0.12);
color: #ff4d4f;
}
.result-info {
background: rgba(153, 153, 153, 0.12);
color: #666666;
}
.history-item-body {
display: flex;
flex-direction: column;
}
.history-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 10rpx 0;
}
.history-label {
font-size: 24rpx;
color: #999999;
width: 160rpx;
flex-shrink: 0;
}
.history-value {
font-size: 24rpx;
color: #333333;
flex: 1;
text-align: right;
}
.history-images {
margin-top: 12rpx;
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.history-image {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
background: #f0f2f5;
}
.repair-group {
background: #f8fafc;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.repair-group-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.repair-group-name {
font-size: 28rpx;
color: #1a3a5c;
font-weight: 600;
}
.repair-group-meta {
font-size: 24rpx;
color: #666666;
}
.repair-item {
background: #ffffff;
border-radius: 14rpx;
padding: 18rpx;
margin-bottom: 16rpx;
}
.repair-item:last-child {
margin-bottom: 0;
}
.repair-item-head {
display: flex;
align-items: center;
margin-bottom: 14rpx;
}
.repair-tag {
padding: 6rpx 14rpx;
border-radius: 999rpx;
font-size: 22rpx;
background: rgba(74, 144, 194, 0.12);
color: #2d5a87;
margin-right: 14rpx;
}
.repair-title {
font-size: 28rpx;
color: #333333;
font-weight: 600;
}
</style>

@ -0,0 +1,374 @@
<template>
<view class="page-container">
<NavBar title="模具查询" subTitle="请选择查询方式" />
<view class="content-section">
<view class="scan-section">
<view class="scan-area" @click="startScan">
<view class="scan-icon">
<text class="icon-text">📷</text>
</view>
<text class="scan-title">自动扫描</text>
<text class="scan-desc">点击启动扫描模具编码</text>
</view>
<view v-if="isScanning" class="scanning-overlay">
<view class="scanning-animation">
<view class="scan-line"></view>
<view class="scan-corners">
<view class="corner corner-tl"></view>
<view class="corner corner-tr"></view>
<view class="corner corner-bl"></view>
<view class="corner corner-br"></view>
</view>
</view>
<text class="scanning-text">正在打开扫码...</text>
</view>
</view>
<view class="divider">
<view class="divider-line"></view>
<text class="divider-text"></text>
<view class="divider-line"></view>
</view>
<view class="input-section">
<view class="input-label">手动输入模具编码</view>
<view class="input-wrapper">
<input v-model="moldCode" class="code-input" type="text" placeholder="请输入模具编码"
placeholder-class="input-placeholder" />
</view>
<view class="confirm-btn" :class="{ active: moldCode.length > 0 }" @click="confirmInput">
<text class="btn-text">确定</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import NavBar from '@/components/common/NavBar.vue'
const moldCode = ref('');
const isScanning = ref(false);
const SCAN_TYPE = 'MOLD';
const MODULE_NAME = '模具';
function parseScanResult(res) {
const raw = String(res?.result || '').trim();
if (!raw) return {};
const splitIndex = raw.indexOf('-');
if (splitIndex <= 0 || splitIndex >= raw.length - 1) return {};
return {
type: raw.slice(0, splitIndex),
id: raw.slice(splitIndex + 1)
};
}
function startScan() {
if (isScanning.value) return;
isScanning.value = true;
const finish = () => {
isScanning.value = false;
}
uni.scanCode({
onlyFromCamera: false,
scanType: ['qrCode', 'barCode'],
success: (res) => {
const { type, id } = parseScanResult(res);
if (!type || !id) {
uni.showToast({ title: '未获取到扫码结果', icon: 'none' })
return
}
if (type !== SCAN_TYPE) {
uni.showToast({ title: `二维码类型不匹配,请扫描${MODULE_NAME}二维码`, icon: 'none' })
return
}
navigateToDetail(id)
},
fail: (err) => {
const msg = String(err?.errMsg || '')
if (msg.includes('cancel')) {
uni.showToast({ title: '已取消扫码', icon: 'none' })
return
}
if (msg.toLowerCase().includes('not support') || msg.toLowerCase().includes('not supported')) {
uni.showToast({ title: '当前平台不支持扫码', icon: 'none' })
return
}
uni.showToast({ title: '扫码失败', icon: 'none' })
},
complete: finish
})
}
function confirmInput() {
if (moldCode.value.trim().length === 0) {
uni.showToast({
title: '请输入模具编码',
icon: 'none'
});
return;
}
navigateToDetail(moldCode.value.trim(), 'input');
}
function navigateToDetail(id, type = 'scan') {
let url
if (type === 'scan') {
//
url = `/pages_function/pages/mold/detail?id=${encodeURIComponent(id)}&type=${type}`
} else {
//
url = `/pages_function/pages/mold/detail?code=${moldCode.value}&type=${type}`
}
uni.navigateTo({
url: url
});
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f0f2f5;
}
.content-section {
padding: 40rpx 30rpx;
}
.scan-section {
position: relative;
background: #ffffff;
border-radius: 24rpx;
padding: 60rpx 40rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.scan-area {
display: flex;
flex-direction: column;
align-items: center;
&:active {
opacity: 0.8;
}
}
.scan-icon {
width: 160rpx;
height: 160rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #3d7ab5 100%);
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30rpx;
.icon-text {
font-size: 72rpx;
}
}
.scan-title {
font-size: 34rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 12rpx;
}
.scan-desc {
font-size: 26rpx;
color: #999999;
}
.scanning-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(26, 58, 92, 0.95);
border-radius: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
}
.scanning-animation {
width: 300rpx;
height: 300rpx;
position: relative;
margin-bottom: 40rpx;
}
.scan-line {
position: absolute;
top: 0;
left: 20rpx;
right: 20rpx;
height: 4rpx;
background: linear-gradient(90deg, transparent, #ff8c00, transparent);
animation: scanMove 1.5s ease-in-out infinite;
}
@keyframes scanMove {
0% {
top: 20rpx;
}
50% {
top: 260rpx;
}
100% {
top: 20rpx;
}
}
.scan-corners {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.corner {
position: absolute;
width: 40rpx;
height: 40rpx;
border-color: #ff8c00;
border-style: solid;
border-width: 0;
}
.corner-tl {
top: 0;
left: 0;
border-top-width: 6rpx;
border-left-width: 6rpx;
border-top-left-radius: 12rpx;
}
.corner-tr {
top: 0;
right: 0;
border-top-width: 6rpx;
border-right-width: 6rpx;
border-top-right-radius: 12rpx;
}
.corner-bl {
bottom: 0;
left: 0;
border-bottom-width: 6rpx;
border-left-width: 6rpx;
border-bottom-left-radius: 12rpx;
}
.corner-br {
bottom: 0;
right: 0;
border-bottom-width: 6rpx;
border-right-width: 6rpx;
border-bottom-right-radius: 12rpx;
}
.scanning-text {
font-size: 32rpx;
color: #ffffff;
margin-bottom: 30rpx;
}
.divider {
display: flex;
align-items: center;
margin-bottom: 30rpx;
.divider-line {
flex: 1;
height: 2rpx;
background: #e8eaed;
}
.divider-text {
padding: 0 30rpx;
font-size: 28rpx;
color: #999999;
}
}
.input-section {
background: #ffffff;
border-radius: 24rpx;
padding: 40rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.input-label {
font-size: 30rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 24rpx;
}
.input-wrapper {
margin-bottom: 30rpx;
}
.code-input {
width: 100%;
height: 96rpx;
background: #f5f7fa;
border-radius: 16rpx;
padding: 0 30rpx;
font-size: 30rpx;
color: #333333;
border: 2rpx solid transparent;
&:focus {
border-color: #1a3a5c;
background: #ffffff;
}
}
.input-placeholder {
color: #c0c4cc;
}
.confirm-btn {
height: 96rpx;
background: #c0c4cc;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&.active {
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
&:active {
opacity: 0.8;
transform: scale(0.98);
}
}
.btn-text {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
}
}
</style>

@ -0,0 +1,107 @@
<template>
<view class="page-container">
<view class="fixed-header">
<NavBar :title="t('moldInspectionItems.detailTitle')" />
</view>
<view class="content-section">
<view class="info-card">
<view class="card-title">{{ t('moldInspectionItems.basicInfo') }}</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">{{ t('moldInspectionItems.code') }}</text>
<text class="info-value">{{ textValue(detailData.subjectCode) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldInspectionItems.name') }}</text>
<text class="info-value">{{ textValue(detailData.subjectName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldInspectionItems.inspectionMethod') }}</text>
<text class="info-value">{{ dictLabel(inspectionMethodOptions, detailData.inspectionMethod) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldInspectionItems.valueType') }}</text>
<text class="info-value">{{ dictLabel(valueTypeOptions, detailData.valueType) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldInspectionItems.isEnable') }}</text>
<text class="info-value">{{ dictLabel(boolOptions, detailData.isEnable) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldInspectionItems.creatorName') }}</text>
<text class="info-value">{{ textValue(detailData.creatorName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldInspectionItems.createTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData.createTime) }}</text>
</view>
<view class="info-row remark-row">
<text class="info-label">{{ t('moldInspectionItems.judgmentCriteria') }}</text>
<text class="info-value remark-value">{{ textValue(detailData.judgmentCriteria) }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { DICT_TYPE, useDict } from '@/utils/dict'
import { getMoldInspectionItemDetail } from '@/api/mes/moldInspectionItems'
const { t } = useI18n()
const { Inspection_method, value_types, infra_boolean_string } = useDict(DICT_TYPE.INSPECTION_METHOD, DICT_TYPE.VALUE_TYPES, DICT_TYPE.INFRA_BOOLEAN_STRING)
const inspectionMethodOptions = Inspection_method
const valueTypeOptions = value_types
const boolOptions = infra_boolean_string
const detailData = reactive({})
function textValue(v) {
if (v === 0) return '0'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s || '-'
}
function dictLabel(options, value) {
const hit = (options.value || []).find((o) => String(o.value) === String(value))
return hit ? hit.label : textValue(value)
}
function formatDateTime(v) {
if (!v) return '-'
const d = new Date(Number(v))
if (Number.isNaN(d.getTime())) return textValue(v)
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
onLoad(async (query) => {
const id = query?.id
if (!id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
try {
const res = await getMoldInspectionItemDetail(id)
Object.assign(detailData, res?.data || {})
} catch (error) {
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
}
})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f0f2f5; }
.fixed-header { position: sticky; top: 0; z-index: 20; }
.content-section { padding: 0 24rpx 24rpx; }
.info-card { margin-top: 20rpx; background: #fff; border-radius: 20rpx; padding: 28rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); }
.card-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; margin-bottom: 18rpx; }
.info-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 18rpx 0; border-bottom: 1rpx solid #edf0f3; }
.info-label { font-size: 27rpx; color: #8a9099; width: 220rpx; }
.info-value { flex: 1; text-align: right; font-size: 28rpx; color: #303133; line-height: 1.45; }
.remark-row { border-bottom: none; }
.remark-value { white-space: pre-wrap; }
</style>

@ -0,0 +1,419 @@
<template>
<view class="page-container">
<NavBar :title="t('moldInspectionItems.moduleName')" :subTitle="t('moldInspectionItems.subTitle')" />
<!-- 查询区域 -->
<view class="search-card">
<view class="search-row">
<view class="search-input-wrap">
<text class="iconfont icon-search search-icon"></text>
<input v-model="searchKeyword" class="search-input" :placeholder="t('moldInspectionItems.searchPlaceholder')" @confirm="handleSearch" />
</view>
<view class="search-btn" @click="handleSearch">{{ t('functionCommon.search') }}</view>
</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="type-card" @click="openDetail(item)">
<view class="card-header">
<view class="header-left">
<text class="type-name">{{ textValue(item.subjectName) }}</text>
<text class="type-code">{{ t('moldInspectionItems.code') }}: {{ textValue(item.subjectCode) }}</text>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('moldInspectionItems.inspectionMethod') }}</text>
<text class="value">{{ dictLabel(inspectionMethodOptions, item.inspectionMethod) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldInspectionItems.valueType') }}</text>
<text class="value">{{ dictLabel(valueTypeOptions, item.valueType) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldInspectionItems.isEnable') }}</text>
<text class="value">{{ dictLabel(boolOptions, item.isEnable) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldInspectionItems.creatorName') }}</text>
<text class="value">{{ textValue(item.creatorName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldInspectionItems.createTime') }}</text>
<text class="value">{{ formatDateTime(item.createTime) }}</text>
</view>
</view>
<view class="card-actions">
<view class="action-btn edit-btn" @click.stop="openEdit(item)">
<uni-icons type="compose" size="18" color="#ffffff"></uni-icons>
</view>
<view class="action-btn delete-btn" @click.stop="confirmDelete(item)">
<uni-icons type="trash" size="18" color="#ffffff"></uni-icons>
</view>
</view>
</view>
<view v-if="loading && pageNo === 1" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!list.length" class="hint">{{ t('moldInspectionItems.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 class="fab-btn" @click="openCreate">
<uni-icons type="plusempty" size="30" color="#fff"></uni-icons>
</view>
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<uni-icons type="arrow-up" size="20" color="#1a3a5c"></uni-icons>
</view>
<!-- 新增/编辑弹框 -->
<uni-popup ref="formPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ formMode === 'create' ? t('moldInspectionItems.createTitle') : t('moldInspectionItems.editTitle') }}</text>
<view class="popup-close" @click="closeForm">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="form-scroll">
<view class="form-content">
<view class="form-item">
<text class="form-label">{{ t('moldInspectionItems.code') }} <text class="required-star">*</text></text>
<input v-model="formData.subjectCode" class="form-input" type="text" :placeholder="t('moldInspectionItems.placeholderCode')" />
</view>
<view class="form-item">
<text class="form-label">{{ t('moldInspectionItems.name') }} <text class="required-star">*</text></text>
<input v-model="formData.subjectName" class="form-input" type="text" :placeholder="t('moldInspectionItems.placeholderName')" />
</view>
<view class="form-item">
<text class="form-label">{{ t('moldInspectionItems.inspectionMethod') }} <text class="required-star">*</text></text>
<picker mode="selector" :range="inspectionMethodOptions" range-key="label" :value="inspectionMethodIndex" @change="onInspectionMethodChange">
<view class="picker-view">{{ inspectionMethodLabel || t('moldInspectionItems.placeholderInspectionMethod') }}</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">{{ t('moldInspectionItems.valueType') }} <text class="required-star">*</text></text>
<picker mode="selector" :range="valueTypeOptions" range-key="label" :value="valueTypeIndex" @change="onValueTypeChange">
<view class="picker-view">{{ valueTypeLabel || t('moldInspectionItems.placeholderValueType') }}</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">{{ t('moldInspectionItems.isEnable') }} <text class="required-star">*</text></text>
<picker mode="selector" :range="boolOptions" range-key="label" :value="boolIndex" @change="onBoolChange">
<view class="picker-view">{{ boolLabel || t('moldInspectionItems.placeholderIsEnable') }}</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">{{ t('moldInspectionItems.judgmentCriteria') }} <text class="required-star">*</text></text>
<textarea v-model="formData.judgmentCriteria" class="form-textarea" :placeholder="t('moldInspectionItems.placeholderJudgmentCriteria')" maxlength="500" />
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeForm"><text class="btn-text">{{ t('functionCommon.cancel') }}</text></view>
<view class="footer-btn confirm-btn" @click="submitForm"><text class="btn-text">{{ t('functionCommon.save') }}</text></view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { DICT_TYPE, useDict } from '@/utils/dict'
import { getMoldInspectionItemPage, getMoldInspectionItemDetail, createMoldInspectionItem, updateMoldInspectionItem, deleteMoldInspectionItem } from '@/api/mes/moldInspectionItems'
const { t } = useI18n()
const { Inspection_method, value_types, infra_boolean_string } = useDict(DICT_TYPE.INSPECTION_METHOD, DICT_TYPE.VALUE_TYPES, DICT_TYPE.INFRA_BOOLEAN_STRING)
const formPopupRef = ref(null)
const searchKeyword = ref('')
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)
const formMode = ref('create')
const formData = reactive({
id: undefined,
subjectCode: '',
subjectName: '',
inspectionMethod: '',
valueType: '',
isEnable: '',
judgmentCriteria: ''
})
const inspectionMethodOptions = computed(() => Inspection_method.value || [])
const valueTypeOptions = computed(() => value_types.value || [])
const boolOptions = computed(() => infra_boolean_string.value || [])
const inspectionMethodLabel = computed(() => dictLabel(inspectionMethodOptions.value, formData.inspectionMethod))
const valueTypeLabel = computed(() => dictLabel(valueTypeOptions.value, formData.valueType))
const boolLabel = computed(() => dictLabel(boolOptions.value, formData.isEnable))
const inspectionMethodIndex = computed(() => optionIndex(inspectionMethodOptions.value, formData.inspectionMethod))
const valueTypeIndex = computed(() => optionIndex(valueTypeOptions.value, formData.valueType))
const boolIndex = computed(() => optionIndex(boolOptions.value, formData.isEnable))
function optionIndex(options, value) {
const idx = options.findIndex((o) => String(o.value) === String(value))
return idx < 0 ? 0 : idx
}
function dictLabel(options, value) {
const hit = (options || []).find((o) => String(o.value) === String(value))
return hit ? hit.label : textValue(value)
}
function textValue(v) {
if (v === 0) return '0'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s || '-'
}
function formatDateTime(v) {
if (!v) return '-'
const d = new Date(Number(v))
if (Number.isNaN(d.getTime())) return textValue(v)
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
function normalizePageData(res) {
const root = res && res.data !== undefined ? res.data : res
const l = root?.list || root?.rows || root?.records || root?.data?.list || []
const total = root?.total ?? root?.data?.total ?? (Array.isArray(l) ? l.length : 0)
return { list: Array.isArray(l) ? l : [], total: Number(total || 0) }
}
async function fetchList(reset = true) {
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,
subjectCode: searchKeyword.value.trim() || undefined,
subjectName: searchKeyword.value.trim() || undefined,
judgmentCriteria: searchKeyword.value.trim() || undefined
}
const res = await getMoldInspectionItemPage(params)
const page = normalizePageData(res)
if (reset) list.value = page.list
else list.value = [...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
}
}
async function handleSearch() { await fetchList(true) }
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
}
function onScroll(e) {
const top = Number(e?.detail?.scrollTop || 0)
showGoTop.value = top > 600
}
function goTop() { scrollTop.value = 0 }
function resetForm() {
formData.id = undefined
formData.subjectCode = ''
formData.subjectName = ''
formData.inspectionMethod = inspectionMethodOptions.value[0]?.value ?? ''
formData.valueType = valueTypeOptions.value[0]?.value ?? ''
formData.isEnable = boolOptions.value[0]?.value ?? ''
formData.judgmentCriteria = ''
}
function openCreate() {
formMode.value = 'create'
resetForm()
formPopupRef.value?.open()
}
async function openEdit(item) {
const id = item?.id
if (!id) {
uni.showToast({ title: t('functionCommon.noIdEdit'), icon: 'none' })
return
}
formMode.value = 'update'
resetForm()
try {
const res = await getMoldInspectionItemDetail(id)
const detail = res?.data || {}
formData.id = detail.id
formData.subjectCode = detail.subjectCode || ''
formData.subjectName = detail.subjectName || ''
formData.inspectionMethod = detail.inspectionMethod ?? formData.inspectionMethod
formData.valueType = detail.valueType ?? formData.valueType
formData.isEnable = detail.isEnable ?? formData.isEnable
formData.judgmentCriteria = detail.judgmentCriteria || ''
formPopupRef.value?.open()
} catch (e) {
uni.showToast({ title: t('moldInspectionItems.loadEditFailed'), icon: 'none' })
}
}
async function openDetail(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
uni.navigateTo({
url: `/pages_function/pages/moldInspectionItems/detail?id=${encodeURIComponent(String(item.id))}`
})
}
async function confirmDelete(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdDelete'), icon: 'none' })
return
}
uni.showModal({
title: t('functionCommon.confirmDelete'),
content: t('moldInspectionItems.confirmDelete'),
success: async ({ confirm }) => {
if (!confirm) return
try {
await deleteMoldInspectionItem([item.id])
uni.showToast({ title: t('functionCommon.deleteSuccess'), icon: 'success' })
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.deleteFailed'), icon: 'none' })
}
}
})
}
function closeForm() { formPopupRef.value?.close() }
function onInspectionMethodChange(e) {
const idx = Number(e?.detail?.value || 0)
formData.inspectionMethod = inspectionMethodOptions.value[idx]?.value
}
function onValueTypeChange(e) {
const idx = Number(e?.detail?.value || 0)
formData.valueType = valueTypeOptions.value[idx]?.value
}
function onBoolChange(e) {
const idx = Number(e?.detail?.value || 0)
formData.isEnable = boolOptions.value[idx]?.value
}
function validateForm() {
if (!String(formData.subjectCode || '').trim()) {
uni.showToast({ title: t('moldInspectionItems.validatorSubjectCodeRequired'), icon: 'none' })
return false
}
if (!String(formData.subjectName || '').trim()) {
uni.showToast({ title: t('moldInspectionItems.validatorSubjectNameRequired'), icon: 'none' })
return false
}
if (formData.inspectionMethod === '' || formData.inspectionMethod === undefined || formData.inspectionMethod === null) {
uni.showToast({ title: t('moldInspectionItems.validatorInspectionMethodRequired'), icon: 'none' })
return false
}
if (formData.valueType === '' || formData.valueType === undefined || formData.valueType === null) {
uni.showToast({ title: t('moldInspectionItems.validatorValueTypeRequired'), icon: 'none' })
return false
}
if (formData.isEnable === '' || formData.isEnable === undefined || formData.isEnable === null) {
uni.showToast({ title: t('moldInspectionItems.validatorIsEnableRequired'), icon: 'none' })
return false
}
if (!String(formData.judgmentCriteria || '').trim()) {
uni.showToast({ title: t('moldInspectionItems.validatorJudgmentCriteriaRequired'), icon: 'none' })
return false
}
return true
}
async function submitForm() {
if (!validateForm()) return
const payload = {
id: formMode.value === 'update' ? formData.id : undefined,
subjectCode: String(formData.subjectCode).trim(),
subjectName: String(formData.subjectName).trim(),
inspectionMethod: formData.inspectionMethod,
valueType: formData.valueType,
isEnable: formData.isEnable,
judgmentCriteria: String(formData.judgmentCriteria).trim()
}
try {
if (formMode.value === 'create') {
await createMoldInspectionItem(payload)
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
} else {
await updateMoldInspectionItem(payload)
uni.showToast({ title: t('functionCommon.updateSuccess'), icon: 'success' })
}
closeForm()
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
}
}
onShow(async () => {
await fetchList(true)
})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.search-card { margin: 16rpx 24rpx; display: flex; gap: 16rpx; background: #fff; border-radius: 14rpx; padding: 16rpx; }
.search-row { display: flex; gap: 12rpx; width: 100%; }
.search-input-wrap { flex: 1; display: flex; align-items: center; background: #f5f7fa; border-radius: 40rpx; padding: 0 20rpx; }
.search-icon { color: #909399; font-size: 30rpx; }
.search-input { flex: 1; height: 68rpx; margin-left: 12rpx; font-size: 26rpx; }
.search-btn { min-width: 120rpx; height: 68rpx; border-radius: 34rpx; background: #1a3a5c; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 26rpx; }
.list-scroll { height: calc(100vh - 280rpx); }
.list-wrap { padding: 0 24rpx 130rpx; }
.type-card { background: #ffffff; border-radius: 18rpx; padding: 24rpx; margin-bottom: 18rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.05); }
.card-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 18rpx; border-bottom: 1rpx solid #edf0f3; }
.header-left { display: flex; flex-direction: column; gap: 8rpx; }
.type-name { font-size: 32rpx; font-weight: 700; color: #1f2d3d; }
.type-code { font-size: 24rpx; color: #8a9099; }
.card-actions { margin-top: 24rpx; display: flex; justify-content: flex-end; gap: 14rpx; }
.action-btn { width: 60rpx; height: 60rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.edit-btn { background: #1a3a5c; }
.delete-btn { background: #e34d59; }
.card-body { padding-top: 16rpx; }
.row { display: flex; justify-content: space-between; align-items: center; margin-top: 12rpx; }
.label { font-size: 26rpx; color: #8a9099; }
.value { font-size: 27rpx; color: #30363d; max-width: 62%; text-align: right; }
.hint { text-align: center; color: #909399; padding: 24rpx 0; }
.fab-btn { position: fixed; right: 34rpx; bottom: 120rpx; width: 96rpx; height: 96rpx; border-radius: 50%; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); display: flex; align-items: center; justify-content: center; box-shadow: 0 10rpx 30rpx rgba(26, 58, 92, 0.3); z-index: 20; }
.go-top-btn { position: fixed; right: 34rpx; bottom: 240rpx; width: 80rpx; height: 80rpx; border-radius: 50%; background: #fff; box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12); display: flex; align-items: center; justify-content: center; }
.popup-content { width: 680rpx; max-height: 80vh; background: #fff; border-radius: 24rpx; overflow: hidden; }
.popup-header { height: 92rpx; display: flex; align-items: center; justify-content: center; position: relative; border-bottom: 1rpx solid #edf0f3; }
.popup-title { font-size: 32rpx; font-weight: 700; color: #1f2d3d; }
.popup-close { position: absolute; right: 24rpx; top: 20rpx; width: 48rpx; height: 48rpx; display: flex; align-items: center; justify-content: center; }
.close-icon { font-size: 42rpx; color: #9aa0a6; line-height: 1; }
.form-scroll { max-height: calc(80vh - 180rpx); }
.form-content { padding: 22rpx 28rpx; }
.form-item { margin-bottom: 18rpx; }
.form-label { display: block; font-size: 25rpx; color: #5f6b7a; margin-bottom: 10rpx; }
.required-star { color: #e34d59; }
.form-input,.picker-view { min-height: 72rpx; background: #f7f9fc; border-radius: 12rpx; padding: 0 18rpx; display: flex; align-items: center; font-size: 26rpx; color: #2f3a45; }
.form-textarea { width: 100%; min-height: 140rpx; background: #f7f9fc; border-radius: 12rpx; padding: 14rpx 18rpx; font-size: 26rpx; color: #2f3a45; box-sizing: border-box; }
.form-footer { height: 88rpx; display: flex; border-top: 1rpx solid #edf0f3; }
.footer-btn { flex: 1; display: flex; align-items: center; justify-content: center; }
.btn-text { font-size: 28rpx; }
.cancel-btn { background: #f4f6fa; color: #5f6b7a; }
.confirm-btn { background: #1a3a5c; color: #fff; }
</style>

@ -0,0 +1,166 @@
<template>
<view class="page-container">
<view class="fixed-header">
<NavBar :title="t('moldInspectionPlan.detailTitle')" />
</view>
<scroll-view scroll-y class="detail-scroll">
<view class="content-section">
<!-- 基础信息 -->
<view class="info-card">
<view class="card-title">{{ t('moldInspectionPlan.basicInfo') }}</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">{{ t('moldInspectionPlan.planName') }}</text>
<text class="info-value">{{ textValue(detailData.planName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldInspectionPlan.planType') }}</text>
<text class="info-value">{{ planTypeText }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldInspectionPlan.description') }}</text>
<text class="info-value">{{ textValue(detailData.description) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldInspectionPlan.creatorName') }}</text>
<text class="info-value">{{ textValue(detailData.creatorName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldInspectionPlan.createTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData.createTime) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldInspectionPlan.updateTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData.updateTime) }}</text>
</view>
</view>
</view>
<!-- 关联点检项 -->
<view class="info-card">
<view class="card-title">{{ t('moldInspectionPlan.subjectListTitle') }}</view>
<view v-if="subjectLoading" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!subjectList.length" class="hint">{{ t('moldInspectionPlan.noSubjectData') }}</view>
<view v-else class="subject-list">
<view v-for="(item, index) in subjectList" :key="item.id || index" class="subject-card">
<view class="subject-index">{{ index + 1 }}</view>
<view class="subject-body">
<view class="subject-row">
<text class="subject-label">{{ t('moldInspectionPlan.subjectCode') }}</text>
<text class="subject-value">{{ textValue(item.subjectCode) }}</text>
</view>
<view class="subject-row">
<text class="subject-label">{{ t('moldInspectionPlan.subjectName') }}</text>
<text class="subject-value">{{ textValue(item.subjectName) }}</text>
</view>
<view class="subject-row">
<text class="subject-label">{{ t('moldInspectionPlan.inspectionMethod') }}</text>
<text class="subject-value">{{ getInspectionMethodLabel(item.inspectionMethod) }}</text>
</view>
<view class="subject-row">
<text class="subject-label">{{ t('moldInspectionPlan.judgmentCriteria') }}</text>
<text class="subject-value">{{ textValue(item.judgmentCriteria) }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
import { getMoldInspectionPlanDetail } from '@/api/mes/moldInspectionPlan'
const { t } = useI18n()
const detailData = reactive({})
const subjectList = ref([])
const subjectLoading = ref(false)
const planTypeText = computed(() => {
const v = detailData.planType
if (v === 1) return t('moldInspectionPlan.planTypeMaintain')
if (v === 2) return t('moldInspectionPlan.planTypeInspect')
return textValue(v)
})
function textValue(v) {
if (v === 0) return '0'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s || '-'
}
function getInspectionMethodLabel(value) {
return getDictLabel(DICT_TYPE.INSPECTION_METHOD, value, textValue(value))
}
function formatDateTime(v) {
if (!v) return '-'
if (Array.isArray(v) && v.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = v
const p = (n) => String(n).padStart(2, '0')
return `${y}-${p(m)}-${p(d)} ${p(hh)}:${p(mm)}:${p(ss)}`
}
const d = new Date(Number(v))
if (Number.isNaN(d.getTime())) return textValue(v)
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
onLoad(async (query) => {
const id = query?.id
if (!id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
await initAllDict()
try {
const cached = uni.getStorageSync('moldInspectionPlanDetail')
if (cached) {
const data = JSON.parse(cached)
Object.assign(detailData, data)
uni.removeStorageSync('moldInspectionPlanDetail')
}
} catch (e) {}
subjectLoading.value = true
try {
const res = await getMoldInspectionPlanDetail(id)
const root = res && res.data !== undefined ? res.data : res
const subjects = Array.isArray(root) ? root : root?.list || root?.data?.list || []
subjectList.value = Array.isArray(subjects) ? subjects : []
} catch (e) {
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
} finally {
subjectLoading.value = false
}
})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.fixed-header { position: sticky; top: 0; z-index: 20; }
.detail-scroll { height: calc(100vh - 88rpx); }
.content-section { padding: 0 24rpx 24rpx; }
.info-card { margin-top: 20rpx; background: #fff; border-radius: 20rpx; padding: 28rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); }
.card-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; margin-bottom: 18rpx; }
.info-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 18rpx 0; border-bottom: 1rpx solid #edf0f3; }
.info-label { font-size: 27rpx; color: #8a9099; width: 220rpx; }
.info-value { flex: 1; text-align: right; font-size: 28rpx; color: #303133; line-height: 1.45; }
.hint { text-align: center; color: #909399; padding: 24rpx 0; font-size: 26rpx; }
.subject-list { display: flex; flex-direction: column; gap: 16rpx; }
.subject-card { display: flex; background: #f7f9fc; border-radius: 14rpx; padding: 20rpx; gap: 16rpx; }
.subject-index { min-width: 48rpx; height: 48rpx; border-radius: 10rpx; background: #1a3a5c; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 24rpx; font-weight: 700; }
.subject-body { flex: 1; display: flex; flex-direction: column; gap: 8rpx; }
.subject-row { display: flex; justify-content: space-between; align-items: center; }
.subject-label { font-size: 24rpx; color: #8a9099; }
.subject-value { font-size: 26rpx; color: #30363d; max-width: 60%; text-align: right; }
</style>

@ -0,0 +1,486 @@
<template>
<view class="page-container">
<NavBar :title="t('moldInspectionPlan.moduleName')" :subTitle="t('moldInspectionPlan.subTitle')" />
<view class="search-card">
<view class="search-row">
<view class="search-input-wrap">
<text class="iconfont icon-search search-icon"></text>
<input v-model="searchKeyword" class="search-input" :placeholder="t('moldInspectionPlan.searchPlaceholder')" @confirm="handleSearch" />
</view>
<view class="search-btn" @click="handleSearch">{{ t('functionCommon.search') }}</view>
</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="type-card" @click="openDetail(item)">
<view class="card-header">
<view class="header-left">
<text class="type-name">{{ textValue(item.planName) }}</text>
<view class="tag-row">
<text class="type-code">{{ planTypeLabelForRow(item.planType) }}</text>
</view>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('moldInspectionPlan.description') }}</text>
<text class="value">{{ textValue(item.description) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldInspectionPlan.creatorName') }}</text>
<text class="value">{{ textValue(item.creatorName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldInspectionPlan.createTime') }}</text>
<text class="value">{{ formatDateTime(item.createTime) }}</text>
</view>
</view>
<view class="card-actions">
<view class="action-btn edit-btn" @click.stop="openEdit(item)">
<uni-icons type="compose" size="18" color="#ffffff"></uni-icons>
</view>
<view class="action-btn delete-btn" @click.stop="confirmDelete(item)">
<uni-icons type="trash" size="18" color="#ffffff"></uni-icons>
</view>
</view>
</view>
<view v-if="loading && pageNo === 1" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!list.length" class="hint">{{ t('moldInspectionPlan.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 class="fab-btn" @click="openCreate">
<uni-icons type="plusempty" size="30" color="#fff"></uni-icons>
</view>
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<uni-icons type="arrow-up" size="20" color="#1a3a5c"></uni-icons>
</view>
<!-- 新增/编辑弹框 -->
<uni-popup ref="formPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ formMode === 'create' ? t('moldInspectionPlan.createTitle') : t('moldInspectionPlan.editTitle') }}</text>
<view class="popup-close" @click="closeForm">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="form-scroll">
<view class="form-content">
<view class="form-item">
<text class="form-label">{{ t('moldInspectionPlan.planName') }} <text class="required-star">*</text></text>
<input v-model="formData.planName" class="form-input" type="text" :placeholder="t('moldInspectionPlan.placeholderPlanName')" />
</view>
<view class="form-item">
<text class="form-label">{{ t('moldInspectionPlan.planType') }} <text class="required-star">*</text></text>
<picker mode="selector" :range="planTypeOptions" range-key="label" :value="planTypeIndex" @change="onPlanTypeChange">
<view class="picker-view">{{ planTypeLabel || t('moldInspectionPlan.placeholderPlanType') }}</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">{{ t('moldInspectionPlan.description') }}</text>
<textarea v-model="formData.description" class="form-textarea" :placeholder="t('moldInspectionPlan.placeholderDescription')" maxlength="500" />
</view>
<view class="form-item">
<text class="form-label">{{ t('moldInspectionPlan.subjectName') }}</text>
<view class="multi-select-wrap" @click="openSubjectPicker">
<text v-if="selectedSubjectLabels.length" class="selected-text">{{ selectedSubjectLabels.join(', ') }}</text>
<text v-else class="placeholder-text">{{ t('moldInspectionPlan.placeholderSubjectSelect') }}</text>
</view>
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeForm"><text class="btn-text">{{ t('functionCommon.cancel') }}</text></view>
<view class="footer-btn confirm-btn" @click="submitForm"><text class="btn-text">{{ t('functionCommon.save') }}</text></view>
</view>
</view>
</uni-popup>
<!-- 点检项选择弹框 -->
<uni-popup ref="subjectPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ t('moldInspectionPlan.subjectSelectTitle') }}</text>
<view class="popup-close" @click="closeSubjectPicker">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="subject-scroll">
<view class="subject-list">
<view v-for="item in subjectOptions" :key="item.id" class="subject-item" @click="toggleSubject(item)">
<view class="checkbox-wrap">
<view :class="['checkbox-box', isSubjectSelected(item.id) ? 'checked' : '']">
<uni-icons v-if="isSubjectSelected(item.id)" type="checkmarkempty" size="14" color="#fff"></uni-icons>
</view>
</view>
<view class="subject-info">
<text class="subject-name">{{ textValue(item.subjectName) }}</text>
<text class="subject-code">{{ textValue(item.subjectCode) }}</text>
</view>
</view>
<view v-if="!subjectOptions.length" class="hint">{{ t('moldInspectionPlan.noSubjectData') }}</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeSubjectPicker"><text class="btn-text">{{ t('functionCommon.cancel') }}</text></view>
<view class="footer-btn confirm-btn" @click="confirmSubjectPicker"><text class="btn-text">{{ t('functionCommon.save') }}</text></view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getMoldInspectionPlanPage, createMoldInspectionPlan, updateMoldInspectionPlan, deleteMoldInspectionPlan, getMoldSubjectAllList } from '@/api/mes/moldInspectionPlan'
const { t } = useI18n()
const formPopupRef = ref(null)
const subjectPopupRef = ref(null)
const searchKeyword = ref('')
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)
const formMode = ref('create')
const subjectOptions = ref([])
const tempSelectedSubjectIds = ref([])
const planTypeOptions = computed(() => [
{ value: 1, label: t('moldInspectionPlan.planTypeMaintain') },
{ value: 2, label: t('moldInspectionPlan.planTypeInspect') }
])
const formData = reactive({
id: undefined,
planName: '',
planType: undefined,
description: '',
subjectIds: []
})
const planTypeIndex = computed(() => {
const idx = planTypeOptions.value.findIndex((o) => o.value === formData.planType)
return idx < 0 ? 0 : idx
})
const planTypeLabel = computed(() => {
const hit = planTypeOptions.value.find((o) => o.value === formData.planType)
return hit ? hit.label : ''
})
const selectedSubjectLabels = computed(() => {
return formData.subjectIds
.map((id) => {
const found = subjectOptions.value.find((s) => s.id === id)
return found ? found.subjectName : ''
})
.filter(Boolean)
})
function planTypeLabelForRow(value) {
const v = value === '' || value === null || value === undefined ? undefined : Number(value)
if (v === 1) return t('moldInspectionPlan.planTypeMaintain')
if (v === 2) return t('moldInspectionPlan.planTypeInspect')
return textValue(value)
}
function textValue(v) {
if (v === 0) return '0'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s || '-'
}
function formatDateTime(v) {
if (!v) return '-'
if (Array.isArray(v) && v.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = v
const p = (n) => String(n).padStart(2, '0')
return `${y}-${p(m)}-${p(d)} ${p(hh)}:${p(mm)}:${p(ss)}`
}
const d = new Date(Number(v))
if (Number.isNaN(d.getTime())) return textValue(v)
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
function normalizePageData(res) {
const root = res && res.data !== undefined ? res.data : res
const l = root?.list || root?.rows || root?.records || root?.data?.list || []
const total = root?.total ?? root?.data?.total ?? (Array.isArray(l) ? l.length : 0)
return { list: Array.isArray(l) ? l : [], total: Number(total || 0) }
}
async function fetchList(reset = true) {
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,
planName: searchKeyword.value.trim() || undefined
}
const res = await getMoldInspectionPlanPage(params)
const page = normalizePageData(res)
if (reset) list.value = page.list
else list.value = [...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
}
}
async function handleSearch() { await fetchList(true) }
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
}
function onScroll(e) {
const top = Number(e?.detail?.scrollTop || 0)
showGoTop.value = top > 600
}
function goTop() { scrollTop.value = 0 }
async function ensureSubjectOptionsLoaded() {
if (subjectOptions.value.length) return
try {
const res = await getMoldSubjectAllList()
const root = res && res.data !== undefined ? res.data : res
const l = Array.isArray(root) ? root : root?.list || root?.data || []
subjectOptions.value = Array.isArray(l) ? l : []
} catch (e) {
subjectOptions.value = []
}
}
function resetForm() {
formData.id = undefined
formData.planName = ''
formData.planType = undefined
formData.description = ''
formData.subjectIds = []
}
async function openCreate() {
formMode.value = 'create'
resetForm()
await ensureSubjectOptionsLoaded()
formPopupRef.value?.open()
}
async function openEdit(item) {
const id = item?.id
if (!id) {
uni.showToast({ title: t('functionCommon.noIdEdit'), icon: 'none' })
return
}
formMode.value = 'update'
resetForm()
await ensureSubjectOptionsLoaded()
formData.id = item.id
formData.planName = item.planName || ''
formData.planType = item.planType ?? undefined
formData.description = item.description || ''
formData.subjectIds = parseSubjectIds(item.subjectIdS || item.subjectIds)
formPopupRef.value?.open()
}
function parseSubjectIds(value) {
if (!value) return []
if (Array.isArray(value)) return value.map((v) => (typeof v === 'number' ? v : Number(v))).filter((v) => Number.isFinite(v))
return String(value)
.split(',')
.map((v) => v.trim())
.filter(Boolean)
.map((v) => Number(v))
.filter((v) => Number.isFinite(v))
}
async function openDetail(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
uni.setStorageSync('moldInspectionPlanDetail', JSON.stringify(item))
uni.navigateTo({
url: `/pages_function/pages/moldInspectionPlan/detail?id=${encodeURIComponent(String(item.id))}`
})
}
async function confirmDelete(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdDelete'), icon: 'none' })
return
}
uni.showModal({
title: t('functionCommon.confirmDelete'),
content: t('moldInspectionPlan.confirmDelete'),
success: async ({ confirm }) => {
if (!confirm) return
try {
await deleteMoldInspectionPlan([item.id])
uni.showToast({ title: t('functionCommon.deleteSuccess'), icon: 'success' })
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.deleteFailed'), icon: 'none' })
}
}
})
}
function closeForm() { formPopupRef.value?.close() }
function onPlanTypeChange(e) {
const idx = Number(e?.detail?.value || 0)
formData.planType = planTypeOptions.value[idx]?.value
}
function openSubjectPicker() {
tempSelectedSubjectIds.value = [...formData.subjectIds]
subjectPopupRef.value?.open()
}
function closeSubjectPicker() {
subjectPopupRef.value?.close()
}
function confirmSubjectPicker() {
formData.subjectIds = [...tempSelectedSubjectIds.value]
subjectPopupRef.value?.close()
}
function isSubjectSelected(id) {
return tempSelectedSubjectIds.value.includes(id)
}
function toggleSubject(item) {
const id = item.id
const idx = tempSelectedSubjectIds.value.indexOf(id)
if (idx >= 0) {
tempSelectedSubjectIds.value.splice(idx, 1)
} else {
tempSelectedSubjectIds.value.push(id)
}
}
function validateForm() {
if (!String(formData.planName || '').trim()) {
uni.showToast({ title: t('moldInspectionPlan.validatorPlanNameRequired'), icon: 'none' })
return false
}
if (formData.planType === undefined || formData.planType === null || formData.planType === '') {
uni.showToast({ title: t('moldInspectionPlan.validatorPlanTypeRequired'), icon: 'none' })
return false
}
return true
}
async function submitForm() {
if (!validateForm()) return
const payload = {
id: formMode.value === 'update' ? formData.id : undefined,
planName: String(formData.planName).trim(),
planType: formData.planType,
description: String(formData.description || '').trim() || undefined,
subjectIdS: formData.subjectIds.length ? formData.subjectIds.join(',') : undefined
}
try {
if (formMode.value === 'create') {
await createMoldInspectionPlan(payload)
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
} else {
await updateMoldInspectionPlan(payload)
uni.showToast({ title: t('functionCommon.updateSuccess'), icon: 'success' })
}
closeForm()
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
}
}
onShow(async () => {
await fetchList(true)
})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.search-card { margin: 16rpx 24rpx; display: flex; gap: 16rpx; background: #fff; border-radius: 14rpx; padding: 16rpx; }
.search-row { display: flex; gap: 12rpx; width: 100%; }
.search-input-wrap { flex: 1; display: flex; align-items: center; background: #f5f7fa; border-radius: 40rpx; padding: 0 20rpx; }
.search-icon { color: #909399; font-size: 30rpx; }
.search-input { flex: 1; height: 68rpx; margin-left: 12rpx; font-size: 26rpx; }
.search-btn { min-width: 120rpx; height: 68rpx; border-radius: 34rpx; background: #1a3a5c; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 26rpx; }
.list-scroll { height: calc(100vh - 280rpx); }
.list-wrap { padding: 0 24rpx 130rpx; }
.type-card { background: #ffffff; border-radius: 18rpx; padding: 24rpx; margin-bottom: 18rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.05); }
.card-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 18rpx; border-bottom: 1rpx solid #edf0f3; }
.header-left { display: flex; flex-direction: column; gap: 8rpx; flex: 1; }
.type-name { font-size: 32rpx; font-weight: 700; color: #1f2d3d; }
.tag-row { display: flex; align-items: center; }
.type-code { font-size: 24rpx; color: #1a3a5c; background: rgba(26, 58, 92, 0.08); padding: 4rpx 14rpx; border-radius: 8rpx; }
.card-actions { margin-top: 24rpx; display: flex; justify-content: flex-end; gap: 14rpx; }
.action-btn { width: 60rpx; height: 60rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.edit-btn { background: #1a3a5c; }
.delete-btn { background: #e34d59; }
.card-body { padding-top: 16rpx; }
.row { display: flex; justify-content: space-between; align-items: center; margin-top: 12rpx; }
.label { font-size: 26rpx; color: #8a9099; }
.value { font-size: 27rpx; color: #30363d; max-width: 62%; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hint { text-align: center; color: #909399; padding: 24rpx 0; }
.fab-btn { position: fixed; right: 34rpx; bottom: 120rpx; width: 96rpx; height: 96rpx; border-radius: 50%; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); display: flex; align-items: center; justify-content: center; box-shadow: 0 10rpx 30rpx rgba(26, 58, 92, 0.3); z-index: 20; }
.go-top-btn { position: fixed; right: 34rpx; bottom: 240rpx; width: 80rpx; height: 80rpx; border-radius: 50%; background: #fff; box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12); display: flex; align-items: center; justify-content: center; }
.popup-content { width: 680rpx; max-height: 80vh; background: #fff; border-radius: 24rpx; overflow: hidden; }
.popup-header { height: 92rpx; display: flex; align-items: center; justify-content: center; position: relative; border-bottom: 1rpx solid #edf0f3; }
.popup-title { font-size: 32rpx; font-weight: 700; color: #1f2d3d; }
.popup-close { position: absolute; right: 24rpx; top: 20rpx; width: 48rpx; height: 48rpx; display: flex; align-items: center; justify-content: center; }
.close-icon { font-size: 42rpx; color: #9aa0a6; line-height: 1; }
.form-scroll { max-height: calc(80vh - 180rpx); }
.form-content { padding: 22rpx 28rpx; }
.form-item { margin-bottom: 18rpx; }
.form-label { display: block; font-size: 25rpx; color: #5f6b7a; margin-bottom: 10rpx; }
.required-star { color: #e34d59; }
.form-input { min-height: 72rpx; background: #f7f9fc; border-radius: 12rpx; padding: 0 18rpx; display: flex; align-items: center; font-size: 26rpx; color: #2f3a45; }
.form-textarea { width: 100%; min-height: 140rpx; background: #f7f9fc; border-radius: 12rpx; padding: 14rpx 18rpx; font-size: 26rpx; color: #2f3a45; box-sizing: border-box; }
.picker-view { min-height: 72rpx; background: #f7f9fc; border-radius: 12rpx; padding: 0 18rpx; display: flex; align-items: center; font-size: 26rpx; color: #2f3a45; }
.multi-select-wrap { min-height: 72rpx; background: #f7f9fc; border-radius: 12rpx; padding: 14rpx 18rpx; font-size: 26rpx; }
.selected-text { color: #2f3a45; word-break: break-all; }
.placeholder-text { color: #c0c4cc; }
.form-footer { height: 88rpx; display: flex; border-top: 1rpx solid #edf0f3; }
.footer-btn { flex: 1; display: flex; align-items: center; justify-content: center; }
.btn-text { font-size: 28rpx; }
.cancel-btn { background: #f4f6fa; color: #5f6b7a; }
.confirm-btn { background: #1a3a5c; color: #fff; }
.subject-scroll { max-height: 50vh; }
.subject-list { padding: 16rpx 24rpx; }
.subject-item { display: flex; align-items: center; padding: 18rpx 0; border-bottom: 1rpx solid #f0f2f5; }
.checkbox-wrap { margin-right: 20rpx; }
.checkbox-box { width: 40rpx; height: 40rpx; border-radius: 8rpx; border: 2rpx solid #d0d5dd; display: flex; align-items: center; justify-content: center; background: #fff; }
.checkbox-box.checked { background: #1a3a5c; border-color: #1a3a5c; }
.subject-info { display: flex; flex-direction: column; gap: 4rpx; }
.subject-name { font-size: 28rpx; color: #30363d; }
.subject-code { font-size: 24rpx; color: #8a9099; }
</style>

@ -0,0 +1,697 @@
<template>
<view class="page-container">
<view class="fixed-header">
<NavBar title="模具台账详情" />
</view>
<view class="content-section">
<view class="info-card">
<view class="card-title">基本信息</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">模具名称</text>
<text class="info-value">{{ getDetailField('name') }}</text>
</view>
<view class="info-row">
<text class="info-label">模具编号</text>
<text class="info-value">{{ getDetailField('code') }}</text>
</view>
<view class="info-row">
<text class="info-label">模具状态</text>
<u-tag :text="statusLabel" :type="statusTagType" size="mini" />
</view>
<view class="info-row">
<text class="info-label">模具型号</text>
<text class="info-value">{{ getDetailField('brandName') }}</text>
</view>
<view class="info-row">
<text class="info-label">模具规格</text>
<text class="info-value">{{ getDetailField('moldType') }}</text>
</view>
<view class="info-row">
<text class="info-label">工序</text>
<text class="info-value">{{ getDetailField('orgType') }}</text>
</view>
<view class="info-row">
<text class="info-label">模穴数</text>
<text class="info-value">{{ getDetailField('moldSize') }}</text>
</view>
<view class="info-row">
<text class="info-label">使用次数/</text>
<text class="info-value">{{ getDetailField('useTime') }}</text>
</view>
<view class="info-row">
<text class="info-label">使用设备</text>
<text class="info-value">{{ machineLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">入库日期</text>
<text class="info-value">{{ inTimeLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">是否启用</text>
<text class="info-value">{{ enableLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">备注</text>
<text class="info-value">{{ getDetailField('remark') }}</text>
</view>
</view>
</view>
<view class="info-card">
<view class="card-title">履历</view>
<view class="tabs-box">
<u-tabs activeColor="#1a3a5c" :list="tabList" :current="currentTab" :is-scroll="false" @change="handleTabChange" />
</view>
<view>
<view v-if="currentTab === 0">
<view v-if="!inspectionGroups.length" class="empty"></view>
<view v-for="group in inspectionGroups" :key="group.key" class="history-group">
<view class="history-group-head">
<text class="history-group-time">[{{ group.time }}]</text>
<text class="history-group-operator">操作人: {{ group.operator }}</text>
</view>
<view v-for="item in group.items" :key="item.key" class="history-item">
<view class="history-item-head">
<text class="result-badge" :class="'result-' + item.resultType">{{ item.resultLabel }}</text>
<text class="history-item-name">{{ item.name }}</text>
</view>
<view class="history-item-body">
<view class="history-row">
<text class="history-label">点检方式</text>
<text class="history-value">{{ detailValue(item.method) }}</text>
</view>
<view class="history-row">
<text class="history-label">判定标准</text>
<text class="history-value">{{ detailValue(item.criteria) }}</text>
</view>
<view class="history-row">
<text class="history-label">点检时间</text>
<text class="history-value">{{ detailValue(item.taskTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">创建时间</text>
<text class="history-value">{{ detailValue(item.createTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">备注</text>
<text class="history-value">{{ detailValue(item.remark) }}</text>
</view>
<view v-if="item.images && item.images.length" class="history-images">
<image v-for="img in item.images" :key="img" class="history-image" :src="img" mode="aspectFill" @click="previewImages(item.images, img)" />
</view>
</view>
</view>
</view>
</view>
<view v-else-if="currentTab === 1">
<view v-if="!maintainGroups.length" class="empty"></view>
<view v-for="group in maintainGroups" :key="group.key" class="history-group">
<view class="history-group-head">
<text class="history-group-time">[{{ group.time }}]</text>
<text class="history-group-operator">操作人: {{ group.operator }}</text>
</view>
<view v-for="item in group.items" :key="item.key" class="history-item">
<view class="history-item-head">
<text class="result-badge" :class="'result-' + item.resultType">{{ item.resultLabel }}</text>
<text class="history-item-name">{{ item.name }}</text>
</view>
<view class="history-item-body">
<view class="history-row">
<text class="history-label">保养方式</text>
<text class="history-value">{{ detailValue(item.method) }}</text>
</view>
<view class="history-row">
<text class="history-label">判定标准</text>
<text class="history-value">{{ detailValue(item.criteria) }}</text>
</view>
<view class="history-row">
<text class="history-label">保养时间</text>
<text class="history-value">{{ detailValue(item.taskTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">创建时间</text>
<text class="history-value">{{ detailValue(item.createTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">备注</text>
<text class="history-value">{{ detailValue(item.remark) }}</text>
</view>
<view v-if="item.images && item.images.length" class="history-images">
<image v-for="img in item.images" :key="img" class="history-image" :src="img" mode="aspectFill" @click="previewImages(item.images, img)" />
</view>
</view>
</view>
</view>
</view>
<view v-else>
<view v-if="!repairGroups.length" class="empty"></view>
<view v-for="group in repairGroups" :key="group.key" class="repair-group">
<view class="repair-group-head">
<text class="repair-group-name">{{ group.name }}</text>
<text class="repair-group-meta">{{ group.items.length }}</text>
</view>
<view v-for="row in group.items" :key="row.key" class="repair-item">
<view class="repair-item-head">
<text class="repair-tag">{{ detailValue(row.subjectCode) }}</text>
<text class="repair-title">{{ detailValue(row.subjectName) }}</text>
</view>
<view class="repair-item-body">
<view class="history-row">
<text class="history-label">项目内容</text>
<text class="history-value">{{ detailValue(row.subjectContent) }}</text>
</view>
<view class="history-row">
<text class="history-label">维修结果</text>
<text class="history-value">
<text class="result-badge" :class="'result-' + row.resultType">{{ row.resultLabel }}</text>
</text>
</view>
<view class="history-row">
<text class="history-label">备注</text>
<text class="history-value">{{ detailValue(row.remark) }}</text>
</view>
<view class="history-row">
<text class="history-label">完成日期</text>
<text class="history-value">{{ detailValue(row.finishDateLabel) }}</text>
</view>
<view v-if="row.images && row.images.length" class="history-images">
<image v-for="img in row.images" :key="img" class="history-image" :src="img" mode="aspectFill" @click="previewImages(row.images, img)" />
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import NavBar from '@/components/common/NavBar.vue'
import { getMoldDetail, getMoldInspectionByMoldId, getMoldMaintenanceByMoldId, getMoldRepairListByMoldId } from '@/api/mes/mold'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const loading = ref(false)
const moldId = ref(undefined)
const detailData = ref(null)
const inspectionList = ref([])
const maintainList = ref([])
const repairList = ref([])
const tabList = ref([{ name: '点检履历' }, { name: '保养履历' }, { name: '维修履历' }])
const currentTab = ref(0)
function getDetailField(field) {
const d = detailData.value
return detailValue(d ? d[field] : undefined)
}
const statusMeta = computed(() => {
const d = detailData.value
const raw = d ? d.status : undefined
const label = getDictLabel(DICT_TYPE.ERP_MOLD_STATUS, raw, detailValue(raw))
return formatStatus(label)
})
const statusLabel = computed(() => statusMeta.value.label)
const statusType = computed(() => statusMeta.value.type)
const statusTagType = computed(() => {
if (statusType.value === 'error') return 'error'
if (statusType.value === 'warning') return 'warning'
return 'success'
})
const machineLabel = computed(() => {
const d = detailData.value
const v = d ? d.machineName || d.machineId : undefined
return detailValue(v)
})
const inTimeLabel = computed(() => {
const d = detailData.value
return formatDateOnly(d ? d.inTime : undefined)
})
const enableLabel = computed(() => {
const d = detailData.value
return formatEnable(d ? d.isEnable : undefined)
})
const inspectionGroups = computed(() =>
buildStepGroups(inspectionList.value, {
timeFieldCandidates: ['taskTime', 'inspectionTime', 'createTime'],
nameFieldCandidates: ['inspectionItemName', 'name', 'itemName'],
resultFieldCandidates: ['inspectionResult', 'result'],
methodFieldCandidates: ['inspectionMethod', 'method'],
criteriaFieldCandidates: ['judgmentCriteria', 'criteria'],
imagesFieldCandidates: ['images'],
remarkFieldCandidates: ['remark']
})
)
const maintainGroups = computed(() =>
buildStepGroups(maintainList.value, {
timeFieldCandidates: ['taskTime', 'inspectionTime', 'createTime'],
nameFieldCandidates: ['maintainItemName', 'inspectionItemName', 'name', 'itemName'],
resultFieldCandidates: ['maintainResult', 'inspectionResult', 'result'],
methodFieldCandidates: ['inspectionMethod', 'method'],
criteriaFieldCandidates: ['judgmentCriteria', 'criteria'],
imagesFieldCandidates: ['images'],
remarkFieldCandidates: ['remark']
})
)
const repairGroups = computed(() => {
const groupsMap = new Map()
const rows = Array.isArray(repairList.value) ? repairList.value : []
for (const row of rows) {
const groupKey = String((row && (row.repairCode || row.repairId || row.subjectName || row.id)) ? (row.repairCode || row.repairId || row.subjectName || row.id) : '-')
if (!groupsMap.has(groupKey)) {
groupsMap.set(groupKey, {
key: groupKey,
name: String((row && (row.repairName || row.repairCode)) ? (row.repairName || row.repairCode) : groupKey),
items: []
})
}
const resultMeta = formatResult(row ? (row.repairResult !== undefined ? row.repairResult : row.result) : undefined)
groupsMap.get(groupKey).items.push({
key: String(row && row.id !== undefined && row.id !== null ? row.id : `${groupKey}_${Math.random()}`),
subjectCode: row ? row.subjectCode : undefined,
subjectName: row ? row.subjectName : undefined,
subjectContent: row ? row.subjectContent : undefined,
remark: row ? row.remark : undefined,
finishDateLabel: formatDateOnly(row ? row.finishDate : undefined),
resultLabel: resultMeta.label,
resultType: resultMeta.type,
images: parseImages(row ? (row.malfunctionUrl || row.malfunctionImages) : undefined)
})
}
return Array.from(groupsMap.values())
})
function handleTabChange(e) {
const idx = e && typeof e === 'object' ? e.index : e
currentTab.value = Number(idx === 0 ? 0 : idx || 0)
}
onLoad(async (query) => {
const rawId = query && (query.id !== undefined ? query.id : query.code)
const decoded = rawId ? decodeURIComponent(String(rawId)) : ''
moldId.value = decoded ? decoded : undefined
await initAllDict()
await fetchAll()
})
async function fetchAll() {
if (!moldId.value) {
uni.showToast({ title: '缺少模具ID', icon: 'none' })
return
}
loading.value = true
try {
const [detailRes, inspectionRes, maintainRes, repairRes] = await Promise.all([
getMoldDetail(moldId.value),
getMoldInspectionByMoldId(moldId.value),
getMoldMaintenanceByMoldId(moldId.value),
getMoldRepairListByMoldId(moldId.value)
])
detailData.value = detailRes.data
inspectionList.value = normalizeList(inspectionRes)
maintainList.value = normalizeList(maintainRes)
repairList.value = normalizeList(repairRes)
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
function normalizeList(res) {
const data = res && res.data !== undefined ? res.data : res
if (Array.isArray(data)) return data
if (data && Array.isArray(data.data)) return data.data
if (data && data.data && Array.isArray(data.data.list)) return data.data.list
if (data && data.data && Array.isArray(data.data.rows)) return data.data.rows
if (data && data.data && Array.isArray(data.data.records)) return data.data.records
if (data && Array.isArray(data.list)) return data.list
if (data && Array.isArray(data.rows)) return data.rows
if (data && Array.isArray(data.records)) return data.records
return []
}
function detailValue(v) {
if (v === 0) return '0'
if (v === false) return '否'
if (v === true) return '是'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s ? s : '-'
}
function formatEnable(v) {
if (v === true || v === 'true' || v === 1 || v === '1') return '是'
if (v === false || v === 'false' || v === 0 || v === '0') return '否'
return detailValue(v)
}
function formatStatus(v) {
const raw = v === null || v === undefined ? '' : String(v).trim()
const upper = raw.toUpperCase()
if (!raw) return { label: '-', type: 'normal' }
if (raw === '1' || upper === 'OK' || raw.includes('正常')) return { label: raw, type: 'normal' }
if (raw === '2' || upper === 'NG' || raw.includes('停') || raw.includes('禁') || raw.includes('坏') || raw.includes('修')) return { label: raw, type: 'error' }
return { label: raw, type: 'warning' }
}
function formatResult(v) {
const raw = v === null || v === undefined ? '' : String(v).trim()
const upper = raw.toUpperCase()
if (!raw) return { label: '-', type: 'info' }
if (raw === '0') return { label: '待检测', type: 'info' }
if (raw === '1' || upper === 'OK') return { label: '通过', type: 'success' }
if (raw === '2' || upper === 'NG') return { label: '不通过', type: 'danger' }
return { label: raw, type: 'info' }
}
function formatHistoryTime(value) {
if (!value) return ''
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d, hh, mm, ss] = value
const pad = (n) => String(n).padStart(2, '0')
if (hh !== undefined) return `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}:${pad(ss)}`
return `${y}-${pad(m)}-${pad(d)}`
}
const s = String(value).trim()
if (!s) return ''
const num = Number(s)
if (Number.isFinite(num)) {
const ms = s.length === 10 ? num * 1000 : num
const d = new Date(ms)
if (!Number.isNaN(d.getTime())) return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
}
const d = new Date(s)
if (!Number.isNaN(d.getTime())) return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
return s
}
function formatDateOnly(value) {
const t = formatHistoryTime(value)
if (!t) return '-'
return String(t).split(' ')[0]
}
function parseImages(value) {
if (!value) return []
if (Array.isArray(value)) return value.map(String).filter(Boolean)
const cleaned = String(value).replace(/[`'"]/g, '').trim()
return cleaned.split(',').map((v) => v.trim()).filter(Boolean)
}
function pickFirst(obj, keys) {
for (const k of keys) {
if (obj && obj[k] !== undefined && obj[k] !== null && String(obj[k]).trim() !== '') return obj[k]
}
return undefined
}
function buildStepGroups(rows, options) {
const groupsMap = new Map()
const list = Array.isArray(rows) ? rows : []
for (const row of list) {
const time = formatHistoryTime(pickFirst(row, options.timeFieldCandidates) || (row ? row.createTime : undefined))
const operator = detailValue(row ? (row.operator || row.creatorName || row.creator) : undefined)
const managementId = row && row.managementId !== undefined && row.managementId !== null ? row.managementId : ''
const groupKey = `${managementId}__${time}__${operator}`
const name = pickFirst(row, options.nameFieldCandidates) || '-'
const resultMeta = formatResult(pickFirst(row, options.resultFieldCandidates))
const item = {
key: String(row && row.id !== undefined && row.id !== null ? row.id : `${groupKey}_${String(name)}`),
name: detailValue(name),
resultLabel: resultMeta.label,
resultType: resultMeta.type,
method: pickFirst(row, options.methodFieldCandidates),
criteria: pickFirst(row, options.criteriaFieldCandidates),
remark: pickFirst(row, options.remarkFieldCandidates),
images: parseImages(pickFirst(row, options.imagesFieldCandidates)),
taskTimeLabel: formatHistoryTime(row ? (row.taskTime || row.inspectionTime) : undefined),
createTimeLabel: formatHistoryTime(row ? row.createTime : undefined)
}
if (!groupsMap.has(groupKey)) {
groupsMap.set(groupKey, { key: groupKey, time: time || '-', operator, items: [item] })
} else {
groupsMap.get(groupKey).items.push(item)
}
}
return Array.from(groupsMap.values())
}
function previewImages(list, current) {
if (!list || !list.length) return
uni.previewImage({ urls: list, current })
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f0f2f5;
}
.fixed-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
.content-section {
padding: 168rpx 30rpx 30rpx;
}
.info-card {
background: #ffffff;
border-radius: 20rpx;
padding: 30rpx;
margin-top: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 24rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #f0f2f5;
}
.info-list {
display: flex;
flex-direction: column;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f7fa;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-size: 28rpx;
color: #999999;
}
.info-value {
font-size: 28rpx;
color: #333333;
}
.tabs-box {
margin-bottom: 24rpx;
}
.empty {
padding: 32rpx 0;
text-align: center;
font-size: 28rpx;
color: #999999;
}
.history-group {
background: #f8fafc;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.history-group-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.history-group-time {
font-size: 24rpx;
color: #1a3a5c;
font-weight: 600;
}
.history-group-operator {
font-size: 24rpx;
color: #666666;
}
.history-item {
background: #ffffff;
border-radius: 14rpx;
padding: 18rpx;
margin-bottom: 16rpx;
}
.history-item:last-child {
margin-bottom: 0;
}
.history-item-head {
display: flex;
align-items: center;
margin-bottom: 14rpx;
}
.history-item-name {
font-size: 28rpx;
color: #333333;
font-weight: 600;
}
.result-badge {
padding: 6rpx 14rpx;
border-radius: 999rpx;
font-size: 22rpx;
margin-right: 14rpx;
}
.result-success {
background: rgba(24, 188, 55, 0.12);
color: #18bc37;
}
.result-danger {
background: rgba(255, 77, 79, 0.12);
color: #ff4d4f;
}
.result-info {
background: rgba(153, 153, 153, 0.12);
color: #666666;
}
.history-item-body {
display: flex;
flex-direction: column;
}
.history-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 10rpx 0;
}
.history-label {
font-size: 24rpx;
color: #999999;
width: 160rpx;
flex-shrink: 0;
}
.history-value {
font-size: 24rpx;
color: #333333;
flex: 1;
text-align: right;
}
.history-images {
margin-top: 12rpx;
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.history-image {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
background: #f0f2f5;
}
.repair-group {
background: #f8fafc;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.repair-group-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.repair-group-name {
font-size: 28rpx;
color: #1a3a5c;
font-weight: 600;
}
.repair-group-meta {
font-size: 24rpx;
color: #666666;
}
.repair-item {
background: #ffffff;
border-radius: 14rpx;
padding: 18rpx;
margin-bottom: 16rpx;
}
.repair-item:last-child {
margin-bottom: 0;
}
.repair-item-head {
display: flex;
align-items: center;
margin-bottom: 14rpx;
}
.repair-tag {
padding: 6rpx 14rpx;
border-radius: 999rpx;
font-size: 22rpx;
background: rgba(74, 144, 194, 0.12);
color: #2d5a87;
margin-right: 14rpx;
}
.repair-title {
font-size: 28rpx;
color: #333333;
font-weight: 600;
}
</style>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,164 @@
<template>
<view class="page-container">
<view class="fixed-header">
<NavBar :title="t('moldTaskConfig.detailTitle')" />
</view>
<scroll-view scroll-y class="detail-scroll">
<view class="content-section">
<!-- 基础信息 -->
<view class="info-card">
<view class="card-title">{{ t('moldTaskConfig.basicInfo') }}</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">{{ t('moldTaskConfig.name') }}</text>
<text class="info-value">{{ textValue(detailData.name) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldTaskConfig.taskType') }}</text>
<text class="info-value">{{ taskTypeText }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldTaskConfig.moldList') }}</text>
<text class="info-value">{{ moldListText }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldTaskConfig.projectFormName') }}</text>
<text class="info-value">{{ textValue(detailData.projectFormName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldTaskConfig.dateRange') }}</text>
<text class="info-value">{{ formatDate(detailData.startDate) }} ~ {{ formatDate(detailData.endDate) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldTaskConfig.cronExpression') }}</text>
<text class="info-value">{{ textValue(detailData.cronExpression) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldTaskConfig.operableUsers') }}</text>
<text class="info-value">{{ operableUsersText }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldTaskConfig.enabled') }}</text>
<text :class="['info-value', enabledClass]">{{ enabledText }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldTaskConfig.creatorName') }}</text>
<text class="info-value">{{ textValue(detailData.creatorName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldTaskConfig.createTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData.createTime) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldTaskConfig.updateTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData.updateTime) }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { computed, reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
const { t } = useI18n()
const detailData = reactive({})
const taskTypeText = computed(() => {
const v = detailData.taskType
if (v === 1) return t('moldTaskConfig.taskTypeInspect')
if (v === 2) return t('moldTaskConfig.taskTypeMaintain')
return textValue(v)
})
const enabledText = computed(() => {
const v = detailData.enabled
if (v === true || v === 1 || v === '1' || v === 'true') return t('functionCommon.yes')
if (v === false || v === 0 || v === '0' || v === 'false') return t('functionCommon.no')
return textValue(v)
})
const enabledClass = computed(() => {
const v = detailData.enabled
if (v === true || v === 1 || v === '1' || v === 'true') return 'text-success'
if (v === false || v === 0 || v === '0' || v === 'false') return 'text-danger'
return ''
})
const moldListText = computed(() => {
const v = detailData.moldList
if (!v) return '-'
if (Array.isArray(v)) return v.join(', ') || '-'
return textValue(v)
})
const operableUsersText = computed(() => {
const v = detailData.operableUsers
if (!v) return '-'
if (Array.isArray(v)) return v.join(', ') || '-'
return textValue(v)
})
function textValue(v) {
if (v === 0) return '0'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s || '-'
}
function formatDate(v) {
if (!v) return '-'
if (Array.isArray(v) && v.length >= 3) {
const [y, m, d] = v
const p = (n) => String(n).padStart(2, '0')
return `${y}-${p(m)}-${p(d)}`
}
const s = String(v).trim().substring(0, 10)
return s || '-'
}
function formatDateTime(v) {
if (!v) return '-'
if (Array.isArray(v) && v.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = v
const p = (n) => String(n).padStart(2, '0')
return `${y}-${p(m)}-${p(d)} ${p(hh)}:${p(mm)}:${p(ss)}`
}
const d = new Date(Number(v))
if (Number.isNaN(d.getTime())) return textValue(v)
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
onLoad(async () => {
try {
const cached = uni.getStorageSync('moldTaskConfigDetail')
if (cached) {
const data = JSON.parse(cached)
Object.assign(detailData, data)
uni.removeStorageSync('moldTaskConfigDetail')
}
} catch (e) {}
})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.fixed-header { position: sticky; top: 0; z-index: 20; }
.detail-scroll { height: calc(100vh - 88rpx); }
.content-section { padding: 0 24rpx 24rpx; }
.info-card { margin-top: 20rpx; background: #fff; border-radius: 20rpx; padding: 28rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); }
.card-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; margin-bottom: 18rpx; }
.info-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 18rpx 0; border-bottom: 1rpx solid #edf0f3; }
.info-label { font-size: 27rpx; color: #8a9099; width: 220rpx; }
.info-value { flex: 1; text-align: right; font-size: 28rpx; color: #303133; line-height: 1.45; }
.text-success { color: #18bc37; }
.text-danger { color: #e34d59; }
</style>

@ -0,0 +1,775 @@
<template>
<view class="page-container">
<NavBar :title="t('moldTaskConfig.moduleName')" :subTitle="t('moldTaskConfig.subTitle')" />
<view class="search-card">
<view class="search-row">
<view class="search-input-wrap">
<text class="iconfont icon-search search-icon"></text>
<input v-model="searchKeyword" class="search-input" :placeholder="t('moldTaskConfig.searchPlaceholder')" @confirm="handleSearch" />
</view>
<view class="search-btn" @click="handleSearch">{{ t('functionCommon.search') }}</view>
</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="type-card" @click="openDetail(item)">
<view class="card-header">
<view class="header-left">
<text class="type-name">{{ textValue(item.name) }}</text>
<view class="tag-row">
<text :class="['type-code', item.taskType === 1 ? 'tag-inspect' : 'tag-maintain']">{{ taskTypeLabel(item.taskType) }}</text>
</view>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('moldTaskConfig.projectFormName') }}</text>
<text class="value">{{ textValue(item.projectFormName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldTaskConfig.dateRange') }}</text>
<text class="value">{{ formatDate(item.startDate) }} ~ {{ formatDate(item.endDate) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldTaskConfig.enabled') }}</text>
<text :class="['value', item.enabled ? 'text-success' : 'text-danger']">{{ enabledText(item.enabled) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldTaskConfig.creatorName') }}</text>
<text class="value">{{ textValue(item.creatorName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldTaskConfig.createTime') }}</text>
<text class="value">{{ formatDateTime(item.createTime) }}</text>
</view>
</view>
<view class="card-actions">
<view class="action-btn ticket-btn" @click.stop="handleCreateTicket(item)">
<uni-icons type="paperplane" size="18" color="#ffffff"></uni-icons>
</view>
<view class="action-btn edit-btn" @click.stop="openEdit(item)">
<uni-icons type="compose" size="18" color="#ffffff"></uni-icons>
</view>
<view class="action-btn delete-btn" @click.stop="confirmDelete(item)">
<uni-icons type="trash" size="18" color="#ffffff"></uni-icons>
</view>
</view>
</view>
<view v-if="loading && pageNo === 1" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!list.length" class="hint">{{ t('moldTaskConfig.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 class="fab-btn" @click="openCreate">
<uni-icons type="plusempty" size="30" color="#fff"></uni-icons>
</view>
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<uni-icons type="arrow-up" size="20" color="#1a3a5c"></uni-icons>
</view>
<!-- 新增/编辑弹框 -->
<uni-popup ref="formPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ formMode === 'create' ? t('moldTaskConfig.createTitle') : t('moldTaskConfig.editTitle') }}</text>
<view class="popup-close" @click="closeForm">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="form-scroll">
<view class="form-content">
<view class="form-item">
<text class="form-label">{{ t('moldTaskConfig.name') }} <text class="required-star">*</text></text>
<input v-model="formData.name" class="form-input" type="text" :placeholder="t('moldTaskConfig.placeholderName')" />
</view>
<view class="form-item">
<text class="form-label">{{ t('moldTaskConfig.taskType') }} <text class="required-star">*</text></text>
<picker mode="selector" :range="taskTypeOptions" range-key="label" :value="taskTypeIndex" @change="onTaskTypeChange">
<view class="picker-view">{{ currentTaskTypeLabel || t('moldTaskConfig.placeholderTaskType') }}</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">{{ t('moldTaskConfig.moldList') }} <text class="required-star">*</text></text>
<view class="multi-select-wrap" @click="openMoldPicker">
<text v-if="selectedMoldLabels.length" class="selected-text">{{ selectedMoldLabels.join(', ') }}</text>
<text v-else class="placeholder-text">{{ t('moldTaskConfig.placeholderMoldList') }}</text>
</view>
</view>
<view class="form-item">
<text class="form-label">{{ t('moldTaskConfig.projectForm') }} <text class="required-star">*</text></text>
<view class="multi-select-wrap" @click="openPlanPicker">
<text v-if="selectedPlanLabels.length" class="selected-text">{{ selectedPlanLabels.join(', ') }}</text>
<text v-else class="placeholder-text">{{ t('moldTaskConfig.placeholderProjectForm') }}</text>
</view>
</view>
<view class="form-item">
<text class="form-label">{{ t('moldTaskConfig.dateRange') }} <text class="required-star">*</text></text>
<view class="date-range-wrap">
<picker mode="date" :value="formData.startDate" @change="onStartDateChange">
<view class="picker-view date-picker">{{ formData.startDate || t('moldTaskConfig.placeholderStartDate') }}</view>
</picker>
<text class="date-separator">~</text>
<picker mode="date" :value="formData.endDate" @change="onEndDateChange">
<view class="picker-view date-picker">{{ formData.endDate || t('moldTaskConfig.placeholderEndDate') }}</view>
</picker>
</view>
</view>
<view class="form-item">
<text class="form-label">{{ t('moldTaskConfig.cronExpression') }}</text>
<input v-model="formData.cronExpression" class="form-input" type="text" :placeholder="t('moldTaskConfig.placeholderCron')" />
</view>
<view class="form-item">
<text class="form-label">{{ t('moldTaskConfig.operableUsers') }}</text>
<view class="multi-select-wrap" @click="openUserPicker">
<text v-if="selectedUserLabels.length" class="selected-text">{{ selectedUserLabels.join(', ') }}</text>
<text v-else class="placeholder-text">{{ t('moldTaskConfig.placeholderOperableUsers') }}</text>
</view>
</view>
<view class="form-item">
<text class="form-label">{{ t('moldTaskConfig.enabled') }} <text class="required-star">*</text></text>
<view class="radio-wrap">
<view :class="['radio-item', formData.enabled ? 'radio-active' : '']" @click="formData.enabled = true">
<text :class="['radio-text', formData.enabled ? 'radio-text-active' : '']">{{ t('functionCommon.yes') }}</text>
</view>
<view :class="['radio-item', !formData.enabled ? 'radio-active' : '']" @click="formData.enabled = false">
<text :class="['radio-text', !formData.enabled ? 'radio-text-active' : '']">{{ t('functionCommon.no') }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeForm"><text class="btn-text">{{ t('functionCommon.cancel') }}</text></view>
<view class="footer-btn confirm-btn" @click="submitForm"><text class="btn-text">{{ t('functionCommon.save') }}</text></view>
</view>
</view>
</uni-popup>
<!-- 模具选择弹框 -->
<uni-popup ref="moldPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ t('moldTaskConfig.moldSelectTitle') }}</text>
<view class="popup-close" @click="closeMoldPicker">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="subject-scroll">
<view class="subject-list">
<view v-for="item in moldOptions" :key="item.id" class="subject-item" @click="toggleMold(item)">
<view class="checkbox-wrap">
<view :class="['checkbox-box', isMoldSelected(item.id) ? 'checked' : '']">
<uni-icons v-if="isMoldSelected(item.id)" type="checkmarkempty" size="16" color="#fff"></uni-icons>
</view>
</view>
<view class="subject-info">
<text class="subject-name">{{ item.name }}</text>
</view>
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeMoldPicker"><text class="btn-text">{{ t('functionCommon.cancel') }}</text></view>
<view class="footer-btn confirm-btn" @click="confirmMoldSelect"><text class="btn-text">{{ t('functionCommon.save') }}</text></view>
</view>
</view>
</uni-popup>
<!-- 点检模板选择弹框 -->
<uni-popup ref="planPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ t('moldTaskConfig.planSelectTitle') }}</text>
<view class="popup-close" @click="closePlanPicker">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="subject-scroll">
<view class="subject-list">
<view v-for="item in planOptions" :key="item.id" class="subject-item" @click="togglePlan(item)">
<view class="checkbox-wrap">
<view :class="['checkbox-box', isPlanSelected(item.id) ? 'checked' : '']">
<uni-icons v-if="isPlanSelected(item.id)" type="checkmarkempty" size="16" color="#fff"></uni-icons>
</view>
</view>
<view class="subject-info">
<text class="subject-name">{{ item.planName }}</text>
</view>
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closePlanPicker"><text class="btn-text">{{ t('functionCommon.cancel') }}</text></view>
<view class="footer-btn confirm-btn" @click="confirmPlanSelect"><text class="btn-text">{{ t('functionCommon.save') }}</text></view>
</view>
</view>
</uni-popup>
<!-- 用户选择弹框 -->
<uni-popup ref="userPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ t('moldTaskConfig.userSelectTitle') }}</text>
<view class="popup-close" @click="closeUserPicker">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="subject-scroll">
<view class="subject-list">
<view v-for="item in userOptions" :key="item.id" class="subject-item" @click="toggleUser(item)">
<view class="checkbox-wrap">
<view :class="['checkbox-box', isUserSelected(item.id) ? 'checked' : '']">
<uni-icons v-if="isUserSelected(item.id)" type="checkmarkempty" size="16" color="#fff"></uni-icons>
</view>
</view>
<view class="subject-info">
<text class="subject-name">{{ item.nickname }}</text>
</view>
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeUserPicker"><text class="btn-text">{{ t('functionCommon.cancel') }}</text></view>
<view class="footer-btn confirm-btn" @click="confirmUserSelect"><text class="btn-text">{{ t('functionCommon.save') }}</text></view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getTaskManagementPage, createTaskManagement, updateTaskManagement, deleteTaskManagement, createTaskManagementTicket } from '@/api/mes/moldTaskConfiguration'
import { getMoldList } from '@/api/mes/mold'
import { getMoldInspectionPlanPage } from '@/api/mes/moldInspectionPlan'
import { getSimpleUserList } from '@/api/mes/moldget'
const { t } = useI18n()
const formPopupRef = ref(null)
const moldPopupRef = ref(null)
const planPopupRef = ref(null)
const userPopupRef = ref(null)
const searchKeyword = ref('')
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)
const formMode = ref('create')
const ticketLoadingId = ref(null)
const taskTypeOptions = computed(() => [
{ label: t('moldTaskConfig.taskTypeInspect'), value: 1 },
{ label: t('moldTaskConfig.taskTypeMaintain'), value: 2 }
])
const formData = reactive({
id: undefined,
name: '',
taskType: undefined,
moldList: [],
projectForm: [],
startDate: '',
endDate: '',
cronExpression: '',
operableUsers: [],
enabled: true
})
const taskTypeIndex = computed(() => {
const idx = taskTypeOptions.value.findIndex(o => o.value === formData.taskType)
return idx === -1 ? 0 : idx
})
const currentTaskTypeLabel = computed(() => {
const opt = taskTypeOptions.value.find(o => o.value === formData.taskType)
return opt ? opt.label : ''
})
const moldOptions = ref([])
const planOptions = ref([])
const userOptions = ref([])
const tempMoldIds = ref([])
const tempPlanIds = ref([])
const tempUserIds = ref([])
const selectedMoldLabels = computed(() => {
return formData.moldList.map(id => {
const item = moldOptions.value.find(m => String(m.id) === String(id))
return item ? item.name : id
})
})
const selectedPlanLabels = computed(() => {
return formData.projectForm.map(id => {
const item = planOptions.value.find(p => String(p.id) === String(id))
return item ? item.planName : id
})
})
const selectedUserLabels = computed(() => {
return formData.operableUsers.map(id => {
const item = userOptions.value.find(u => String(u.id) === String(id))
return item ? item.nickname : id
})
})
onLoad(async () => {
await fetchList(true)
loadOptions()
})
async function loadOptions() {
try {
const [moldRes, planRes, userRes] = await Promise.all([
getMoldList(),
getMoldInspectionPlanPage({ pageNo: 1, pageSize: 10 }),
getSimpleUserList()
])
const moldRoot = moldRes && moldRes.data !== undefined ? moldRes.data : moldRes
moldOptions.value = Array.isArray(moldRoot) ? moldRoot : (moldRoot?.list || [])
const planRoot = planRes && planRes.data !== undefined ? planRes.data : planRes
planOptions.value = planRoot?.list || []
const userRoot = userRes && userRes.data !== undefined ? userRes.data : userRes
userOptions.value = Array.isArray(userRoot) ? userRoot : (userRoot?.list || [])
} catch (e) {}
}
async function handleSearch() {
await fetchList(true)
}
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
}
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,
name: searchKeyword.value.trim() || undefined
}
const res = await getTaskManagementPage(params)
const page = normalizePageData(res)
if (reset) {
list.value = page.list
} else {
list.value = [...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 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 || root?.data?.records || []
const candidateTotal = root?.total ?? root?.data?.total ?? (Array.isArray(candidateList) ? candidateList.length : 0)
return { list: Array.isArray(candidateList) ? candidateList : [], total: Number(candidateTotal || 0) }
}
function onScroll(e) {
showGoTop.value = (e?.detail?.scrollTop || 0) > 600
}
function goTop() {
scrollTop.value = 0
}
function taskTypeLabel(v) {
if (v === 1) return t('moldTaskConfig.taskTypeInspect')
if (v === 2) return t('moldTaskConfig.taskTypeMaintain')
return textValue(v)
}
function enabledText(v) {
if (v === true || v === 1 || v === '1' || v === 'true') return t('functionCommon.yes')
if (v === false || v === 0 || v === '0' || v === 'false') return t('functionCommon.no')
return textValue(v)
}
function textValue(v) {
if (v === 0) return '0'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s || '-'
}
function formatDate(v) {
if (!v) return '-'
if (Array.isArray(v) && v.length >= 3) {
const [y, m, d] = v
const p = (n) => String(n).padStart(2, '0')
return `${y}-${p(m)}-${p(d)}`
}
const s = String(v).trim().substring(0, 10)
return s || '-'
}
function formatDateTime(v) {
if (!v) return '-'
if (Array.isArray(v) && v.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = v
const p = (n) => String(n).padStart(2, '0')
return `${y}-${p(m)}-${p(d)} ${p(hh)}:${p(mm)}:${p(ss)}`
}
const d = new Date(Number(v))
if (Number.isNaN(d.getTime())) return textValue(v)
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
function openDetail(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
uni.setStorageSync('moldTaskConfigDetail', JSON.stringify(item))
uni.navigateTo({
url: `/pages_function/pages/moldTaskConfiguration/detail?id=${encodeURIComponent(String(item.id))}`
})
}
function openCreate() {
formMode.value = 'create'
resetForm()
formPopupRef.value?.open()
}
async function openEdit(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdEdit'), icon: 'none' })
return
}
try {
const res = await getTaskManagementPage({ pageNo: 1, pageSize: 1, id: item.id })
const page = normalizePageData(res)
const detail = page.list.find(r => r.id === item.id) || item
formMode.value = 'update'
formData.id = detail.id
formData.name = detail.name || ''
formData.taskType = detail.taskType
formData.moldList = parseIdsValue(detail.moldList)
formData.projectForm = parseIdsValue(detail.projectForm)
if (!formData.projectForm.length && detail.projectFormName) {
const names = parseIdsValue(detail.projectFormName)
formData.projectForm = names.map(name => {
const p = planOptions.value.find(o => o.planName === name)
return p ? String(p.id) : null
}).filter(Boolean)
}
formData.startDate = detail.startDate || ''
formData.endDate = detail.endDate || ''
formData.cronExpression = detail.cronExpression || ''
formData.operableUsers = parseIdsValue(detail.operableUsers)
formData.enabled = detail.enabled === true || detail.enabled === 'true' || detail.enabled === 1
formPopupRef.value?.open()
} catch (e) {
uni.showToast({ title: t('moldTaskConfig.loadEditFailed'), icon: 'none' })
}
}
function parseIdsValue(value) {
if (!value) return []
if (Array.isArray(value)) return value.map(v => String(v).trim()).filter(Boolean)
return String(value).split(',').map(v => v.trim()).filter(Boolean)
}
function toCommaSeparatedIds(value) {
const ids = parseIdsValue(value)
return ids.length ? ids.join(',') : undefined
}
async function confirmDelete(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdDelete'), icon: 'none' })
return
}
uni.showModal({
title: t('functionCommon.confirmDelete'),
content: t('moldTaskConfig.confirmDeleteContent', { name: textValue(item.name) }),
success: async (res) => {
if (!res.confirm) return
try {
await deleteTaskManagement([item.id])
uni.showToast({ title: t('functionCommon.deleteSuccess'), icon: 'success' })
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.deleteFailed'), icon: 'none' })
}
}
})
}
async function handleCreateTicket(item) {
if (!item?.id) return
if (ticketLoadingId.value) return
ticketLoadingId.value = item.id
try {
await createTaskManagementTicket(item.id)
uni.showToast({ title: t('moldTaskConfig.createTicketSuccess'), icon: 'success' })
} catch (e) {
uni.showToast({ title: t('moldTaskConfig.createTicketFail'), icon: 'none' })
} finally {
ticketLoadingId.value = null
}
}
function closeForm() {
formPopupRef.value?.close()
}
function resetForm() {
formData.id = undefined
formData.name = ''
formData.taskType = undefined
formData.moldList = []
formData.projectForm = []
formData.startDate = ''
formData.endDate = ''
formData.cronExpression = ''
formData.operableUsers = []
formData.enabled = true
}
function onTaskTypeChange(e) {
const idx = e.detail.value
formData.taskType = taskTypeOptions.value[idx]?.value
}
function onStartDateChange(e) {
formData.startDate = e.detail.value
}
function onEndDateChange(e) {
formData.endDate = e.detail.value
}
async function submitForm() {
if (!formData.name.trim()) {
uni.showToast({ title: t('moldTaskConfig.validatorNameRequired'), icon: 'none' })
return
}
if (!formData.taskType) {
uni.showToast({ title: t('moldTaskConfig.validatorTaskTypeRequired'), icon: 'none' })
return
}
if (!formData.moldList.length) {
uni.showToast({ title: t('moldTaskConfig.validatorMoldListRequired'), icon: 'none' })
return
}
if (!formData.projectForm.length) {
uni.showToast({ title: t('moldTaskConfig.validatorProjectFormRequired'), icon: 'none' })
return
}
if (!formData.startDate || !formData.endDate) {
uni.showToast({ title: t('moldTaskConfig.validatorDateRangeRequired'), icon: 'none' })
return
}
const payload = {
id: formMode.value === 'update' ? formData.id : undefined,
name: formData.name.trim(),
taskType: formData.taskType,
moldList: toCommaSeparatedIds(formData.moldList),
projectForm: toCommaSeparatedIds(formData.projectForm),
startDate: formData.startDate,
endDate: formData.endDate,
cronExpression: formData.cronExpression.trim() || undefined,
operableUsers: toCommaSeparatedIds(formData.operableUsers),
enabled: formData.enabled
}
try {
if (formMode.value === 'create') {
await createTaskManagement(payload)
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
} else {
await updateTaskManagement(payload)
uni.showToast({ title: t('functionCommon.updateSuccess'), icon: 'success' })
}
closeForm()
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
}
}
function openMoldPicker() {
tempMoldIds.value = [...formData.moldList]
moldPopupRef.value?.open()
}
function closeMoldPicker() {
moldPopupRef.value?.close()
}
function isMoldSelected(id) {
return tempMoldIds.value.includes(String(id))
}
function toggleMold(item) {
const id = String(item.id)
const idx = tempMoldIds.value.indexOf(id)
if (idx === -1) {
tempMoldIds.value.push(id)
} else {
tempMoldIds.value.splice(idx, 1)
}
}
function confirmMoldSelect() {
formData.moldList = [...tempMoldIds.value]
closeMoldPicker()
}
function openPlanPicker() {
tempPlanIds.value = [...formData.projectForm]
planPopupRef.value?.open()
}
function closePlanPicker() {
planPopupRef.value?.close()
}
function isPlanSelected(id) {
return tempPlanIds.value.includes(String(id))
}
function togglePlan(item) {
const id = String(item.id)
const idx = tempPlanIds.value.indexOf(id)
if (idx === -1) {
tempPlanIds.value.push(id)
} else {
tempPlanIds.value.splice(idx, 1)
}
}
function confirmPlanSelect() {
formData.projectForm = [...tempPlanIds.value]
closePlanPicker()
}
function openUserPicker() {
tempUserIds.value = [...formData.operableUsers]
userPopupRef.value?.open()
}
function closeUserPicker() {
userPopupRef.value?.close()
}
function isUserSelected(id) {
return tempUserIds.value.includes(String(id))
}
function toggleUser(item) {
const id = String(item.id)
const idx = tempUserIds.value.indexOf(id)
if (idx === -1) {
tempUserIds.value.push(id)
} else {
tempUserIds.value.splice(idx, 1)
}
}
function confirmUserSelect() {
formData.operableUsers = [...tempUserIds.value]
closeUserPicker()
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.search-card { background: #ffffff; margin: 20rpx 24rpx; border-radius: 18rpx; padding: 20rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.04); }
.search-row { display: flex; align-items: center; gap: 16rpx; }
.search-input-wrap { flex: 1; display: flex; align-items: center; background: #f5f7fa; border-radius: 44rpx; padding: 0 20rpx; }
.search-icon { margin-right: 16rpx; font-size: 34rpx; color: #666; }
.search-input { flex: 1; height: 72rpx; font-size: 28rpx; color: #333; background: transparent; }
.search-btn { min-width: 120rpx; height: 72rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); border-radius: 14rpx; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 28rpx; font-weight: 600; }
.list-scroll { height: calc(100vh - 280rpx); }
.list-wrap { padding: 0 24rpx 130rpx; }
.type-card { background: #ffffff; border-radius: 18rpx; padding: 24rpx; margin-bottom: 18rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.05); }
.card-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 18rpx; border-bottom: 1rpx solid #edf0f3; }
.header-left { display: flex; flex-direction: column; gap: 8rpx; flex: 1; }
.type-name { font-size: 32rpx; font-weight: 700; color: #1f2d3d; }
.tag-row { display: flex; align-items: center; }
.type-code { font-size: 24rpx; padding: 4rpx 14rpx; border-radius: 8rpx; }
.tag-inspect { color: #1a3a5c; background: rgba(26, 58, 92, 0.08); }
.tag-maintain { color: #18bc37; background: rgba(24, 188, 55, 0.08); }
.card-actions { margin-top: 24rpx; display: flex; justify-content: flex-end; gap: 14rpx; }
.action-btn { width: 60rpx; height: 60rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.edit-btn { background: #1a3a5c; }
.delete-btn { background: #e34d59; }
.ticket-btn { background: #18bc37; }
.card-body { padding-top: 16rpx; }
.row { display: flex; justify-content: space-between; align-items: center; margin-top: 12rpx; }
.label { font-size: 26rpx; color: #8a9099; }
.value { font-size: 27rpx; color: #30363d; max-width: 62%; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.text-success { color: #18bc37; }
.text-danger { color: #e34d59; }
.hint { text-align: center; color: #909399; padding: 24rpx 0; }
.fab-btn { position: fixed; right: 28rpx; bottom: 140rpx; width: 96rpx; height: 96rpx; border-radius: 48rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); display: flex; align-items: center; justify-content: center; box-shadow: 0 8rpx 24rpx rgba(26, 58, 92, 0.32); z-index: 99; }
.go-top-btn { position: fixed; right: 28rpx; bottom: 254rpx; width: 88rpx; height: 88rpx; border-radius: 44rpx; background: rgba(26, 58, 92, 0.9); display: flex; align-items: center; justify-content: center; z-index: 99; box-shadow: 0 6rpx 16rpx rgba(26, 58, 92, 0.25); }
.popup-content { width: 680rpx; max-height: 80vh; border-radius: 24rpx; overflow: hidden; }
.popup-header { padding: 24rpx; display: flex; align-items: center; justify-content: space-between; border-bottom: 1rpx solid #edf0f3; position: relative; }
.popup-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; }
.popup-close { position: absolute; right: 24rpx; top: 20rpx; width: 48rpx; height: 48rpx; display: flex; align-items: center; justify-content: center; }
.close-icon { font-size: 42rpx; color: #9aa0a6; line-height: 1; }
.form-scroll { max-height: calc(80vh - 180rpx); }
.form-content { padding: 22rpx 28rpx; }
.form-item { margin-bottom: 18rpx; }
.form-label { display: block; font-size: 25rpx; color: #5f6b7a; margin-bottom: 10rpx; }
.required-star { color: #e34d59; }
.form-input { min-height: 72rpx; background: #f7f9fc; border-radius: 12rpx; padding: 0 18rpx; display: flex; align-items: center; font-size: 26rpx; color: #2f3a45; }
.picker-view { min-height: 72rpx; background: #f7f9fc; border-radius: 12rpx; padding: 0 18rpx; display: flex; align-items: center; font-size: 26rpx; color: #2f3a45; }
.multi-select-wrap { min-height: 72rpx; background: #f7f9fc; border-radius: 12rpx; padding: 14rpx 18rpx; font-size: 26rpx; }
.selected-text { color: #2f3a45; word-break: break-all; }
.placeholder-text { color: #c0c4cc; }
.date-range-wrap { display: flex; align-items: center; gap: 12rpx; }
.date-picker { flex: 1; min-width: 0; }
.date-separator { font-size: 26rpx; color: #8a9099; }
.radio-wrap { display: flex; gap: 20rpx; }
.radio-item { min-width: 120rpx; height: 72rpx; background: #f7f9fc; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; border: 2rpx solid #e4e7ed; }
.radio-active { border-color: #1a3a5c; background: rgba(26, 58, 92, 0.06); }
.radio-text { font-size: 26rpx; color: #8a9099; }
.radio-text-active { color: #1a3a5c; font-weight: 600; }
.form-footer { height: 88rpx; display: flex; border-top: 1rpx solid #edf0f3; }
.footer-btn { flex: 1; display: flex; align-items: center; justify-content: center; }
.btn-text { font-size: 28rpx; }
.cancel-btn { background: #f4f6fa; color: #5f6b7a; }
.confirm-btn { background: #1a3a5c; color: #fff; }
.subject-scroll { max-height: 50vh; }
.subject-list { padding: 16rpx 24rpx; }
.subject-item { display: flex; align-items: center; padding: 18rpx 0; border-bottom: 1rpx solid #f0f2f5; }
.checkbox-wrap { margin-right: 20rpx; }
.checkbox-box { width: 40rpx; height: 40rpx; border-radius: 8rpx; border: 2rpx solid #d0d5dd; display: flex; align-items: center; justify-content: center; background: #fff; }
.checkbox-box.checked { background: #1a3a5c; border-color: #1a3a5c; }
.subject-info { display: flex; flex-direction: column; gap: 4rpx; }
.subject-name { font-size: 28rpx; color: #30363d; }
</style>

@ -0,0 +1,207 @@
<template>
<view class="page-container">
<NavBar title="模具类型详情" />
<view class="content-section">
<view class="info-card">
<view class="card-title">基础信息</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">型号编码</text>
<text class="info-value">{{ fieldValue('code') }}</text>
</view>
<view class="info-row">
<text class="info-label">型号名称</text>
<text class="info-value">{{ fieldValue('name') }}</text>
</view>
<view class="info-row">
<text class="info-label">规格</text>
<text class="info-value">{{ fieldValue('moldType') }}</text>
</view>
<view class="info-row">
<text class="info-label">工序</text>
<text class="info-value">{{ orgTypeText }}</text>
</view>
<view class="info-row">
<text class="info-label">预期寿命(小时)</text>
<text class="info-value">{{ fieldValue('useTime') }}</text>
</view>
<view class="info-row">
<text class="info-label">模穴数</text>
<text class="info-value">{{ fieldValue('moldSize') }}</text>
</view>
<view class="info-row">
<text class="info-label">维保模式</text>
<text class="info-value">{{ fieldValue('maintainType') }}</text>
</view>
<view class="info-row">
<text class="info-label">维保周期</text>
<text class="info-value">{{ fieldValue('maintainTime') }}</text>
</view>
<view class="info-row">
<text class="info-label">是否启用</text>
<text class="info-value">{{ enableText }}</text>
</view>
<view class="info-row">
<text class="info-label">创建时间</text>
<text class="info-value">{{ formatDateTime(detailData?.createTime) }}</text>
</view>
<view class="info-row">
<text class="info-label">更新时间</text>
<text class="info-value">{{ formatDateTime(detailData?.updateTime) }}</text>
</view>
<view class="info-row remark-row">
<text class="info-label">备注</text>
<text class="info-value remark-value">{{ fieldValue('remark') }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import NavBar from '@/components/common/NavBar.vue'
import { getMoldBrandDetail } from '@/api/mes/mold'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const detailId = ref(undefined)
const detailData = ref(null)
const orgTypeText = computed(() =>
getDictLabel(DICT_TYPE.MES_ORG_TYPE, detailData.value?.orgType, textValue(detailData.value?.orgType))
)
const enableText = computed(() => {
const value = detailData.value?.isEnable
if (value === true || value === 1 || value === '1' || value === 'true') return '是'
if (value === false || value === 0 || value === '0' || value === 'false') return '否'
return textValue(value)
})
onLoad(async (query) => {
const id = query?.id ? decodeURIComponent(String(query.id)) : ''
detailId.value = id || undefined
await initAllDict()
await fetchDetail()
})
async function fetchDetail() {
if (!detailId.value) {
uni.showToast({ title: '缺少模具类型ID', icon: 'none' })
return
}
try {
const res = await getMoldBrandDetail(detailId.value)
detailData.value = normalizeDetail(res)
} catch (e) {
uni.showToast({ title: '详情加载失败', icon: 'none' })
}
}
function normalizeDetail(res) {
const root = res && res.data !== undefined ? res.data : res
if (root?.data && typeof root.data === 'object') return root.data
if (root && typeof root === 'object') return root
return {}
}
function fieldValue(field) {
return textValue(detailData.value ? detailData.value[field] : undefined)
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return '否'
if (value === true) return '是'
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function formatDateTime(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = value
const pad = (n) => String(n).padStart(2, '0')
return `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}:${pad(ss)}`
}
const text = String(value).trim()
if (!text) return '-'
const numeric = Number(text)
if (Number.isFinite(numeric)) {
const timestamp = text.length === 10 ? numeric * 1000 : numeric
const date = new Date(timestamp)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
}
const date = new Date(text)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
return text
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f0f2f5;
}
.content-section {
padding: 0 24rpx 24rpx;
}
.info-card {
margin-top: 20rpx;
background: #ffffff;
border-radius: 20rpx;
padding: 28rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.card-title {
font-size: 32rpx;
color: #1a3a5c;
font-weight: 700;
margin-bottom: 18rpx;
}
.info-list {
background: #ffffff;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 18rpx 0;
border-bottom: 1rpx solid #edf0f3;
}
.info-label {
font-size: 27rpx;
color: #8a9099;
width: 220rpx;
}
.info-value {
flex: 1;
text-align: right;
font-size: 28rpx;
color: #30363d;
line-height: 1.45;
}
.remark-row {
border-bottom: none;
}
.remark-value {
white-space: pre-wrap;
}
</style>

@ -0,0 +1,760 @@
<template>
<view class="page-container">
<NavBar title="模具类型" subTitle="按编码或名称快速查询" />
<view class="search-section">
<view class="search-wrapper">
<view class="search-icon">
<text class="iconfont icon-search"></text>
</view>
<input
v-model="searchKeyword"
class="search-input"
type="text"
placeholder="请输入模具型号编码或名称"
placeholder-class="input-placeholder"
@confirm="handleSearch"
/>
<view v-if="searchKeyword" class="clear-btn" @click="clearSearch">
<text class="clear-icon">×</text>
</view>
</view>
<view class="search-btn" @click="handleSearch">
<text class="search-btn-text">查询</text>
</view>
</view>
<scroll-view
scroll-y
class="content-scroll"
:scroll-top="scrollTop"
@scroll="handleScroll"
@scrolltolower="loadMore"
:lower-threshold="80"
>
<view class="list-wrap">
<view v-for="item in list" :key="item.id" class="type-card" @click="openDetail(item)">
<view class="card-header">
<view class="header-left">
<text class="type-name">{{ textValue(item.name) }}</text>
<view class="code-wrapper">
<text class="type-code">编码{{ textValue(item.code) }}</text>
<CopyButton :content="item.code" />
</view>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">规格</text>
<text class="value">{{ textValue(item.moldType) }}</text>
</view>
<view class="row">
<text class="label">工序</text>
<text class="value">{{ orgTypeLabel(item.orgType) }}</text>
</view>
<view class="row">
<text class="label">预期寿命</text>
<text class="value">{{ textValue(item.useTime) }}</text>
</view>
<view class="row">
<text class="label">模穴数</text>
<text class="value">{{ textValue(item.moldSize) }}</text>
</view>
<view class="row">
<text class="label">创建时间</text>
<text class="value">{{ formatDateTime(item.createTime) }}</text>
</view>
</view>
<view class="card-actions">
<view class="action-btn edit-btn" @click.stop="openEdit(item)">
<uni-icons type="compose" size="18" color="#ffffff"></uni-icons>
</view>
<view class="action-btn delete-btn" @click.stop="confirmDelete(item)">
<uni-icons type="trash" size="18" color="#ffffff"></uni-icons>
</view>
</view>
</view>
<view v-if="loading && pageNo === 1" class="loading-text">...</view>
<view v-else-if="!list.length" class="empty-text">暂无模具类型数据</view>
<view v-else-if="loadingMore" class="loading-text">正在加载更多...</view>
<view v-else-if="finished" class="finished-text">没有更多数据了</view>
</view>
</scroll-view>
<view class="add-btn" @click="openCreate">
<text class="add-icon">+</text>
</view>
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<text class="go-top-icon"></text>
</view>
<uni-popup ref="formPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ formMode === 'create' ? '新增模具类型' : '编辑模具类型' }}</text>
<view class="popup-close" @click="closeForm">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="form-scroll">
<view class="form-content">
<view class="form-item">
<text class="form-label">编码 <text class="required-star">*</text></text>
<input v-model="formData.code" class="form-input" type="text" placeholder="请输入编码" :disabled="formMode === 'update'"/>
</view>
<view class="form-item">
<text class="form-label">名称 <text class="required-star">*</text></text>
<input v-model="formData.name" class="form-input" type="text" placeholder="请输入名称" />
</view>
<view class="form-item">
<text class="form-label">规格 <text class="required-star">*</text></text>
<input v-model="formData.moldType" class="form-input" type="text" placeholder="请输入规格" />
</view>
<view class="form-item">
<text class="form-label">工序</text>
<input v-model="formData.orgType" class="form-input" type="text" placeholder="请输入工序编码" />
</view>
<view class="form-item">
<text class="form-label">预期寿命(小时)</text>
<input v-model="formData.useTime" class="form-input" type="number" placeholder="请输入预期寿命" />
</view>
<view class="form-item">
<text class="form-label">模穴数 <text class="required-star">*</text></text>
<input v-model="formData.moldSize" class="form-input" type="number" placeholder="请输入模穴数" />
</view>
<view class="form-item">
<text class="form-label">备注</text>
<textarea v-model="formData.remark" class="form-textarea" placeholder="请输入备注" :maxlength="200" />
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeForm">
<text class="btn-text">取消</text>
</view>
<view class="footer-btn confirm-btn" @click="submitForm">
<text class="btn-text">保存</text>
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import NavBar from '@/components/common/NavBar.vue'
import CopyButton from '@/components/common/CopyButton.vue'
import { createMoldBrand, deleteMoldBrand, getMoldBrandDetail, getMoldBrandPage, updateMoldBrand } from '@/api/mes/mold'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const formPopupRef = ref(null)
const searchKeyword = ref('')
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const finished = ref(false)
const pageNo = ref(1)
const pageSize = ref(10)
const total = ref(0)
const scrollTop = ref(0)
const showGoTop = ref(false)
const formMode = ref('create')
const formData = reactive({
id: undefined,
code: '',
name: '',
moldType: '',
orgType: '',
useTime: '',
moldSize: '',
remark: ''
})
onLoad(async () => {
await initAllDict()
await fetchList(true)
})
async function handleSearch() {
await fetchList(true)
}
async function clearSearch() {
searchKeyword.value = ''
await fetchList(true)
}
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
}
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,
code: searchKeyword.value.trim() || undefined,
name: searchKeyword.value.trim() || undefined
}
const res = await getMoldBrandPage(params)
const page = normalizePageData(res)
total.value = page.total
if (reset) {
list.value = page.list
} else {
list.value = [...list.value, ...page.list]
}
const loadedCount = list.value.length
finished.value = loadedCount >= total.value || page.list.length < pageSize.value
} catch (e) {
if (!reset) {
pageNo.value = Math.max(1, pageNo.value - 1)
}
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
loadingMore.value = false
}
}
function handleScroll(e) {
const top = e?.detail?.scrollTop || 0
showGoTop.value = top > 600
}
function goTop() {
scrollTop.value = 0
}
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 ||
root?.data?.records ||
[]
const candidateTotal =
root?.total ??
root?.data?.total ??
(Array.isArray(candidateList) ? candidateList.length : 0)
return {
list: Array.isArray(candidateList) ? candidateList : [],
total: Number(candidateTotal || 0)
}
}
function orgTypeLabel(value) {
return getDictLabel(DICT_TYPE.MES_ORG_TYPE, value, textValue(value))
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return '否'
if (value === true) return '是'
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function formatDateTime(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = value
const pad = (n) => String(n).padStart(2, '0')
return `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}:${pad(ss)}`
}
const text = String(value).trim()
if (!text) return '-'
const numeric = Number(text)
if (Number.isFinite(numeric)) {
const timestamp = text.length === 10 ? numeric * 1000 : numeric
const date = new Date(timestamp)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
}
const date = new Date(text)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
return text
}
function openCreate() {
formMode.value = 'create'
resetForm()
formPopupRef.value?.open()
}
async function openEdit(item) {
const id = item?.id
if (id === undefined || id === null) {
uni.showToast({ title: '缺少ID无法编辑', icon: 'none' })
return
}
try {
const res = await getMoldBrandDetail(id)
const detail = normalizeDetailData(res)
formMode.value = 'update'
formData.id = detail?.id
formData.code = textValueForInput(detail?.code)
formData.name = textValueForInput(detail?.name)
formData.moldType = textValueForInput(detail?.moldType)
formData.orgType = textValueForInput(detail?.orgType)
formData.useTime = textValueForInput(detail?.useTime)
formData.moldSize = textValueForInput(detail?.moldSize)
formData.remark = textValueForInput(detail?.remark)
formPopupRef.value?.open()
} catch (e) {
uni.showToast({ title: '加载编辑数据失败', icon: 'none' })
}
}
async function confirmDelete(item) {
const id = item?.id
if (id === undefined || id === null) {
uni.showToast({ title: '缺少ID无法删除', icon: 'none' })
return
}
uni.showModal({
title: '确认删除',
content: `确认删除模具类型"${textValue(item?.name)}"吗?`,
success: async (res) => {
if (!res.confirm) return
try {
await deleteMoldBrand(id)
uni.showToast({ title: '删除成功', icon: 'success' })
await fetchList(true)
} catch (e) {
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
})
}
function closeForm() {
formPopupRef.value?.close()
}
async function submitForm() {
if (!formData.code.trim() || !formData.name.trim() || !formData.moldType.trim()) {
uni.showToast({ title: '编码、名称、规格必填', icon: 'none' })
return
}
if (String(formData.moldSize).trim() === '') {
uni.showToast({ title: '模穴数必填', icon: 'none' })
return
}
const payload = {
id: formMode.value === 'update' ? formData.id : undefined,
code: formData.code.trim(),
name: formData.name.trim(),
moldType: formData.moldType.trim(),
orgType: formData.orgType.trim() || undefined,
useTime: toNumberOrUndefined(formData.useTime),
moldSize: toNumberOrUndefined(formData.moldSize),
remark: formData.remark.trim() || undefined,
isEnable: true
}
try {
if (formMode.value === 'create') {
await createMoldBrand(payload)
uni.showToast({ title: '新增成功', icon: 'success' })
} else {
await updateMoldBrand(payload)
uni.showToast({ title: '更新成功', icon: 'success' })
}
closeForm()
await fetchList(true)
} catch (e) {
uni.showToast({ title: '保存失败', icon: 'none' })
}
}
function resetForm() {
formData.id = undefined
formData.code = ''
formData.name = ''
formData.moldType = ''
formData.orgType = ''
formData.useTime = ''
formData.moldSize = ''
formData.remark = ''
}
function toNumberOrUndefined(value) {
if (value === null || value === undefined || String(value).trim() === '') return undefined
const num = Number(value)
return Number.isFinite(num) ? num : undefined
}
function textValueForInput(value) {
if (value === null || value === undefined) return ''
return String(value)
}
function normalizeDetailData(res) {
const root = res && res.data !== undefined ? res.data : res
if (root?.data && typeof root.data === 'object') return root.data
if (root && typeof root === 'object') return root
return {}
}
function openDetail(item) {
const id = item?.id
if (!id && id !== 0) {
uni.showToast({ title: '缺少ID无法查看详情', icon: 'none' })
return
}
uni.navigateTo({
url: `/pages_function/pages/moldType/detail?id=${encodeURIComponent(String(id))}`
})
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f0f2f5;
}
.search-section {
background: #ffffff;
margin: 20rpx 24rpx;
border-radius: 18rpx;
padding: 20rpx;
box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.04);
}
.search-wrapper {
display: flex;
align-items: center;
background: #f5f7fa;
border-radius: 44rpx;
padding: 0 20rpx;
}
.search-icon {
margin-right: 16rpx;
.iconfont {
font-size: 34rpx;
color: #666666;
}
}
.search-input {
flex: 1;
height: 72rpx;
font-size: 28rpx;
color: #333333;
background: transparent;
}
.input-placeholder {
color: #999999;
}
.clear-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
.clear-icon {
font-size: 34rpx;
color: #999999;
}
}
.search-btn {
margin-top: 18rpx;
height: 76rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
}
.search-btn-text {
color: #ffffff;
font-size: 30rpx;
font-weight: 600;
}
.content-scroll {
height: calc(100vh - 360rpx);
}
.list-wrap {
padding: 0 24rpx 30rpx;
}
.type-card {
background: #ffffff;
border-radius: 18rpx;
padding: 24rpx;
margin-bottom: 18rpx;
box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 18rpx;
border-bottom: 1rpx solid #edf0f3;
}
.header-left {
display: flex;
flex-direction: column;
}
.card-actions {
margin-top: 24rpx;
display: flex;
justify-content: flex-end;
gap: 14rpx;
}
.action-btn {
width: 60rpx;
height: 60rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
}
.action-icon {
color: #ffffff;
font-size: 30rpx;
}
.edit-btn {
background: #1a3a5c;
}
.delete-btn {
background: #ff4d4f;
}
.type-name {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 8rpx;
}
.type-code {
font-size: 24rpx;
color: #8a9099;
}
.code-wrapper {
display: flex;
align-items: center;
gap: 8rpx;
}
.arrow {
font-size: 44rpx;
color: #a1a8b0;
}
.card-body {
padding-top: 16rpx;
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12rpx;
}
.label {
font-size: 26rpx;
color: #8a9099;
}
.value {
font-size: 27rpx;
color: #30363d;
max-width: 62%;
text-align: right;
}
.add-btn {
position: fixed;
right: 28rpx;
bottom: 140rpx;
width: 96rpx;
height: 96rpx;
border-radius: 48rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(26, 58, 92, 0.32);
z-index: 99;
}
.add-icon {
color: #ffffff;
font-size: 56rpx;
line-height: 1;
}
.go-top-btn {
position: fixed;
right: 28rpx;
bottom: 254rpx;
width: 88rpx;
height: 88rpx;
border-radius: 44rpx;
background: rgba(26, 58, 92, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 99;
box-shadow: 0 6rpx 16rpx rgba(26, 58, 92, 0.25);
}
.go-top-icon {
color: #ffffff;
font-size: 36rpx;
font-weight: 700;
}
.popup-content {
width: 680rpx;
max-height: 80vh;
border-radius: 20rpx;
overflow: hidden;
}
.popup-header {
padding: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1rpx solid #edf0f3;
}
.popup-title {
font-size: 32rpx;
color: #1a3a5c;
font-weight: 700;
}
.close-icon {
font-size: 38rpx;
color: #8e95a0;
}
.form-scroll {
max-height: 56vh;
}
.form-content {
padding: 24rpx;
}
.form-item {
margin-bottom: 18rpx;
}
.form-label {
display: block;
margin-bottom: 8rpx;
font-size: 26rpx;
color: #8a9099;
}
.required-star {
color: #ff4d4f;
}
.form-input {
height: 76rpx;
padding: 0 20rpx;
border-radius: 12rpx;
background: #f5f7fa;
font-size: 28rpx;
color: #30363d;
}
.form-textarea {
width: 100%;
min-height: 130rpx;
padding: 18rpx 20rpx;
border-radius: 12rpx;
background: #f5f7fa;
font-size: 28rpx;
color: #30363d;
}
.form-footer {
display: flex;
gap: 14rpx;
padding: 20rpx 24rpx 24rpx;
}
.footer-btn {
flex: 1;
height: 76rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
}
.cancel-btn {
background: #edf0f4;
}
.confirm-btn {
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
}
.btn-text {
font-size: 28rpx;
font-weight: 600;
color: #ffffff;
}
.cancel-btn .btn-text {
color: #586070;
}
.loading-text,
.empty-text,
.finished-text {
text-align: center;
padding: 28rpx 0;
color: #99a1aa;
font-size: 26rpx;
}
</style>

@ -0,0 +1,306 @@
<template>
<view class="page-container">
<view class="fixed-header">
<NavBar :title="t('moldWorkOrder.detailTitle')" />
</view>
<scroll-view scroll-y class="detail-scroll">
<view class="content-section">
<!-- 基础信息 -->
<view class="info-card">
<view class="card-title">{{ t('moldWorkOrder.basicInfo') }}</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.planNo') }}</text>
<text class="info-value">{{ textValue(detailData.planNo) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.moldName') }}</text>
<text class="info-value">{{ textValue(detailData.moldName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.planType') }}</text>
<text class="info-value">{{ planTypeText }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.configName') }}</text>
<text class="info-value">{{ textValue(detailData.configName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.jobStatus') }}</text>
<text :class="['info-value', jobStatusClass]">{{ jobStatusText }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.jobResult') }}</text>
<text :class="['info-value', jobResultClass]">{{ jobResultText }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.operatorName') }}</text>
<text class="info-value">{{ textValue(detailData.operatorName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.taskTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData.taskTime) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.taskEndTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData.taskEndTime) }}</text>
</view>
<view v-if="String(detailData.jobStatus) === '4'" class="info-row">
<text class="info-label">{{ t('moldWorkOrder.cancelReason') }}</text>
<text class="info-value">{{ textValue(detailData.cancelReason) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.createTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData.createTime) }}</text>
</view>
</view>
</view>
<!-- 检验结果列表 -->
<view class="info-card">
<view class="card-title">{{ t('moldWorkOrder.resultListTitle') }}</view>
<view v-if="resultLoading" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!resultList.length" class="hint">{{ t('moldWorkOrder.noResultData') }}</view>
<view v-else class="result-list">
<view v-for="(item, index) in resultList" :key="item.id || index" class="result-card">
<view class="result-index">{{ index + 1 }}</view>
<view class="result-body">
<view class="result-row">
<text class="result-label">{{ t('moldWorkOrder.inspectionItemName') }}</text>
<text class="result-value">{{ textValue(item.inspectionItemName) }}</text>
</view>
<view class="result-row">
<text class="result-label">{{ t('moldWorkOrder.inspectionMethod') }}</text>
<text class="result-value">{{ getInspectionMethodLabel(item.inspectionMethod) }}</text>
</view>
<view class="result-row">
<text class="result-label">{{ t('moldWorkOrder.judgmentCriteria') }}</text>
<text class="result-value">{{ textValue(item.judgmentCriteria) }}</text>
</view>
<view class="result-row">
<text class="result-label">{{ t('moldWorkOrder.valueType') }}</text>
<text class="result-value">{{ getValueTypeLabel(item.valueType) }}</text>
</view>
<view class="result-row">
<text class="result-label">{{ t('moldWorkOrder.inspectionResult') }}</text>
<text :class="['result-value', inspectionResultClass(item.inspectionResult)]">{{ inspectionResultText(item.inspectionResult) }}</text>
</view>
<view v-if="item.textInput" class="result-row">
<text class="result-label">{{ t('moldWorkOrder.textInput') }}</text>
<text class="result-value">{{ textValue(item.textInput) }}</text>
</view>
<view v-if="item.remark" class="result-row">
<text class="result-label">{{ t('moldWorkOrder.remark') }}</text>
<text class="result-value">{{ textValue(item.remark) }}</text>
</view>
<view v-if="item.images" class="result-row">
<text class="result-label">{{ t('moldWorkOrder.images') }}</text>
<view class="result-images">
<image v-for="(img, imgIdx) in parseImages(item.images)" :key="imgIdx" :src="img" class="result-img" mode="aspectFill" @click="previewImage(img, parseImages(item.images))" />
</view>
</view>
</view>
</view>
</view>
<view v-if="resultList.length < resultTotal" class="load-more-btn" @click="loadMoreResults">
<text class="load-more-text">{{ resultLoadingMore ? t('functionCommon.loadingMore') : t('moldWorkOrder.loadMore') }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getTicketResultsPage } from '@/api/mes/moldWorkOrderInquiry'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const { t } = useI18n()
const detailData = reactive({})
const resultList = ref([])
const resultTotal = ref(0)
const resultLoading = ref(false)
const resultLoadingMore = ref(false)
const resultPageNo = ref(1)
const resultPageSize = ref(10)
const managementId = ref(null)
const planTypeText = computed(() => {
const v = detailData.planType
if (String(v) === '1') return t('moldWorkOrder.planTypeInspect')
if (String(v) === '2') return t('moldWorkOrder.planTypeMaintain')
return textValue(v)
})
const jobStatusText = computed(() => {
return getDictLabel(DICT_TYPE.JOB_STATUS, detailData.jobStatus, textValue(detailData.jobStatus))
})
const jobStatusClass = computed(() => {
const s = String(detailData.jobStatus)
if (s === '0') return 'text-warning'
if (s === '1') return 'text-primary'
if (s === '2') return 'text-success'
if (s === '3') return 'text-danger'
if (s === '4') return 'text-muted'
return ''
})
const jobResultText = computed(() => {
const v = detailData.jobResult
if (String(v) === '1') return t('moldWorkOrder.jobResultOk')
if (String(v) === '2') return t('moldWorkOrder.jobResultNg')
return textValue(v)
})
const jobResultClass = computed(() => {
if (String(detailData.jobResult) === '1') return 'text-success'
if (String(detailData.jobResult) === '2') return 'text-danger'
return ''
})
function getInspectionMethodLabel(value) {
return getDictLabel(DICT_TYPE.INSPECTION_METHOD, value, textValue(value))
}
function getValueTypeLabel(value) {
return getDictLabel(DICT_TYPE.VALUE_TYPES, value, textValue(value))
}
function inspectionResultText(v) {
if (String(v) === '0') return t('moldWorkOrder.inspectionResultPending')
if (String(v) === '1') return t('moldWorkOrder.inspectionResultPass')
if (String(v) === '2') return t('moldWorkOrder.inspectionResultFail')
return textValue(v)
}
function inspectionResultClass(v) {
if (String(v) === '0') return 'text-warning'
if (String(v) === '1') return 'text-success'
if (String(v) === '2') return 'text-danger'
return ''
}
function textValue(v) {
if (v === 0) return '0'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s || '-'
}
function formatDateTime(v) {
if (!v) return '-'
if (Array.isArray(v) && v.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = v
const p = (n) => String(n).padStart(2, '0')
return `${y}-${p(m)}-${p(d)} ${p(hh)}:${p(mm)}:${p(ss)}`
}
const d = new Date(Number(v))
if (Number.isNaN(d.getTime())) return textValue(v)
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
function parseImages(value) {
if (!value) return []
if (Array.isArray(value)) return value.map(String).filter(Boolean)
return String(value).split(',').map(v => v.trim()).filter(Boolean)
}
function previewImage(current, urls) {
uni.previewImage({ current, urls })
}
async function fetchResults(reset) {
if (!managementId.value) return
if (reset) {
resultPageNo.value = 1
}
if (resultPageNo.value === 1) {
resultLoading.value = true
} else {
resultLoadingMore.value = true
}
try {
const res = await getTicketResultsPage({
pageNo: resultPageNo.value,
pageSize: resultPageSize.value,
managementId: managementId.value
})
const root = res && res.data !== undefined ? res.data : res
const items = root?.list || root?.rows || root?.records || []
const total = root?.total ?? 0
if (reset) {
resultList.value = Array.isArray(items) ? items : []
} else {
resultList.value = [...resultList.value, ...(Array.isArray(items) ? items : [])]
}
resultTotal.value = Number(total)
} catch (e) {
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
} finally {
resultLoading.value = false
resultLoadingMore.value = false
}
}
function loadMoreResults() {
if (resultLoadingMore.value) return
resultPageNo.value += 1
fetchResults(false)
}
onLoad(async (query) => {
const id = query?.id
if (!id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
await initAllDict()
managementId.value = id
try {
const cached = uni.getStorageSync('moldWorkOrderDetail')
if (cached) {
const data = JSON.parse(cached)
Object.assign(detailData, data)
uni.removeStorageSync('moldWorkOrderDetail')
}
} catch (e) {}
await fetchResults(true)
})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.fixed-header { position: sticky; top: 0; z-index: 20; }
.detail-scroll { height: calc(100vh - 88rpx); }
.content-section { padding: 0 24rpx 24rpx; }
.info-card { margin-top: 20rpx; background: #fff; border-radius: 20rpx; padding: 28rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); }
.card-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; margin-bottom: 18rpx; }
.info-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 18rpx 0; border-bottom: 1rpx solid #edf0f3; }
.info-label { font-size: 27rpx; color: #8a9099; width: 220rpx; }
.info-value { flex: 1; text-align: right; font-size: 28rpx; color: #303133; line-height: 1.45; }
.text-success { color: #18bc37; }
.text-danger { color: #e34d59; }
.text-warning { color: #f0883e; }
.text-primary { color: #1a3a5c; }
.text-muted { color: #909399; }
.hint { text-align: center; color: #909399; padding: 24rpx 0; font-size: 26rpx; }
.result-list { display: flex; flex-direction: column; gap: 16rpx; }
.result-card { display: flex; background: #f7f9fc; border-radius: 14rpx; padding: 20rpx; gap: 16rpx; }
.result-index { min-width: 48rpx; height: 48rpx; border-radius: 10rpx; background: #1a3a5c; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 24rpx; font-weight: 700; }
.result-body { flex: 1; display: flex; flex-direction: column; gap: 8rpx; }
.result-row { display: flex; justify-content: space-between; align-items: flex-start; }
.result-label { font-size: 24rpx; color: #8a9099; min-width: 160rpx; }
.result-value { font-size: 26rpx; color: #30363d; flex: 1; text-align: right; }
.result-images { display: flex; flex-wrap: wrap; gap: 10rpx; flex: 1; justify-content: flex-end; }
.result-img { width: 100rpx; height: 100rpx; border-radius: 8rpx; }
.load-more-btn { text-align: center; padding: 20rpx 0 0; }
.load-more-text { font-size: 26rpx; color: #1a3a5c; }
</style>

@ -0,0 +1,406 @@
<template>
<view class="page-container">
<NavBar :title="t('moldWorkOrder.moduleName')" :subTitle="t('moldWorkOrder.subTitle')" />
<!-- 搜索区域 -->
<view class="search-card">
<view class="search-row">
<view class="search-input-wrap">
<text class="iconfont icon-search search-icon"></text>
<input v-model="searchKeyword" class="search-input" :placeholder="t('moldWorkOrder.searchPlaceholder')" @confirm="handleSearch" />
</view>
<view class="search-btn" @click="handleSearch">{{ t('functionCommon.search') }}</view>
</view>
<view class="filter-row">
<picker mode="selector" :range="planTypeFilterOptions" range-key="label" @change="onPlanTypeFilter">
<view :class="['filter-tag', filterPlanType ? 'filter-active' : '']">{{ filterPlanTypeLabel || t('moldWorkOrder.planType') }}</view>
</picker>
<picker mode="selector" :range="jobStatusFilterOptions" range-key="label" @change="onJobStatusFilter">
<view :class="['filter-tag', filterJobStatus ? 'filter-active' : '']">{{ filterJobStatusLabel || t('moldWorkOrder.jobStatus') }}</view>
</picker>
<picker mode="selector" :range="jobResultFilterOptions" range-key="label" @change="onJobResultFilter">
<view :class="['filter-tag', filterJobResult ? 'filter-active' : '']">{{ filterJobResultLabel || t('moldWorkOrder.jobResult') }}</view>
</picker>
</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="type-card" @click="openDetail(item)">
<view class="card-header">
<view class="header-left">
<text class="type-name">{{ textValue(item.planNo) }}</text>
<view class="tag-row">
<text :class="['type-code', String(item.planType) === '1' ? 'tag-inspect' : 'tag-maintain']">{{ planTypeLabel(item.planType) }}</text>
</view>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('moldWorkOrder.moldName') }}</text>
<text class="value">{{ textValue(item.moldName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldWorkOrder.configName') }}</text>
<text class="value">{{ textValue(item.configName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldWorkOrder.jobStatus') }}</text>
<text :class="['value', jobStatusClass(item.jobStatus)]">{{ jobStatusLabel(item.jobStatus) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldWorkOrder.jobResult') }}</text>
<text :class="['value', jobResultClass(item.jobResult)]">{{ jobResultLabel(item.jobResult) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldWorkOrder.operatorName') }}</text>
<text class="value">{{ textValue(item.operatorName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldWorkOrder.taskTime') }}</text>
<text class="value">{{ formatDateTime(item.taskTime) }}</text>
</view>
</view>
<view class="card-actions">
<view v-if="canCancel(item)" class="action-btn cancel-btn" @click.stop="handleCancel(item)">
<uni-icons type="closeempty" size="18" color="#ffffff"></uni-icons>
</view>
</view>
</view>
<view v-if="loading && pageNo === 1" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!list.length" class="hint">{{ t('moldWorkOrder.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="#1a3a5c"></uni-icons>
</view>
<!-- 取消原因弹框 -->
<uni-popup ref="cancelPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ t('moldWorkOrder.cancelTask') }}</text>
<view class="popup-close" @click="closeCancelPopup">
<text class="close-icon">×</text>
</view>
</view>
<view class="form-content">
<view class="form-item">
<text class="form-label">{{ t('moldWorkOrder.cancelReason') }} <text class="required-star">*</text></text>
<textarea v-model="cancelReason" class="form-textarea" :placeholder="t('moldWorkOrder.placeholderCancelReason')" />
</view>
</view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeCancelPopup"><text class="btn-text">{{ t('functionCommon.cancel') }}</text></view>
<view class="footer-btn confirm-btn" @click="submitCancel"><text class="btn-text">{{ t('functionCommon.save') }}</text></view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getTicketManagementPage, batchUpdateTicketStatus } from '@/api/mes/moldWorkOrderInquiry'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const { t } = useI18n()
const cancelPopupRef = ref(null)
const searchKeyword = ref('')
const filterPlanType = ref('')
const filterJobStatus = ref('')
const filterJobResult = ref('')
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)
const cancelReason = ref('')
const cancelTargetId = ref(null)
const planTypeFilterOptions = computed(() => [
{ label: t('moldWorkOrder.planTypeInspect'), value: '1' },
{ label: t('moldWorkOrder.planTypeMaintain'), value: '2' }
])
const jobStatusFilterOptions = computed(() => {
const opts = [
{ label: t('moldWorkOrder.jobStatusPending'), value: '0' },
{ label: t('moldWorkOrder.jobStatusProcessing'), value: '1' },
{ label: t('moldWorkOrder.jobStatusCompleted'), value: '2' },
{ label: t('moldWorkOrder.jobStatusTimeout'), value: '3' },
{ label: t('moldWorkOrder.jobStatusCancelled'), value: '4' }
]
return opts
})
const jobResultFilterOptions = computed(() => [
{ label: t('moldWorkOrder.jobResultOk'), value: '1' },
{ label: t('moldWorkOrder.jobResultNg'), value: '2' }
])
const filterPlanTypeLabel = computed(() => {
if (!filterPlanType.value) return ''
const opt = planTypeFilterOptions.value.find(o => o.value === filterPlanType.value)
return opt ? opt.label : ''
})
const filterJobStatusLabel = computed(() => {
if (!filterJobStatus.value) return ''
return getDictLabel(DICT_TYPE.JOB_STATUS, filterJobStatus.value, '')
})
const filterJobResultLabel = computed(() => {
if (!filterJobResult.value) return ''
const opt = jobResultFilterOptions.value.find(o => o.value === filterJobResult.value)
return opt ? opt.label : ''
})
onLoad(async () => {
await initAllDict()
await fetchList(true)
})
async function handleSearch() {
await fetchList(true)
}
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
}
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,
planNo: searchKeyword.value.trim() || undefined,
planType: filterPlanType.value || undefined,
jobStatus: filterJobStatus.value || undefined,
jobResult: filterJobResult.value || undefined
}
const res = await getTicketManagementPage(params)
const page = normalizePageData(res)
if (reset) {
list.value = page.list
} else {
list.value = [...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 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 || root?.data?.records || []
const candidateTotal = root?.total ?? root?.data?.total ?? (Array.isArray(candidateList) ? candidateList.length : 0)
return { list: Array.isArray(candidateList) ? candidateList : [], total: Number(candidateTotal || 0) }
}
function onScroll(e) {
showGoTop.value = (e?.detail?.scrollTop || 0) > 600
}
function goTop() {
scrollTop.value = 0
}
function onPlanTypeFilter(e) {
const idx = e.detail.value
const opt = planTypeFilterOptions.value[idx]
filterPlanType.value = opt ? opt.value : ''
handleSearch()
}
function onJobStatusFilter(e) {
const idx = e.detail.value
const opt = jobStatusFilterOptions.value[idx]
filterJobStatus.value = opt ? opt.value : ''
handleSearch()
}
function onJobResultFilter(e) {
const idx = e.detail.value
const opt = jobResultFilterOptions.value[idx]
filterJobResult.value = opt ? opt.value : ''
handleSearch()
}
function planTypeLabel(v) {
if (String(v) === '1') return t('moldWorkOrder.planTypeInspect')
if (String(v) === '2') return t('moldWorkOrder.planTypeMaintain')
return textValue(v)
}
function jobStatusLabel(v) {
return getDictLabel(DICT_TYPE.JOB_STATUS, v, textValue(v))
}
function jobStatusClass(v) {
const s = String(v)
if (s === '0') return 'text-warning'
if (s === '1') return 'text-primary'
if (s === '2') return 'text-success'
if (s === '3') return 'text-danger'
if (s === '4') return 'text-muted'
return ''
}
function jobResultLabel(v) {
if (String(v) === '1') return t('moldWorkOrder.jobResultOk')
if (String(v) === '2') return t('moldWorkOrder.jobResultNg')
return textValue(v)
}
function jobResultClass(v) {
if (String(v) === '1') return 'text-success'
if (String(v) === '2') return 'text-danger'
return ''
}
function canCancel(item) {
const s = String(item.jobStatus)
return s === '0' || s === '1'
}
function textValue(v) {
if (v === 0) return '0'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s || '-'
}
function formatDateTime(v) {
if (!v) return '-'
if (Array.isArray(v) && v.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = v
const p = (n) => String(n).padStart(2, '0')
return `${y}-${p(m)}-${p(d)} ${p(hh)}:${p(mm)}:${p(ss)}`
}
const d = new Date(Number(v))
if (Number.isNaN(d.getTime())) return textValue(v)
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
function openDetail(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
uni.setStorageSync('moldWorkOrderDetail', JSON.stringify(item))
uni.navigateTo({
url: `/pages_function/pages/moldWorkOrderInquiry/detail?id=${encodeURIComponent(String(item.id))}`
})
}
function handleCancel(item) {
cancelTargetId.value = item.id
cancelReason.value = ''
cancelPopupRef.value?.open()
}
function closeCancelPopup() {
cancelPopupRef.value?.close()
}
async function submitCancel() {
if (!cancelReason.value.trim()) {
uni.showToast({ title: t('moldWorkOrder.validatorCancelReasonRequired'), icon: 'none' })
return
}
try {
await batchUpdateTicketStatus({
ids: String(cancelTargetId.value),
jobStatus: '4',
cancelReason: cancelReason.value.trim()
})
uni.showToast({ title: t('moldWorkOrder.cancelSuccess'), icon: 'success' })
closeCancelPopup()
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('moldWorkOrder.cancelFail'), icon: 'none' })
}
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.search-card { background: #ffffff; margin: 20rpx 24rpx; border-radius: 18rpx; padding: 20rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.04); }
.search-row { display: flex; align-items: center; gap: 16rpx; }
.search-input-wrap { flex: 1; display: flex; align-items: center; background: #f5f7fa; border-radius: 44rpx; padding: 0 20rpx; }
.search-icon { margin-right: 16rpx; font-size: 34rpx; color: #666; }
.search-input { flex: 1; height: 72rpx; font-size: 28rpx; color: #333; background: transparent; }
.search-btn { min-width: 120rpx; height: 72rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); border-radius: 14rpx; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 28rpx; font-weight: 600; }
.filter-row { display: flex; gap: 16rpx; margin-top: 16rpx; flex-wrap: wrap; }
.filter-tag { font-size: 24rpx; padding: 8rpx 20rpx; border-radius: 24rpx; background: #f5f7fa; color: #8a9099; border: 1rpx solid #e4e7ed; }
.filter-active { background: rgba(26, 58, 92, 0.08); color: #1a3a5c; border-color: #1a3a5c; }
.list-scroll { height: calc(100vh - 340rpx); }
.list-wrap { padding: 0 24rpx 40rpx; }
.type-card { background: #ffffff; border-radius: 18rpx; padding: 24rpx; margin-bottom: 18rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.05); }
.card-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 18rpx; border-bottom: 1rpx solid #edf0f3; }
.header-left { display: flex; flex-direction: column; gap: 8rpx; flex: 1; }
.type-name { font-size: 32rpx; font-weight: 700; color: #1f2d3d; }
.tag-row { display: flex; align-items: center; }
.type-code { font-size: 24rpx; padding: 4rpx 14rpx; border-radius: 8rpx; }
.tag-inspect { color: #1a3a5c; background: rgba(26, 58, 92, 0.08); }
.tag-maintain { color: #18bc37; background: rgba(24, 188, 55, 0.08); }
.card-body { padding-top: 16rpx; }
.row { display: flex; justify-content: space-between; align-items: center; margin-top: 12rpx; }
.label { font-size: 26rpx; color: #8a9099; }
.value { font-size: 27rpx; color: #30363d; max-width: 62%; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.text-success { color: #18bc37; }
.text-danger { color: #e34d59; }
.text-warning { color: #f0883e; }
.text-primary { color: #1a3a5c; }
.text-muted { color: #909399; }
.card-actions { margin-top: 24rpx; display: flex; justify-content: flex-end; gap: 14rpx; }
.action-btn { width: 60rpx; height: 60rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.cancel-btn { background: #f0883e; }
.hint { text-align: center; color: #909399; padding: 24rpx 0; }
.go-top-btn { position: fixed; right: 28rpx; bottom: 140rpx; width: 88rpx; height: 88rpx; border-radius: 44rpx; background: rgba(26, 58, 92, 0.9); display: flex; align-items: center; justify-content: center; z-index: 99; box-shadow: 0 6rpx 16rpx rgba(26, 58, 92, 0.25); }
.popup-content { width: 680rpx; max-height: 80vh; overflow: hidden; }
.popup-header { padding: 24rpx; display: flex; align-items: center; justify-content: space-between; border-bottom: 1rpx solid #edf0f3; position: relative; }
.popup-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; }
.popup-close { position: absolute; right: 24rpx; top: 20rpx; width: 48rpx; height: 48rpx; display: flex; align-items: center; justify-content: center; }
.close-icon { font-size: 42rpx; color: #9aa0a6; line-height: 1; }
.form-content { padding: 22rpx 28rpx; }
.form-item { margin-bottom: 18rpx; }
.form-label { display: block; font-size: 25rpx; color: #5f6b7a; margin-bottom: 10rpx; }
.required-star { color: #e34d59; }
.form-textarea { min-height: 160rpx; background: #f7f9fc; border-radius: 12rpx; padding: 18rpx; font-size: 26rpx; color: #2f3a45; width: 100%; box-sizing: border-box; }
.form-footer { height: 88rpx; display: flex; border-top: 1rpx solid #edf0f3; }
.footer-btn { flex: 1; display: flex; align-items: center; justify-content: center; }
.btn-text { font-size: 28rpx; }
.confirm-btn { background: #1a3a5c; color: #fff; }
</style>

@ -0,0 +1,220 @@
<template>
<view class="page-container">
<view class="fixed-header">
<NavBar :title="t('moldGet.detailTitle')" />
</view>
<view class="content-section">
<view class="info-card">
<view class="card-title">{{ t('moldGet.basicInfo') }}</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">{{ t('moldGet.outNo') }}</text>
<text class="info-value">{{ textValue(detail.no) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldGet.outType') }}</text>
<text class="info-value">{{ textValue(detail.outType) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldGet.outTime') }}</text>
<text class="info-value">{{ dateTimeLabel(detail.outTime) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldGet.warehouse') }}</text>
<text class="info-value">{{ textValue(detail.warehouseName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldGet.creator') }}</text>
<text class="info-value">{{ textValue(detail.creatorName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldGet.status') }}</text>
<text class="info-value">{{ statusLabel(detail.status) }}</text>
</view>
<view class="info-row remark-row">
<text class="info-label">{{ t('moldGet.remark') }}</text>
<text class="info-value remark-value">{{ textValue(detail.remark) }}</text>
</view>
</view>
</view>
<view class="tabs-card">
<u-tabs :list="tabList" :current="currentTab" :is-scroll="false" activeColor="#1a3a5c" @change="onTabChange" />
<view v-if="currentTab === 0" class="item-wrap">
<view v-if="!items.length" class="empty">{{ t('moldGet.noItems') }}</view>
<view v-for="(item, index) in items" :key="index" class="item-card">
<view class="item-head">{{ textValue(item.productName) }}</view>
<view class="item-row">
<text class="item-label">{{ t('moldGet.moldCode') }}</text>
<text class="item-value">{{ textValue(item.productCode || item.productBarCode) }}</text>
</view>
<view class="item-row">
<text class="item-label">{{ t('moldGet.count') }}</text>
<text class="item-value">{{ textValue(item.count) }}</text>
</view>
<view class="item-row">
<text class="item-label">{{ t('moldGet.itemRemark') }}</text>
<text class="item-value">{{ textValue(item.remark) }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getMoldGetDetail } from '@/api/mes/moldget'
import { DICT_TYPE, useDict } from '@/utils/dict'
const { t } = useI18n()
const { erp_audit_status } = useDict(DICT_TYPE.ERP_AUDIT_STATUS)
const detail = ref({})
const items = ref([])
const currentTab = ref(0)
const tabList = ref([{ name: t('moldGet.itemListTitle') }])
function textValue(v) {
if (v === 0) return '0'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s || '-'
}
function dateTimeLabel(v) {
if (!v) return '-'
const d = new Date(Number(v))
if (Number.isNaN(d.getTime())) return textValue(v)
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
function statusLabel(status) {
const hit = (erp_audit_status.value || []).find((s) => String(s.value) === String(status))
return hit ? hit.label : textValue(status)
}
function onTabChange(e) {
const idx = e && typeof e === 'object' ? e.index : e
currentTab.value = Number(idx || 0)
}
async function loadDetail(id) {
const res = await getMoldGetDetail(id)
const data = res?.data || {}
detail.value = data
items.value = Array.isArray(data.items) ? data.items : []
}
onLoad(async (query) => {
const id = query?.id
if (!id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
try {
await loadDetail(id)
} catch (e) {
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
}
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: #f0f2f5;
}
.fixed-header {
position: sticky;
top: 0;
z-index: 20;
}
.content-section {
padding: 0 24rpx 24rpx;
}
.info-card,
.tabs-card {
margin-top: 20rpx;
background: #fff;
border-radius: 20rpx;
padding: 28rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.tabs-card {
margin-bottom: 16rpx;
}
.card-title {
font-size: 32rpx;
color: #1a3a5c;
font-weight: 700;
margin-bottom: 18rpx;
}
.info-list {
background: #ffffff;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 18rpx 0;
border-bottom: 1rpx solid #edf0f3;
}
.info-label {
font-size: 27rpx;
color: #8a9099;
width: 220rpx;
}
.info-value {
flex: 1;
text-align: right;
font-size: 28rpx;
color: #303133;
line-height: 1.45;
}
.remark-row {
border-bottom: none;
}
.remark-value {
white-space: pre-wrap;
}
.item-wrap {
margin-top: 18rpx;
}
.item-card {
background: #f8fafc;
border-radius: 12rpx;
padding: 16rpx;
margin-bottom: 12rpx;
}
.item-head {
font-size: 26rpx;
color: #1a3a5c;
font-weight: 700;
}
.item-row {
margin-top: 10rpx;
display: flex;
justify-content: space-between;
}
.item-label {
color: #909399;
font-size: 22rpx;
}
.item-value {
color: #303133;
font-size: 22rpx;
max-width: 65%;
text-align: right;
}
.empty {
text-align: center;
color: #909399;
padding: 24rpx 0;
}
</style>

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

Loading…
Cancel
Save