Compare commits

..

77 Commits
main ... newUI

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

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

@ -0,0 +1 @@
VITE_APP_BASE_URL=

21547
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -56,16 +56,17 @@
"@dcloudio/uni-mp-xhs": "3.0.0-4020320240708001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4020320240708001",
"@dcloudio/uni-ui": "^1.5.6",
"@element-plus/icons-vue": "^2.3.1",
"@jridgewell/sourcemap-codec": "^1.4.15",
"@qiun/wx-ucharts": "2.5.0-20230101",
"@ttou/uview-typings": "^2.0.5",
"clipboard": "^2.0.11",
"dayjs": "^1.11.9",
"element-plus": "^2.7.6",
"mqtt": "4.1.0",
"pinia": "2.0.28",
"pinia-plugin-persistedstate": "^3.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"
},
@ -75,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>
@ -16,4 +65,4 @@ export default {
<style lang="scss">
@import "uview-plus/index.scss";
@import '@/static/scss/index.scss';
</style>
</style>

@ -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({

@ -1,80 +0,0 @@
import request from '@/utils/request'
// 新增能源设备
export function createEnergyDevice(data) {
return request({
url: '/admin-api/mes/energy-device/create',
method: 'POST',
data: data
})
}
// 查询能源设备详情
export function getEnergyDeviceById(id) {
return request({
url: '/admin-api/mes/energy-device/get?id=' + id,
method: 'GET',
})
}
// 修改能源设备
export function updateEnergyDevice(data) {
return request({
url: '/admin-api/mes/energy-device/update',
method: 'PUT',
data: data
})
}
// 查询能源设备列表
export function getEnergyDevice(params) {
return request({
url: '/admin-api/mes/energy-device/page',
method: 'GET',
params: params
})
}
// 删除能源设备
export function deleteEnergyDevice(id) {
return request({
url: '/admin-api/mes/energy-device/delete?id='+id,
method: 'DELETE'
})
}
// 新增抄表记录
export function createEnergyDeviceCheckRecord(data) {
return request({
url: '/admin-api/mes/energy-device/energy-device-check-record/create',
method: 'POST',
data: data
})
}
// 查询抄表记录
export function getEnergyDeviceCheckRecord(params) {
return request({
url: '/admin-api/mes/energy-device/energy-device-check-record/page',
method: 'GET',
params
})
}
// 修改抄表记录
export function updateEnergyDeviceCheckRecord(data) {
return request({
url: '/admin-api/mes/energy-device/update',
method: 'PUT',
data: data
})
}
// 删除抄表记录
export function deleteEnergyDeviceCheckRecordById(id) {
return request({
url: '/admin-api/mes/energy-device/energy-device-check-record/delete?id='+id,
method: 'DELETE'
})
}

@ -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'
})
}

@ -1,45 +0,0 @@
import request from '@/utils/request'
// 查询产线工位列表
export function getListOrgWorker(params) {
return request({
url: '/admin-api/mes/organization/listOrgWorker',
method: 'GET',
params: params
})
}
// 获得班组成员
export function getUserList2(params) {
return request({
url: '/admin-api/mes/work-team/work-team-detail/getUserList2',
method: 'GET',
params: params
})
}
// 多个日期新增工位安排
export function createWorker(params) {
return request({
url: '/admin-api/mes/org-worker/createWorker',
method: 'GET',
params: params
})
}
// 查询工位安排分页
export function getOrgWorkerPage(params) {
return request({
url: '/admin-api/mes/org-worker/page',
method: 'GET',
params: params
})
}
// 删除工位安排
export function deleteOrgWorker(id) {
return request({
url: '/admin-api/mes/org-worker/delete?id='+id,
method: 'DELETE'
})
}

@ -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(',') }
})
}

@ -1,3 +1,4 @@
import request from '@/utils/request'
@ -8,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 }
})
}

@ -61,43 +61,3 @@ export function deleteByReportId(id) {
method: 'delete'
})
}
// 30天报工数据
export function getReportTime(id) {
return request({
url: '/admin-api/mes/app/product/get30DaysReportList?userId='+id,
method: 'get'
})
}
// 上月报工汇总
export function getLastMonthSum(id) {
return request({
url: '/admin-api/mes/app/product/getLastMonthSum?userId='+id,
method: 'get'
})
}
// 本月报工汇总
export function getThisMonthSum(id) {
return request({
url: '/admin-api/mes/app/product/getThisMonthSum?userId='+id,
method: 'get'
})
}
// 本月上月计时汇总
export function getSumReportTime(id) {
return request({
url: '/admin-api/mes/app/product/getSumReportTime?userId='+id,
method: 'get'
})
}
// 最近30天计时列表
export function getDayReportTime(id) {
return request({
url: '/admin-api/mes/app/product/getDay30ReportTime?userId='+id,
method: 'get'
})
}

@ -50,23 +50,14 @@ export function delData(dictCode) {
method: 'delete'
})
}
//
export const deviceTypes=[
{text: '电表', value: '电表'},
{text: '水表', value: '水表'},
{text: '燃气表', value: '燃气表'},
]
export const organizationalStatus=[
{text: '关闭', value: 'close'},
{text: '空闲', value: 'free'},
{text: '使用', value: 'inuse'},
]
export const isEnable=[
{text: '是', value: true},
{text: '否', value: false},
]
export function getSimpleDictList() {
return request({
url: '/admin-api/system/dict-data/simple-list',
method: 'get'
})
}
//
export const processTypes = [
{text: '制浆', value: 'zhijiang'}, {text: '成型', value: 'chengxing'},
{text: '烘干', value: 'honggan'}, {text: '转移', value: 'zhuanyi'},

@ -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,7 +869,9 @@ 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);
ctx.draw();
if(typeof ctx.draw === 'function'){
ctx.draw();
}
}
}
},

@ -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://localhost:48081',
baseUrl: import.meta.env.VITE_APP_BASE_URL,
// 应用信息
appInfo: {
// 应用名称
@ -25,4 +24,4 @@ const config = {
}
}
export default config
export default config

@ -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,16 +2,20 @@ import App from './App.vue'
import plugins from './plugins'
import store from './store'
import uviewPlus from 'uview-plus'
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)
directive(app)
@ -24,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,24 +1,13 @@
<template>
<view>
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
title="新增投料记录"
bg-color="transparent"
:auto-back="true"
:title-style="{ fontWeight: 'bold' }"
safe-area-inset-top
placeholder
/>
</u-sticky>
<view class="container">
<uni-forms ref="customForm" :rules="customRules" labelWidth="80px" :modelValue="formData">
<uni-forms-item label="制浆线" required name="feedingPipeline" class="u-flex align-center">
<view class="container">
<NavBar title="新增投料记录" />
<view class="example">
<uni-forms ref="customForm" :rules="customRules" labelWidth="60px" :modelValue="formData">
<uni-forms-item label="制浆线" required name="feedingPipeline">
<uni-data-checkbox v-model="formData.feedingPipeline" :localdata="pipelineTypes()" />
</uni-forms-item>
<uni-forms-item label="投料类型" required name="feedingType" class="u-flex align-center">
<uni-forms-item label="投料类型" required name="feedingType">
<uni-data-checkbox v-model="formData.feedingType" :localdata="feedingTypes()" @change="handleTypeChange"/>
</uni-forms-item>
<uni-forms-item v-if="formData.feedingType==='wet' || formData.feedingType==='dry'" label="重量/kg" name="weight">
@ -27,45 +16,44 @@
<uni-forms-item label="备注" name="remark">
<uni-easyinput v-model="formData.remark" placeholder="" />
</uni-forms-item>
<uni-group title="card 模式" mode="card" v-for="(item,index) in formData.feedingRecordDetails" :key="item.id"
:name="['feedingRecordDetails',index,'value']">
<uni-group title="card 模式" mode="card" v-for="(item,index) in formData.productList" :key="item.id"
:name="['productList',index,'value']">
<template v-slot:title>
<view class="uni-group-title">
<uni-row>
<uni-col :span="22">
<uni-col :span="18">
<uni-forms-item label="原料" required name="itemId">
<uni-data-select placement="top" v-model="formData.feedingRecordDetails[index].itemId" :localdata="itemList"></uni-data-select>
<uni-data-select placement="top" v-model="formData.productList[index].itemId" :localdata="itemList"></uni-data-select>
</uni-forms-item>
</uni-col>
<uni-col :span="2" class="u-p-t-10">
<uni-col :span="6" align="center">
<uni-icons type="trash" size="25" color="red" @click="del(item.id)"></uni-icons>
</uni-col>
</uni-row>
</view>
</template>
<uni-forms-item label="单位" required name="unitId">
<uni-data-select v-model="formData.feedingRecordDetails[index].unitId" :localdata="unitList"></uni-data-select>
<uni-data-select v-model="formData.productList[index].unitId" :localdata="unitList"></uni-data-select>
</uni-forms-item>
<uni-forms-item label="数量" required name="weight">
<uni-easyinput type="number" v-model="formData.feedingRecordDetails[index].weight"/>
<uni-easyinput type="number" v-model="formData.productList[index].weight"/>
</uni-forms-item>
</uni-group>
</uni-forms>
<view class="u-flex justify-end">
<view class="u-flex u-m-t-30">
<u-button v-if="formData.feedingType==='org'" type="primary" @click="add" class="u-m-r-30">
<uni-icons type="plus" class="u-m-r-10"/> 添加原料
</u-button>
<u-button type="success" @click="submit('customForm')">
<uni-icons type="checkbox" class="u-m-r-10"/>保存
</u-button>
</view>
<view class="button-group">
<button v-if="formData.feedingType==='org'" type="primary" size="mini" @click="add">
<uni-icons type="plus" size="15"></uni-icons>
添加原料
</button>
<button type="primary" size="mini" @click="submit('customForm')"></button>
</view>
</view>
</view>
</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";
@ -73,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 {
//
@ -81,15 +72,15 @@ export default {
id: undefined,
feedingPipeline: undefined,
feedingType: undefined,
weight: 0,
weight: undefined,
remark: undefined,
feedingRecordDetails: []
productList: []
},
//
customRules: {
feedingPipeline: {rules: [{required: true, errorMessage: '姓名不能为空'}]},
feedingType: {rules: [{required: true, errorMessage: '工位不能为空'}]},
feedingRecordDetails: {rules: [
productList: {rules: [
{format: 'array', errorMessage: '产品列表格式错误'},
{validateFunction: function(rule, value, data, callback) {
if (value.length < 2) {
@ -119,10 +110,10 @@ export default {
submit(ref) {
this.$refs[ref].validate().then(res => {
var ok = 1;
if(this.formData.feedingRecordDetails && this.formData.feedingRecordDetails.length > 0) {
for (let i = 0; i < this.formData.feedingRecordDetails.length; i++) {
if(!this.formData.feedingRecordDetails[i].itemId || this.formData.feedingRecordDetails[i].itemId===''
|| !this.formData.feedingRecordDetails[i].weight|| this.formData.feedingRecordDetails[i].weight <= 0) {
if(this.formData.productList && this.formData.productList.length > 0) {
for (let i = 0; i < this.formData.productList.length; i++) {
if(!this.formData.productList[i].itemId || this.formData.productList[i].itemId===''
|| !this.formData.productList[i].weight|| this.formData.productList[i].weight <= 0) {
ok = 0;
uni.showToast({
title: `原料信息不能为空!`,
@ -138,17 +129,18 @@ export default {
create(this.formData).then(response => {
modal.msgSuccess("保存成功")
tab.navigateBack()
uni.$emit('saveDraft', true)
})
}
})
}
}).catch(err => {})
}).catch(err => {
console.log('err', err);
})
},
handleTypeChange(e){
this.formData.feedingType =e.detail.value;
if(this.formData.feedingType === 'wet' || this.formData.feedingType==='dry'){
this.formData.feedingRecordDetails = []
this.formData.productList = []
}
},
getItemList() {
@ -160,7 +152,7 @@ export default {
})
},
add() {
this.formData.feedingRecordDetails.push({
this.formData.productList.push({
id: Date.now(),
itemId: undefined,
unitId: 3,
@ -168,21 +160,15 @@ export default {
})
},
del(id) {
let index = this.formData.feedingRecordDetails.findIndex(v => v.id === id)
this.formData.feedingRecordDetails.splice(index, 1)
let index = this.formData.productList.findIndex(v => v.id === id)
this.formData.productList.splice(index, 1)
}
}
}
</script>
<style lang="scss" scoped>
.sticky {
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%);
backdrop-filter: blur(27.18px);
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1),
0 0.5px 0 0 rgba(0, 0, 0, 0.1);
}
.container {
<style lang="scss">
.example {
padding: 15px;
background-color: #fff;
}
@ -201,21 +187,7 @@ export default {
margin-left: 10px;
}
.uni-group-title{
height: 55px;
padding: 10px;
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%);
padding-top: 5px;
background-color: #f4c7c7;
}
.uni-group--card {
margin :0
}
.uniui-plus:before{
color: #ffffff
}
.uniui-checkbox:before{
color: #ffffff
}
.u-button {
height: 35px;
width: auto;
}
</style>

@ -1,326 +0,0 @@
<template>
<view class="container">
<uni-card :is-shadow="false" is-full>
<text class="uni-h6">如个人无法填报请寻找主管代为报工</text>
</uni-card>
<view class="example">
<!-- 自定义表单校验 -->
<uni-forms ref="customForm" :rules="customRules" labelWidth="60px" :modelValue="customFormData">
<uni-forms-item label="工序" required name="orgType">
<uni-data-checkbox disabled v-model="customFormData.orgType" :localdata="processTypes()" @change="handleOrgTypeChange" />
</uni-forms-item>
<uni-forms-item label="班别" required name="groupType">
<uni-data-checkbox disabled v-model="customFormData.groupType" :localdata="groupTypes()" />
</uni-forms-item>
<uni-forms-item label="日期" required name="reportDateString">
<uni-datetime-picker disabled v-model="customFormData.reportDateString" type="date" :clear-icon="true" @change="maskClick" />
</uni-forms-item>
<uni-forms-item label="工位" required name="orgId">
<uni-data-select disabled v-model="customFormData.orgId" :localdata="orgIdList"></uni-data-select>
</uni-forms-item>
<uni-forms-item label="计时时段" name="reportTime">
<uni-easyinput disabled type="text" v-model="customFormData.reportTime" placeholder="如:8:00-12:00" />
</uni-forms-item>
<uni-forms-item label="总计时/h" required name="totalTime">
<uni-easyinput disabled type="number" v-model="customFormData.totalTime" placeholder="" />
</uni-forms-item>
<uni-group title="card 模式" mode="card" v-for="(item,index) in customFormData.produceReportDetails" :key="item.id"
:name="['produceReportDetails',index,'value']">
<template v-slot:title>
<view class="uni-group-title">
<uni-row>
<uni-col :span="20">
<uni-forms-item label="产品" required name="planId">
<uni-data-select disabled v-model="customFormData.produceReportDetails[index].planId" :localdata="planProductList"></uni-data-select>
</uni-forms-item>
</uni-col>
<uni-col :span="4" align="center">
<!-- <button class="button" size="mini" type="warn" @click="del(item.id)"></button>-->
<!-- <uni-icons type="trash" size="25" color="red" @click="del(item.id)"></uni-icons>-->
</uni-col>
</uni-row>
</view>
</template>
<uni-row>
<uni-col :span="12">
<uni-forms-item label="成品数" required name="qualityNumber">
<uni-easyinput disabled type="number" v-model="customFormData.produceReportDetails[index].qualityNumber" @change="changeNumber(item.id)"/>
</uni-forms-item>
</uni-col>
<uni-col :span="12">
<uni-forms-item label="废品数" name="wasteNumber">
<uni-easyinput disabled type="number" v-model="customFormData.produceReportDetails[index].wasteNumber" @change="changeNumber(item.id)"/>
</uni-forms-item>
</uni-col>
</uni-row>
<uni-row>
<uni-col :span="12">
<uni-forms-item label="总数" name="totalNumber">
<uni-easyinput disabled v-model="customFormData.produceReportDetails[index].totalNumber" placeholder="" />
</uni-forms-item>
</uni-col>
<uni-col :span="12">
<uni-forms-item label="成品率%" required name="qualityRate">
<uni-easyinput disabled type="number" v-model="customFormData.produceReportDetails[index].qualityRate" />
</uni-forms-item>
</uni-col>
</uni-row>
<uni-row>
<uni-col :span="12">
<uni-forms-item label="打包数" name="packageNumber">
<uni-easyinput disabled type="number" v-model="customFormData.produceReportDetails[index].packageNumber" />
</uni-forms-item>
</uni-col>
<uni-col :span="12">
<uni-forms-item label="备注" name="remark">
<uni-easyinput disabled type="text" v-model="customFormData.produceReportDetails[index].remark" />
</uni-forms-item>
</uni-col>
</uni-row>
</uni-group>
</uni-forms>
<view class="button-group">
<!-- <button type="primary" size="mini" @click="add">-->
<!-- <uni-icons type="plus" size="15"></uni-icons>-->
<!-- 新增产品-->
<!-- </button>-->
<button type="primary" size="mini" @click="handleBack"></button>
</view>
</view>
</view>
</template>
<script>
import useUserStore from "@/store/modules/user";
import {processTypes, groupTypes} from "@/api/system/dict/data";
import {getOrgList, getProductList, createReport, getPlanProductList} from "@/api/mes/organization"
import {getCurrentDate, timestampToTime} from "@/utils/dateUtil"
import {showConfirm} from "@/utils/common";
import tab from "@/plugins/tab";
import modal from "@/plugins/modal";
import {getById, getDetailByReportId, update} from "@/api/mes/report";
export default {
components: {},
data() {
return {
//
customFormData: {
id: undefined,
name: this.getUserInfo().name,
userId: this.getUserInfo().userId,
orgId: undefined,
orgType: undefined,
reportTime: undefined,
totalTime: 0,
reportStatus: undefined,
remark: undefined,
groupType: undefined,
reportType: undefined,
reportDateString: getCurrentDate(),
reportDate: undefined,
produceReportDetails: []
},
//
customRules: {
userId: {rules: [{required: true, errorMessage: '姓名不能为空'}]},
orgId: {rules: [{required: true, errorMessage: '工位不能为空'}]},
orgType: {rules: [{required: true, errorMessage: '工序不能为空'}]},
reportDateString: {rules: [{required: true, errorMessage: '报工日期不能为空'}]},
totalTime: {rules: [{required: true, errorMessage: '总计时不能为空'},
{format: 'number', errorMessage: '总计时格式错误,应填计时小时数'}]},
groupType: {rules: [{required: true, errorMessage: '班别不能为空'}]},
produceReportDetails: {rules: [
{format: 'array', errorMessage: '产品列表格式错误'},
{validateFunction: function(rule, value, data, callback) {
if (value.length < 2) {
callback('产品信息不能为空')
}
return true
}
}]
}
},
orgIdList:[],
planProductList:[],
updateId: undefined
}
},
computed: {
},
onLoad() {
this.getOrgIdList()
// this.getPlanProductList()
this.updateId = tab.getParams()
if(this.updateId){
this.disableEnable = true
//
getById(this.updateId).then(response => {
this.customFormData = response.data
this.customFormData.reportDateString = timestampToTime(this.customFormData.reportDate)
getDetailByReportId(this.updateId).then(res => {
this.customFormData.produceReportDetails = res.data
})
this.getPlanProductList()
})
}
},
onReady() {
//
this.$refs.customForm.setRules(this.customRules)
},
methods: {
getUserInfo() {
return useUserStore()
},
processTypes() {
return processTypes
},
groupTypes() {
return groupTypes
},
submit(ref) {
if(this.customFormData.reportDateString.length<11)this.customFormData.reportDateString+=' 00:00:00';
this.customFormData.reportType = '个人';
console.log(this.customFormData);
this.$refs[ref].validate().then(res => {
var ok = 1;
if(this.customFormData.produceReportDetails && this.customFormData.produceReportDetails.length > 0) {
for (let i = 0; i < this.customFormData.produceReportDetails.length; i++) {
if(!this.customFormData.produceReportDetails[i].planId || this.customFormData.produceReportDetails[i].planId===''
|| !this.customFormData.produceReportDetails[i].qualityNumber|| this.customFormData.produceReportDetails[i].qualityNumber <= 0) {
ok = 0;
uni.showToast({
title: `产品信息不能为空!`,
icon: 'error'
})
break;
}
}
}
if(ok===1){
showConfirm("确认保存生产报工单吗?").then(res => {
if (res.confirm) {
if(this.updateId){
this.customFormData.id = this.updateId
update(this.customFormData).then(response => {
modal.msgSuccess("修改成功")
tab.navigateBack()
})
}else {
this.customFormData.id = null
createReport(this.customFormData).then(response => {
modal.msgSuccess("保存成功")
tab.switchTab('/pages/report')
})
}
}
})
}
}).catch(err => {
console.log('err', err);
})
},
maskClick(e){
this.customFormData.reportDateString =e
this.getOrgIdList()
},
getOrgIdList() {
let data = {
'orgType': this.customFormData.orgType,
'reportDate': this.customFormData.reportDateString+" 00:00:00",
'groupType': this.customFormData.groupType
}
getOrgList(data).then(response => {
this.orgIdList = response.data;
if(!this.orgIdList || this.orgIdList.length===0){
uni.showToast({
title: '日期:'+this.customFormData.reportDateString+`未查询到工位安排!`,
icon: 'error'
})
}
else if(this.orgIdList.length===1){
this.customFormData.orgId = this.orgIdList[0].value
}
})
},
getPlanProductList() {
if(this.customFormData.orgType)
getPlanProductList(this.customFormData.orgType).then(response => {
this.planProductList = response.data;
if(!this.planProductList || this.orgIdList.planProductList===0){}
})
},
add() {
if(this.customFormData.orgType){
this.customFormData.produceReportDetails.push({
id: Date.now(),
productId: undefined,
planId: undefined,
qualityNumber: 0,
wasteNumber: 0,
totalNumber: 0,
qualityRate:0,
packageNumber: 0,
remark: ''
})
}else{
uni.showToast({
title: `请先选择工序!`,
icon: 'error'
})
}
},
del(id) {
let index = this.customFormData.produceReportDetails.findIndex(v => v.id === id)
this.customFormData.produceReportDetails.splice(index, 1)
},
changeNumber(id){
let index = this.customFormData.produceReportDetails.findIndex(v => v.id === id)
if(!this.customFormData.produceReportDetails[index].qualityNumber)
this.customFormData.produceReportDetails[index].qualityNumber=0;
if(!this.customFormData.produceReportDetails[index].wasteNumber)
this.customFormData.produceReportDetails[index].wasteNumber=0;
this.customFormData.produceReportDetails[index].totalNumber =
Number(this.customFormData.produceReportDetails[index].qualityNumber) + Number(this.customFormData.produceReportDetails[index].wasteNumber);
this.customFormData.produceReportDetails[index].qualityRate =
(Number(this.customFormData.produceReportDetails[index].qualityNumber)*100/Number(this.customFormData.produceReportDetails[index].totalNumber)).toFixed(2);
},
handleOrgTypeChange(e){
console.log(e)
this.getPlanProductList()
},
handleBack(){
tab.navigateBack()
}
}
}
</script>
<style lang="scss">
.example {
padding: 15px;
background-color: #fff;
}
.button-group {
margin-top: 15px;
display: flex;
justify-content: space-around;
}
.button {
display: flex;
align-items: center;
height: 35px;
line-height: 35px;
margin-left: 10px;
}
.uni-group-title{
padding-top: 5px;
background-color: #f4c7c7;
}
</style>

@ -1,72 +1,60 @@
<template>
<view class="container">
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
title="计划进度"
bg-color="transparent"
:title-style="{ fontWeight: 'bold' }"
:auto-back="true"
safe-area-inset-top
placeholder
/>
</u-sticky>
<NavBar title="计划进度" />
<uni-card :is-shadow="false" is-full>
<view class="u-flex justify-between">
<view class="u-flex">
<u-image
src="@/static/images/icon/product.png"
width="40rpx"
height="40rpx"
<uni-row>
<uni-col :span="10">
<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">产品:
<text class="highlight warning">{{ (planDo && planDo.productName) ? planDo.productName : '-' }}</text>
</text>
</uni-col>
</uni-row>
</uni-card>
<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"
/>
<u-text type="primary" class="u-m-l-10" :text="planDo.productName"></u-text>
<text class="progress-text">{{ calcQualityPercent(row).toFixed(2) }}%</text>
</view>
<view><u-text type="success" :text="planDo.code"></u-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>
</uni-card>
<uni-table ref="table" stripe emptyText="暂无数据">
<uni-tr>
<uni-th width="60" align="center">工序</uni-th>
<uni-th width="60" align="center">数量</uni-th>
<uni-th width="80" align="center">进度</uni-th>
<uni-th width="60" align="center">废品</uni-th>
<uni-th width="60" align="center">废品率</uni-th>
</uni-tr>
<uni-tr v-for="(item, index) in progressList" :key="index">
<uni-td align="center"><text> {{ findTextByValue(item.orgType )}}</text></uni-td>
<uni-td align="center">
<view class="name">{{ item.totalQualityNumber }}</view>
</uni-td>
<uni-td align="center">
<u-line-progress
:percentage="Number((item.totalQualityNumber / planDo.planNumber) * 100).toFixed(2)"
active-color="#50E0C1"
inactive-color="rgba(139, 237, 215, 0.47)"
>
<text class="u-percentage-slot">{{Number((item.totalQualityNumber / planDo.planNumber) * 100).toFixed(2)}}%</text>
</u-line-progress>
</uni-td>
<uni-td align="center">
<view class="name">{{ item.totalWasteNumber }}</view>
</uni-td>
<uni-td align="center">
<view class="name">{{ Number((item.totalWasteNumber/(item.totalQualityNumber + item.totalWasteNumber))*100 ).toFixed(2)}}%</view>
</uni-td>
</uni-tr>
</uni-table>
<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} from "@/api/mes/plan"
import { processTypes, findTextByValue } from "@/api/system/dict/data";
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,
@ -86,33 +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;
})
modal.closeLoading()
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" scoped>
.sticky {
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%);
backdrop-filter: blur(27.18px);
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1),
0 0.5px 0 0 rgba(0, 0, 0, 0.1);
<style lang="scss">
.container {
padding: 24rpx;
}
.highlight {
margin-left: 8rpx;
font-weight: 600;
}
.u-percentage-slot {
font-size: 10px;
margin-left: 40px;
.primary {
color: #1a73e8;
}
.warning {
color: #f59e0b;
}
.progress-table {
background: #ffffff;
border-radius: 16rpx;
overflow: hidden;
}
:deep(.u-line-progress__line) {
.progress-row {
display: flex;
align-items: center;
justify-content: 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,115 +1,102 @@
<template>
<view>
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
title="代报工"
bg-color="transparent"
:title-style="{ fontWeight: 'bold' }"
:auto-back="true"
safe-area-inset-top
placeholder
/>
</u-sticky>
<view class="container">
<view class="container">
<NavBar title="代报工" />
<view class="example">
<!-- 自定义表单校验 -->
<uni-forms ref="customForm" :rules="customRules" labelWidth="60px" :modelValue="customFormData">
<uni-forms-item required label="工人" name="userId">
<uni-data-select v-model="customFormData.userId" :localdata="userList" @change="handleUserChange" :disabled="disableEnable">
</uni-data-select>
</uni-forms-item>
<uni-forms-item label="工序" required name="orgType" label-width="75px">
<uni-forms-item label="工序" required name="orgType">
<uni-data-checkbox v-model="customFormData.orgType" :localdata="processTypes()" :disabled="disableEnable" @change="handleOrgTypeChange"/>
</uni-forms-item>
<uni-forms-item label="班别" required name="groupType" label-width="75px">
<uni-forms-item label="班别" required name="groupType">
<uni-data-checkbox v-model="customFormData.groupType" :localdata="groupTypes()" :disabled="disableEnable"/>
</uni-forms-item>
<uni-forms-item label="日期" required name="reportDateString" label-width="75px">
<uni-forms-item label="日期" required name="reportDateString">
<uni-datetime-picker v-model="customFormData.reportDateString" type="date" :clear-icon="true" @change="maskClick" :disabled="disableEnable"/>
</uni-forms-item>
<uni-forms-item v-if="customFormData.userId && customFormData.reportDateString" label="工位" required name="orgId">
<uni-data-select v-model="customFormData.orgId" :localdata="orgIdList" :disabled="disableEnable" ></uni-data-select>
</uni-forms-item>
<uni-forms-item label="计时段" name="reportTime" label-width="75px">
<uni-forms-item label="计时段" name="reportTime">
<uni-easyinput type="text" v-model="customFormData.reportTime" placeholder="如:8:00-12:00" />
</uni-forms-item>
<uni-forms-item label="计时/h" required name="totalTime" label-width="75px">
<uni-forms-item label="/h" required name="totalTime">
<uni-easyinput type="number" v-model="customFormData.totalTime" placeholder="" />
</uni-forms-item>
<uni-group title="card 模式" mode="card" v-for="(item,index) in customFormData.produceReportDetails" :key="item.id"
:name="['produceReportDetails',index,'value']">
<uni-group title="card 模式" mode="card" v-for="(item,index) in customFormData.productList" :key="item.id"
:name="['productList',index,'value']">
<template v-slot:title>
<view class="uni-group-title">
<uni-row>
<uni-col :span="22">
<uni-forms-item label="产品" required name="planId" label-width="50px">
<uni-data-select v-model="customFormData.produceReportDetails[index].planId" :localdata="planProductList" @change="handleProductChange"></uni-data-select>
<uni-col :span="20">
<uni-forms-item label="产品" required name="planId">
<uni-data-select v-model="customFormData.productList[index].planId" :localdata="planProductList" @change="handleProductChange"></uni-data-select>
</uni-forms-item>
</uni-col>
<uni-col :span="2" class="u-p-t-10">
<uni-col :span="4" align="center">
<uni-icons type="trash" size="25" color="red" @click="del(item.id)"></uni-icons>
</uni-col>
</uni-row>
</view>
</template>
<uni-row>
<uni-col :span="12" class="u-p-r-20">
<uni-forms-item label="成品数" required name="qualityNumber" label-width="72px">
<uni-easyinput type="number" v-model="customFormData.produceReportDetails[index].qualityNumber" @change="changeNumber(item.id)"/>
<uni-col :span="12">
<uni-forms-item label="成品数" required name="qualityNumber">
<uni-easyinput type="number" v-model="customFormData.productList[index].qualityNumber" @change="changeNumber(item.id)"/>
</uni-forms-item>
</uni-col>
<uni-col :span="12">
<uni-forms-item label="废品数" name="wasteNumber" label-width="72px">
<uni-easyinput type="number" v-model="customFormData.produceReportDetails[index].wasteNumber" @change="changeNumber(item.id)"/>
<uni-forms-item label="废品数" name="wasteNumber">
<uni-easyinput type="number" v-model="customFormData.productList[index].wasteNumber" @change="changeNumber(item.id)"/>
</uni-forms-item>
</uni-col>
</uni-row>
<uni-row>
<uni-col :span="12" class="u-p-r-20">
<uni-forms-item label="总数" name="totalNumber" label-width="72px">
<uni-easyinput disabled v-model="customFormData.produceReportDetails[index].totalNumber" placeholder="" />
<uni-col :span="12">
<uni-forms-item label="总数" name="totalNumber">
<uni-easyinput disabled v-model="customFormData.productList[index].totalNumber" placeholder="" />
</uni-forms-item>
</uni-col>
<uni-col :span="12">
<uni-forms-item label="成品率%" required name="qualityRate" label-width="72px">
<uni-easyinput disabled type="number" v-model="customFormData.produceReportDetails[index].qualityRate" />
<uni-forms-item label="成品率%" required name="qualityRate">
<uni-easyinput disabled type="number" v-model="customFormData.productList[index].qualityRate" />
</uni-forms-item>
</uni-col>
</uni-row>
<uni-row>
<uni-col :span="12" class="u-p-r-20">
<uni-forms-item label="打包数" name="packageNumber" label-width="72px">
<uni-easyinput type="number" v-model="customFormData.produceReportDetails[index].packageNumber" />
<uni-col :span="12">
<uni-forms-item label="打包数" name="packageNumber">
<uni-easyinput type="number" v-model="customFormData.productList[index].packageNumber" />
</uni-forms-item>
</uni-col>
<uni-col :span="12">
<uni-forms-item label="备注" name="remark" label-width="72px">
<uni-easyinput type="text" v-model="customFormData.produceReportDetails[index].remark" />
<uni-forms-item label="备注" name="remark">
<uni-easyinput type="text" v-model="customFormData.productList[index].remark" />
</uni-forms-item>
</uni-col>
</uni-row>
</uni-group>
</uni-forms>
<view class="u-flex justify-end">
<view class="u-flex u-m-t-30">
<u-button type="primary" @click="add" class="u-m-r-30">
<uni-icons type="plus" class="u-m-r-10"/>新增产品
</u-button>
<u-button type="success" @click="submit('customForm')">
<uni-icons type="checkbox" class="u-m-r-10"/>保存
</u-button>
</view>
<view class="button-group">
<button type="primary" size="mini" @click="add">
<uni-icons type="plus" size="15"></uni-icons>
新增产品
</button>
<button type="primary" size="mini" @click="submit('customForm')"></button>
</view>
</view>
</view>
</template>
<script>
import NavBar from '@/components/common/NavBar.vue'
import {processTypes, groupTypes} from "@/api/system/dict/data";
import {
getProductList,
createReport,
getOtherPersonalUser,
getOtherOrgList,
@ -121,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 {
//
@ -139,7 +128,7 @@ export default {
reportType: undefined,
reportDateString: getCurrentDate(),
reportDate: undefined,
produceReportDetails: []
productList: []
},
//
customRules: {
@ -150,9 +139,11 @@ export default {
totalTime: {rules: [{required: true, errorMessage: '计件时间不能为空'},
{format: 'number', errorMessage: '计件时间格式错误,应填计件小时数'}]},
groupType: {rules: [{required: true, errorMessage: '班别不能为空'}]},
produceReportDetails: {rules: [
productList: {rules: [
{format: 'array', errorMessage: '产品列表格式错误'},
{validateFunction: function(rule, value, data, callback) {
console.log('value:'+value)
console.log('data:'+data)
if (value.length < 2) {
callback('产品信息不能为空')
}
@ -173,22 +164,18 @@ export default {
onLoad() {
this.getUserList()
this.updateId = tab.getParams()
console.log(this.updateId)
if(this.updateId){
this.disableEnable = true
//
getById(this.updateId).then(response => {
this.customFormData = response.data
this.customFormData.reportDateString = timestampToTime(this.customFormData.reportDate)
//
this.getOrgIdList()
//
this.getPlanProductList()
// this.getOrgIdList()
getDetailByReportId(this.updateId).then(res => {
this.customFormData.produceReportDetails = res.data
this.customFormData.productList = res.data
})
})
}
},
onReady() {
@ -201,12 +188,13 @@ export default {
submit(ref) {
if(this.customFormData.reportDateString.length<11)this.customFormData.reportDateString+=' 00:00:00';
this.customFormData.reportType = '代报工';
console.log(this.customFormData);
this.$refs[ref].validate().then(res => {
var ok = 1;
if(this.customFormData.produceReportDetails && this.customFormData.produceReportDetails.length > 0) {
for (let i = 0; i < this.customFormData.produceReportDetails.length; i++) {
if(!this.customFormData.produceReportDetails[i].planId || this.customFormData.produceReportDetails[i].planId===''
|| !this.customFormData.produceReportDetails[i].qualityNumber|| this.customFormData.produceReportDetails[i].qualityNumber <= 0) {
if(this.customFormData.productList && this.customFormData.productList.length > 0) {
for (let i = 0; i < this.customFormData.productList.length; i++) {
if(!this.customFormData.productList[i].planId || this.customFormData.productList[i].planId===''
|| !this.customFormData.productList[i].qualityNumber|| this.customFormData.productList[i].qualityNumber <= 0) {
ok = 0;
uni.showToast({
title: `产品信息不能为空!`,
@ -224,22 +212,19 @@ export default {
update(this.customFormData).then(response => {
modal.msgSuccess("修改成功")
tab.navigateBack()
uni.$emit('success', true)
})
}else{
this.customFormData.id = null
createReport(this.customFormData).then(response => {
modal.msgSuccess("保存成功")
tab.navigateBack()
//
uni.$emit('success', true)
})
}
}
})
}
}).catch(err => {
console.log('err', err);
})
},
maskClick(e){
@ -248,6 +233,7 @@ export default {
this.getOrgIdList()
},
getOrgIdList() {
this.customFormData.orgId = undefined
let dateStr = this.customFormData.reportDateString.length < 11 ?
this.customFormData.reportDateString+" 00:00:00": this.customFormData.reportDateString;
let data = {
@ -264,7 +250,6 @@ export default {
icon: 'error'
})
}
if(this.updateId){}
else if(this.orgIdList.length===1){
this.customFormData.orgId = this.orgIdList[0].value
}
@ -284,7 +269,7 @@ export default {
},
add() {
if(this.customFormData.orgType){
this.customFormData.produceReportDetails.push({
this.customFormData.productList.push({
id: Date.now(),
productId: undefined,
planId: undefined,
@ -303,20 +288,20 @@ export default {
}
},
del(id) {
let index = this.customFormData.produceReportDetails.findIndex(v => v.id === id)
this.customFormData.produceReportDetails.splice(index, 1)
let index = this.customFormData.productList.findIndex(v => v.id === id)
this.customFormData.productList.splice(index, 1)
},
changeNumber(id){
let index = this.customFormData.produceReportDetails.findIndex(v => v.id === id)
if(!this.customFormData.produceReportDetails[index].qualityNumber)
this.customFormData.produceReportDetails[index].qualityNumber=0;
if(!this.customFormData.produceReportDetails[index].wasteNumber)
this.customFormData.produceReportDetails[index].wasteNumber=0;
let index = this.customFormData.productList.findIndex(v => v.id === id)
if(!this.customFormData.productList[index].qualityNumber)
this.customFormData.productList[index].qualityNumber=0;
if(!this.customFormData.productList[index].wasteNumber)
this.customFormData.productList[index].wasteNumber=0;
this.customFormData.produceReportDetails[index].totalNumber =
Number(this.customFormData.produceReportDetails[index].qualityNumber) + Number(this.customFormData.produceReportDetails[index].wasteNumber);
this.customFormData.produceReportDetails[index].qualityRate =
(Number(this.customFormData.produceReportDetails[index].qualityNumber)*100/Number(this.customFormData.produceReportDetails[index].totalNumber)).toFixed(2);
this.customFormData.productList[index].totalNumber =
Number(this.customFormData.productList[index].qualityNumber) + Number(this.customFormData.productList[index].wasteNumber);
this.customFormData.productList[index].qualityRate =
(Number(this.customFormData.productList[index].qualityNumber)*100/Number(this.customFormData.productList[index].totalNumber)).toFixed(2);
},
handleUserChange(e){
this.customFormData.userId = e
@ -326,39 +311,33 @@ export default {
this.getPlanProductList()
},
handleProductChange(e){
console.log(e)
}
}
}
</script>
<style lang="scss" scoped>
.sticky {
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%);
backdrop-filter: blur(27.18px);
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1),
0 0.5px 0 0 rgba(0, 0, 0, 0.1);
}
.container {
padding: 10px;
background-color: #fff;
}
<style lang="scss">
.example {
padding: 15px;
background-color: #fff;
}
.uniui-plus:before{
color: #ffffff
}
.uniui-checkbox:before{
color: #ffffff
}
.u-button {
height: 35px;
width: auto;
}
.uni-group-title{
height: 55px;
padding: 10px;
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%);
}
.uni-group--card {
margin :0
}
.button-group {
margin-top: 15px;
display: flex;
justify-content: space-around;
}
.button {
display: flex;
align-items: center;
height: 35px;
line-height: 35px;
margin-left: 10px;
}
.uni-group-title{
padding-top: 5px;
background-color: #f4c7c7;
}
</style>

@ -1,53 +1,41 @@
<template>
<view>
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
title="生产报工"
bg-color="transparent"
:title-style="{ fontWeight: 'bold' }"
:auto-back="true"
safe-area-inset-top
placeholder
/>
</u-sticky>
<view class="container">
<NavBar title="生产报工" />
<uni-card :is-shadow="false" is-full>
<u-text type="warning" class="uni-h6" text="如个人无法填报,请寻找主管代为报工"></u-text>
<text class="uni-h6">如个人无法填报请寻找主管代为报工</text>
</uni-card>
<view class="container">
<view class="example">
<!-- 自定义表单校验 -->
<uni-forms ref="customForm" :rules="customRules" label-width="75px" :modelValue="customFormData">
<uni-forms ref="customForm" :rules="customRules" labelWidth="60px" :modelValue="customFormData">
<uni-forms-item label="工序" required name="orgType">
<uni-data-checkbox v-model="customFormData.orgType" :localdata="processTypes()" @change="handleOrgTypeChange" />
</uni-forms-item>
<uni-forms-item label="班别" required name="groupType" label-width="75px">
<uni-forms-item label="班别" required name="groupType">
<uni-data-checkbox v-model="customFormData.groupType" :localdata="groupTypes()" />
</uni-forms-item>
<uni-forms-item label="日期" required name="reportDateString" label-width="75px">
<uni-forms-item label="日期" required name="reportDateString">
<uni-datetime-picker v-model="customFormData.reportDateString" type="date" :clear-icon="true" @change="maskClick" />
</uni-forms-item>
<uni-forms-item label="工位" required name="orgId" label-width="75px">
<uni-forms-item label="工位" required name="orgId">
<uni-data-select v-model="customFormData.orgId" :localdata="orgIdList"></uni-data-select>
</uni-forms-item>
<uni-forms-item label="计时段" name="reportTime" label-width="75px">
<uni-forms-item label="计时段" name="reportTime">
<uni-easyinput type="text" v-model="customFormData.reportTime" placeholder="如:8:00-12:00" />
</uni-forms-item>
<uni-forms-item label="计时/h" required name="totalTime" label-width="75px">
<uni-forms-item label="/h" required name="totalTime">
<uni-easyinput type="number" v-model="customFormData.totalTime" placeholder="" />
</uni-forms-item>
<uni-group title="card 模式" mode="card" v-for="(item,index) in customFormData.produceReportDetails" :key="item.id"
:name="['produceReportDetails',index,'value']">
<uni-group title="card 模式" mode="card" v-for="(item,index) in customFormData.productList" :key="item.id"
:name="['productList',index,'value']">
<template v-slot:title>
<view class="uni-group-title">
<uni-row>
<uni-col :span="22">
<uni-forms-item label="产品" required name="planId" label-width="50px">
<uni-data-select v-model="customFormData.produceReportDetails[index].planId" :localdata="planProductList"></uni-data-select>
<uni-col :span="12">
<uni-forms-item label="产品" required name="planId">
<uni-data-select v-model="customFormData.productList[index].planId" :localdata="planProductList"></uni-data-select>
</uni-forms-item>
</uni-col>
<uni-col :span="2" class="u-p-t-10">
<uni-col :span="12" align="center">
<!-- <button class="button" size="mini" type="warn" @click="del(item.id)"></button>-->
<uni-icons type="trash" size="25" color="red" @click="del(item.id)"></uni-icons>
</uni-col>
@ -55,77 +43,85 @@
</view>
</template>
<uni-row>
<uni-col :span="12" class="u-p-r-30">
<uni-col :span="12">
<uni-forms-item label="成品数" required name="qualityNumber">
<uni-easyinput type="number" v-model="customFormData.produceReportDetails[index].qualityNumber" @change="changeNumber(item.id)"/>
<uni-easyinput type="number" v-model="customFormData.productList[index].qualityNumber" @change="changeNumber(item.id)"/>
</uni-forms-item>
</uni-col>
<uni-col :span="12">
<uni-forms-item label="废品数" name="wasteNumber">
<uni-easyinput type="number" v-model="customFormData.produceReportDetails[index].wasteNumber" @change="changeNumber(item.id)"/>
<uni-easyinput type="number" v-model="customFormData.productList[index].wasteNumber" @change="changeNumber(item.id)"/>
</uni-forms-item>
</uni-col>
</uni-row>
<uni-row>
<uni-col :span="12" class="u-p-r-30">
<uni-col :span="12">
<uni-forms-item label="总数" name="totalNumber">
<uni-easyinput disabled v-model="customFormData.produceReportDetails[index].totalNumber" placeholder="" />
<uni-easyinput disabled v-model="customFormData.productList[index].totalNumber" placeholder="" />
</uni-forms-item>
</uni-col>
<uni-col :span="12">
<uni-forms-item label="成品率%" required name="qualityRate">
<uni-easyinput disabled type="number" v-model="customFormData.produceReportDetails[index].qualityRate" />
<uni-easyinput disabled type="number" v-model="customFormData.productList[index].qualityRate" />
</uni-forms-item>
</uni-col>
</uni-row>
<uni-row>
<uni-col :span="12" class="u-p-r-30">
<uni-col :span="12">
<uni-forms-item label="打包数" name="packageNumber">
<uni-easyinput type="number" v-model="customFormData.produceReportDetails[index].packageNumber" />
<uni-easyinput type="number" v-model="customFormData.productList[index].packageNumber" />
</uni-forms-item>
</uni-col>
<uni-col :span="12">
<uni-forms-item label="备注" name="remark">
<uni-easyinput type="text" v-model="customFormData.produceReportDetails[index].remark" />
<uni-easyinput type="text" v-model="customFormData.productList[index].remark" />
</uni-forms-item>
</uni-col>
</uni-row>
</uni-group>
</uni-forms>
<view class="u-flex justify-end">
<view class="u-flex u-m-t-30">
<u-button type="primary" @click="add" class="u-m-r-30">
<uni-icons type="plus" class="u-m-r-10"/>新增产品
</u-button>
<u-button type="success" @click="submit('customForm')">
<uni-icons type="checkbox" class="u-m-r-10"/>保存
</u-button>
</view>
<view class="button-group">
<button type="primary" size="mini" @click="add">
<uni-icons type="plus" size="15"></uni-icons>
新增产品
</button>
<button type="primary" size="mini" @click="submit('customForm')"></button>
</view>
</view>
</view>
</template>
<script>
import NavBar from '@/components/common/NavBar.vue'
import useUserStore from "@/store/modules/user";
import { processTypes, groupTypes } from "@/api/system/dict/data";
import { getOrgList, createReport, getPlanProductList} from "@/api/mes/organization"
import { getCurrentDate, timestampToTime } from "@/utils/dateUtil"
import { showConfirm } from "@/utils/common";
import {processTypes, groupTypes} from "@/api/system/dict/data";
import {getOrgList, getProductList, createReport, getPlanProductList} from "@/api/mes/organization"
import {getCurrentDate} from "@/utils/dateUtil"
import {showConfirm} from "@/utils/common";
import tab from "@/plugins/tab";
import modal from "@/plugins/modal";
import { getById, getDetailByReportId, update } from "@/api/mes/report";
export default {
components: {},
components: { NavBar },
data() {
let userStore = {
name: '',
userId: ''
}
try {
userStore = useUserStore()
} catch (e) {
userStore = {
name: '',
userId: ''
}
}
return {
//
customFormData: {
id: undefined,
name: this.getUserInfo().name,
userId: this.getUserInfo().userId,
name: userStore.name,
userId: userStore.userId,
orgId: undefined,
orgType: undefined,
reportTime: undefined,
@ -136,7 +132,7 @@ export default {
reportType: undefined,
reportDateString: getCurrentDate(),
reportDate: undefined,
produceReportDetails: []
productList: []
},
//
customRules: {
@ -144,10 +140,10 @@ export default {
orgId: {rules: [{required: true, errorMessage: '工位不能为空'}]},
orgType: {rules: [{required: true, errorMessage: '工序不能为空'}]},
reportDateString: {rules: [{required: true, errorMessage: '报工日期不能为空'}]},
totalTime: {rules: [{required: true, errorMessage: '计时不能为空'},
{format: 'number', errorMessage: '总计时格式错误,应填计时小时数'}]},
totalTime: {rules: [{required: true, errorMessage: '不能为空'},
{format: 'number', errorMessage: '计件时间格式错误,应填计件小时数'}]},
groupType: {rules: [{required: true, errorMessage: '班别不能为空'}]},
produceReportDetails: {rules: [
productList: {rules: [
{format: 'array', errorMessage: '产品列表格式错误'},
{validateFunction: function(rule, value, data, callback) {
if (value.length < 2) {
@ -159,36 +155,20 @@ export default {
}
},
orgIdList:[],
planProductList:[],
updateId: undefined
planProductList:[]
}
},
computed: {
},
onLoad() {
this.getOrgIdList()
// this.getPlanProductList()
this.updateId = tab.getParams();
if(this.updateId){
this.disableEnable = true
//
getById(this.updateId).then(response => {
this.customFormData = response.data
this.customFormData.reportDateString = timestampToTime(this.customFormData.reportDate)
getDetailByReportId(this.updateId).then(res => {
this.customFormData.produceReportDetails = res.data
})
this.getPlanProductList()
})
}
},
onReady() {
//
this.$refs.customForm.setRules(this.customRules)
},
methods: {
getUserInfo() {
return useUserStore()
},
processTypes() {
return processTypes
},
@ -198,12 +178,13 @@ export default {
submit(ref) {
if(this.customFormData.reportDateString.length<11)this.customFormData.reportDateString+=' 00:00:00';
this.customFormData.reportType = '个人';
console.log(this.customFormData);
this.$refs[ref].validate().then(res => {
var ok = 1;
if(this.customFormData.produceReportDetails && this.customFormData.produceReportDetails.length > 0) {
for (let i = 0; i < this.customFormData.produceReportDetails.length; i++) {
if(!this.customFormData.produceReportDetails[i].planId || this.customFormData.produceReportDetails[i].planId===''
|| !this.customFormData.produceReportDetails[i].qualityNumber|| this.customFormData.produceReportDetails[i].qualityNumber <= 0) {
if(this.customFormData.productList && this.customFormData.productList.length > 0) {
for (let i = 0; i < this.customFormData.productList.length; i++) {
if(!this.customFormData.productList[i].planId || this.customFormData.productList[i].planId===''
|| !this.customFormData.productList[i].qualityNumber|| this.customFormData.productList[i].qualityNumber <= 0) {
ok = 0;
uni.showToast({
title: `产品信息不能为空!`,
@ -216,26 +197,15 @@ export default {
if(ok===1){
showConfirm("确认保存生产报工单吗?").then(res => {
if (res.confirm) {
if(this.updateId){
this.customFormData.id = this.updateId
update(this.customFormData).then(response => {
modal.msgSuccess("修改成功")
tab.navigateBack()
uni.$emit('handleSuccess', true)
})
}else {
this.customFormData.id = null
createReport(this.customFormData).then(response => {
modal.msgSuccess("保存成功")
tab.switchTab('/pages/report')
//
uni.$emit('handleSuccess', true)
})
}
createReport(this.customFormData).then(response => {
modal.msgSuccess("保存成功")
tab.switchTab('/pages/report')
})
}
})
}
}).catch(err => {
console.log('err', err);
})
},
maskClick(e){
@ -270,7 +240,7 @@ export default {
},
add() {
if(this.customFormData.orgType){
this.customFormData.produceReportDetails.push({
this.customFormData.productList.push({
id: Date.now(),
productId: undefined,
planId: undefined,
@ -289,55 +259,50 @@ export default {
}
},
del(id) {
let index = this.customFormData.produceReportDetails.findIndex(v => v.id === id)
this.customFormData.produceReportDetails.splice(index, 1)
let index = this.customFormData.productList.findIndex(v => v.id === id)
this.customFormData.productList.splice(index, 1)
},
changeNumber(id){
let index = this.customFormData.produceReportDetails.findIndex(v => v.id === id)
if(!this.customFormData.produceReportDetails[index].qualityNumber)
this.customFormData.produceReportDetails[index].qualityNumber=0;
if(!this.customFormData.produceReportDetails[index].wasteNumber)
this.customFormData.produceReportDetails[index].wasteNumber=0;
let index = this.customFormData.productList.findIndex(v => v.id === id)
if(!this.customFormData.productList[index].qualityNumber)
this.customFormData.productList[index].qualityNumber=0;
if(!this.customFormData.productList[index].wasteNumber)
this.customFormData.productList[index].wasteNumber=0;
this.customFormData.produceReportDetails[index].totalNumber =
Number(this.customFormData.produceReportDetails[index].qualityNumber) + Number(this.customFormData.produceReportDetails[index].wasteNumber);
this.customFormData.produceReportDetails[index].qualityRate =
(Number(this.customFormData.produceReportDetails[index].qualityNumber)*100/Number(this.customFormData.produceReportDetails[index].totalNumber)).toFixed(2);
this.customFormData.productList[index].totalNumber =
Number(this.customFormData.productList[index].qualityNumber) + Number(this.customFormData.productList[index].wasteNumber);
this.customFormData.productList[index].qualityRate =
(Number(this.customFormData.productList[index].qualityNumber)*100/Number(this.customFormData.productList[index].totalNumber)).toFixed(2);
},
handleOrgTypeChange(e){
console.log(e)
this.getPlanProductList()
}
}
}
</script>
<style lang="scss" scoped>
.sticky {
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%);
backdrop-filter: blur(27.18px);
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1),
0 0.5px 0 0 rgba(0, 0, 0, 0.1);
}
.container {
padding: 15px;
background-color: #fff;
}
<style lang="scss">
.example {
padding: 15px;
background-color: #fff;
}
.uniui-plus:before{
color: #ffffff
}
.uniui-checkbox:before{
color: #ffffff
}
.u-button {
height: 35px;
}
.uni-group-title{
height: 55px;
padding: 10px;
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%);
}
.uni-group--card {
margin :0
}
.button-group {
margin-top: 15px;
display: flex;
justify-content: space-around;
}
.button {
display: flex;
align-items: center;
height: 35px;
line-height: 35px;
margin-left: 10px;
}
.uni-group-title{
padding-top: 5px;
background-color: #f4c7c7;
}
</style>

@ -11,106 +11,60 @@
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
"globalStyle": {
"navigationBarTitleText": "BESURE"
},
"pages": [
{
"path": "pages/login",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/application/index",
"path": "pages/index",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom",
"enablePullDownRefresh": true
"enablePullDownRefresh": false
}
},
{
"path": "pages/plan",
"path": "pages/login",
"style": {
"navigationStyle": "custom",
"enablePullDownRefresh": true
"navigationBarTitleText": "登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/work",
"style": {
"navigationBarTitleText": "管理",
"navigationStyle": "custom",
"enablePullDownRefresh": true
}
},
{
"path": "pages/report",
"style": {
"navigationStyle": "custom",
"enablePullDownRefresh": true
"enablePullDownRefresh": false
}
},
{
"path": "pages/mine",
"style": {
"navigationBarTitleText": "个人中心",
"navigationStyle": "custom"
}
},
{
"path": "pages/common/webview/index",
"style": {
"navigationBarTitleText": "浏览网页"
}
},
{
"path": "pages/common/textview/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/application/components/deskArrangement",
"style": {
"navigationBarTitleText": "浏览网页",
"navigationStyle": "custom"
}
},
{
"path": "pages/application/components/deskArrangementHandle",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/application/components/assigningWork",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/application/components/energyEquipment",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/application/components/energyEquipmentCreateOrEdit",
"path": "pages/common/textview/index",
"style": {
"navigationBarTitleText": "浏览文本",
"navigationStyle": "custom"
}
},
{
"path": "pages/application/components/energyEquipmentDetail",
"path": "pages/report",
"style": {
"navigationBarTitleText": "报表",
"navigationStyle": "custom",
"onReachBottomDistance": 50
}
},
{
"path": "pages/application/components/energyEquipmentForm",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/application/components/energyEquipmentFormReset",
"style": {
"navigationStyle": "custom"
"enablePullDownRefresh": false
}
}
],
@ -121,25 +75,21 @@
{
"path": "reportForm",
"style": {
"navigationBarTitleText": "生产报工",
"navigationStyle": "custom"
}
},
{
"path": "ViewForm",
"style": {
"navigationBarTitleText": "报工详情"
}
},
{
"path": "replaceForm",
"style": {
"navigationBarTitleText": "代报工",
"navigationStyle": "custom"
}
}
,
},
{
"path": "planProgress",
"style": {
"navigationBarTitleText": "计划进度",
"navigationStyle": "custom"
}
}
@ -151,6 +101,7 @@
{
"path": "feedingRecordForm",
"style": {
"navigationBarTitleText": "新增投料记录",
"navigationStyle": "custom"
}
}
@ -162,42 +113,49 @@
{
"path": "avatar/index",
"style": {
"navigationBarTitleText": "修改头像"
"navigationBarTitleText": "修改头像",
"navigationStyle": "custom"
}
},
{
"path": "info/index",
"style": {
"navigationBarTitleText": "个人信息",
"navigationStyle": "custom"
}
},
{
"path": "info/edit",
"style": {
"navigationBarTitleText": "编辑资料",
"navigationStyle": "custom"
}
},
{
"path": "pwd/index",
"style": {
"navigationBarTitleText": "修改密码",
"navigationStyle": "custom"
}
},
{
"path": "setting/index",
"style": {
"navigationBarTitleText": "应用设置",
"navigationStyle": "custom"
}
},
{
"path": "help/index",
"style": {
"navigationBarTitleText": "常见问题",
"navigationStyle": "custom"
}
},
{
"path": "about/index",
"style": {
"navigationBarTitleText": "关于我们",
"navigationStyle": "custom"
}
}
@ -338,49 +296,334 @@
"path": "code/index"
}
]
},
{
"root": "pages_function/pages",
"pages": [
{
"path": "mold/index",
"style": {
"navigationBarTitleText": "模具查询",
"navigationStyle": "custom"
}
},
{
"path": "mold/detail",
"style": {
"navigationBarTitleText": "模具详情",
"navigationStyle": "custom"
}
},
{
"path": "equipment/index",
"style": {
"navigationBarTitleText": "设备查询",
"navigationStyle": "custom"
}
},
{
"path": "equipment/detail",
"style": {
"navigationBarTitleText": "设备详情",
"navigationStyle": "custom"
}
},
{
"path": "spare/index",
"style": {
"navigationBarTitleText": "备件查询",
"navigationStyle": "custom"
}
},
{
"path": "spare/detail",
"style": {
"navigationBarTitleText": "备件详情",
"navigationStyle": "custom"
}
},
{
"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"
}
}
]
}
],
"tabBar": {
"color": "#000000",
"selectedColor": "#000000",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/application/index",
"iconPath": "static/images/tabbar/app.png",
"selectedIconPath": "static/images/tabbar/app_.png",
"text": "我的应用"
},
{
"pagePath": "pages/plan",
"iconPath": "static/images/tabbar/home.png",
"selectedIconPath": "static/images/tabbar/home_.png",
"text": "计划"
},
{
"pagePath": "pages/report",
"iconPath": "static/images/tabbar/work.png",
"selectedIconPath": "static/images/tabbar/work_.png",
"text": "报工"
},
{
"pagePath": "pages/work",
"iconPath": "static/images/tabbar/material.png",
"selectedIconPath": "static/images/tabbar/material_.png",
"text": "投料"
},
{
"pagePath": "pages/mine",
"iconPath": "static/images/tabbar/mine.png",
"selectedIconPath": "static/images/tabbar/mine_.png",
"text": "我的"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "BESURE",
"navigationBarBackgroundColor": "#ffffff"
}
}
"preloadRule": {}
}

@ -1,140 +0,0 @@
<template>
<view>
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
title="新增派工"
bg-color="transparent"
:auto-back="true"
:title-style="{ fontWeight: 'bold' }"
safe-area-inset-top
placeholder
/>
</u-sticky>
<view class="container">
<!-- 自定义表单校验 -->
<uni-forms ref="customForm" :rules="customRules" labelWidth="80px" :modelValue="formData">
<uni-forms-item label="工位" required name="name">
<u-input v-model="name" placholder="请输入名称" disabled/>
</uni-forms-item>
<uni-forms-item label="班别" required name="groupType">
<uni-data-checkbox v-model="formData.groupType" :localdata="groupTypes" @change="getUserList"/>
</uni-forms-item>
<uni-forms-item label="工作日期" required name="planDates">
<u-input v-model="formData.planDates" placeholder="请选择工作日期" prefixIcon="calendar" @click="show=true"></u-input>
<u-calendar :show="show" mode="multiple" @confirm="confirm" @close="show=false"></u-calendar>
</uni-forms-item>
<uni-forms-item v-if="formData.groupType" required label="工人" name="workerId">
<uni-data-select v-model="formData.workerId" :localdata="userList"/>
</uni-forms-item>
</uni-forms>
<view class="u-flex justify-end">
<view @click="submit()">
<u-button type="primary">
<uni-icons type="checkbox" class="u-m-r-10"/>
确定
</u-button></view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad, onReady } from "@dcloudio/uni-app";
import tab from "@/plugins/tab";
import modal from "@/plugins/modal";
import { groupTypes } from "@/api/system/dict/data";
import { createWorker, getUserList2 } from "@/api/mes/deskArrangement";
const formData = ref({
orgId: undefined,
groupType: undefined,
workerId: undefined,
planDates: undefined,
orgType: undefined
})
const name = ref('')
const userList = ref([])
/** 工序类型变化,可选负责人跟着变化 */
const getUserList = ()=>{
if(formData.value.orgId && formData.value.groupType) {
getUserList2({ orgId: formData.value.orgId, groupType: formData.value.groupType } ).then(response => {
userList.value = response.data
userList.value.forEach(user => {
user.text = user.nickname;
user.value = user.id;
delete user.nickname;
delete user.id;
});
})
}
}
const customForm = ref()
//
const customRules = ref({
groupType: { rules: [{ required: true, errorMessage: '班别不能为空'}]},
workerId: { rules: [{ required: true, errorMessage: '工人不能为空'}]},
planDates: { rules: [{ required: true, errorMessage: '工作日期不能为空'}]}
})
const submit = ()=>{
customForm.value.validate(async(valid)=>{
if(!valid){
//
for (let i = 0; i < formData.value.planDates.length; i++) {
const dateObj = new Date(formData.value.planDates[i]);
formData.value.planDates[i] = dateObj.toISOString().replace('T', ' ').split('.')[0];
}
await createWorker(formData.value)
modal.msgSuccess("保存成功")
await tab.navigateBack()
uni.$emit('success', true)
}
})
}
onReady(()=>{
//
customForm.value.setRules(customRules.value)
})
const show = ref(false)
const confirm = (e)=>{
formData.value.planDates = e
show.value = false
}
onLoad(() => {
name.value = tab.getParams().name
formData.value.orgId = tab.getParams().id
formData.value.orgType = tab.getParams().orgType
getUserList()
});
</script>
<style scoped lang="sass">
.sticky
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%)
backdrop-filter: blur(27.18px)
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1), 0 0.5px 0 0 rgba(0, 0, 0, 0.1)
padding-bottom: 20rpx
.container
padding: 20rpx
margin: 20rpx
background-color: #ffffff
.uniui-checkbox:before
color: #ffffff
.u-button
height: 70rpx
:deep(.u-calendar-header)
padding: 40rpx
:deep(.u-calendar-header__title)
display: block
:deep(.u-calendar-month__title)
padding: 40rpx
</style>

@ -1,240 +0,0 @@
<template>
<view>
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
title="工位安排"
bg-color="transparent"
:auto-back="true"
:title-style="{ fontWeight: 'bold' }"
safe-area-inset-top
placeholder
/>
<view class="u-flex u-flex-center u-m-20">
<u-input
v-model="queryParams.name"
placeholder="请输入组织名称"
border="surround"
:clear-icon="true"
suffixIcon="search"
font-size="12"
class="u-flex u-flex-center u-m-r-20"
@click="getOrgWorkerList"
/>
<u-input
v-model="queryParams.machineId"
placeholder="请输入关联机台"
border="surround"
:clear-icon="true"
suffixIcon="search"
font-size="12"
@click="getOrgWorkerList"
/>
</view>
<view class="u-flex u-flex-center u-m-l-20 u-m-r-20">
<uni-data-select v-model="queryParams.status" placeholder="请选择组织状态" :localdata="organizationalStatus" @click="getOrgWorkerList">
</uni-data-select>
<view class="u-m-l-20">
<u-button @click="getOrgWorkerList" plain style="height: 35px; background: transparent">搜索</u-button>
</view>
</view>
</u-sticky>
<view class="container">
<view class="u-menu-wrap">
<scroll-view scroll-y="" scroll-with-animation="" class="u-tab-view menu-scroll-view" :scroll-top="scrollTop">
<view v-for="(item, index) in tabbar" :key="index" class="u-tab-item"
:class="[current === index ? 'u-tab-item-active' : '']" :data-current="index"
@tap.stop="switchMenu(index)">
<text class="u-line-1">{{ item.text }}</text>
</view>
</scroll-view>
</view>
<view v-if="orgWorkerList.length" class="wrap">
<view>
<u-list>
<u-list-item
v-for="(item, index) in orgWorkerList"
:key="index"
>
<view class="content" @click="tab.navigateTo('/pages/application/components/deskArrangementHandle', { orgId: item.id, orgType: item.name})">
<view class="u-flex u-m-t-30 u-m-b-30">
<view class="u-flex flex_1">组织名称
<u-text type="success" :text="item.name" class="u-m-l-10" size="12"></u-text>
</view>
<view class="u-flex flex_1">组织等级
<u-text type="primary" :text="findTextByValue(orgClass, item.orgClass)" class="u-flex" size="12"> </u-text>
</view>
</view>
<view class="u-flex u-m-b-30">
<view class="u-flex flex_1">类型
<u-text type="success" :text="findTextByValue(tabbar, item.orgType)" class="u-m-l-10" size="12"></u-text>
</view>
<view class="u-flex flex_1">今日工人
<u-text type="primary" :text="item.workerUserName" class="u-m-l-10" size="12"></u-text>
</view>
</view>
<view class="u-m-t-10 u-m-b-20"><u-line/></view>
<view class="u-flex justify-end">
<view v-if="item.orgClass==='workplace' && item.status==='free'" class="u-m-r-20" @click.stop="navigatorTo(item.orgType, item.id, item.name, item.workerUserName)"> <u-button type="error" plain>派工</u-button></view>
</view>
</view>
</u-list-item>
</u-list>
</view>
</view>
<view v-else class="flex_1"> <u-empty icon="http://cdn.uviewui.com/uview/empty/data.png" /></view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { findTextByValue, organizationalStatus } from "@/api/system/dict/data";
import tab from "@/plugins/tab";
import { getListOrgWorker } from "@/api/mes/deskArrangement";
import { onLoad } from "@dcloudio/uni-app";
import { showConfirm } from "@/utils/common";
const tabbar = ref([
{ text: '所有', value: ''}, { text: '制浆', value: 'zhijiang'},{ text: '成型', value: 'chengxing'},{ text: '烘干', value: 'honggan'},{ text: '转移', value: 'zhuanyi'},{ text: '加湿', value: 'jiashi'},
{ text: '热压',value: 'reya'},{ text: '切边',value: 'qiebian'},{ text: '品检',value: 'pinjian'},{ text: '打包',value: 'dabao'},{ text: '贴标',value: 'tiebiao'},{ text: '品印',value: 'pinyin'},{ text: '塑封',value: 'sufeng'}
])
const orgClass = ref([
{ text: '工位', value: 'workplace'}, { text: '工序', value: 'process'}, { text: '产线', value: 'pipeline'}, { text: '车间', value: 'workshop'}, { text: '工厂', value: 'factory'}
])
const current = ref(0)
const scrollTop = ref(0)
const menuHeight = ref(0) //
const menuItemHeight = ref(0) // item
const switchMenu = async (index)=>{
if (index === current.value) return;
current.value = index;
// item
scrollTop.value = index * menuItemHeight.value + menuItemHeight.value / 2 - menuHeight.value / 2;
orgWorkerList.value = []
getOrgWorkerList()
}
//
const getElRect = (elClass, dataVal) =>{
new Promise(() => {
const query = uni.createSelectorQuery().in(this);
query.select('.' + elClass).fields({ size: true }, res => {
// resnull
if (!res) {
setTimeout(() => {
getElRect(elClass);
}, 10);
return;
}
this[dataVal] = res.height;
}).exec();
})
}
const queryParams = ref({
name: '',
machineId: '',
status: '',
orgType: '',
pageNo: 1
})
const orgWorkerList = ref([])
const getOrgWorkerList = ()=> {
queryParams.value.orgType = tabbar.value[current.value].value
getListOrgWorker(queryParams.value).then(response => {
orgWorkerList.value = response.data
})
}
const navigatorTo = (type, id, orgName, workerName)=>{
if(workerName != null && workerName.length > 0){
showConfirm("工位:"+orgName+",今天已经派工,确定要重新派工吗?").then(
)
}
tab.navigateTo('/pages/application/components/assigningWork', { orgType: type, id: id, name: orgName })
}
onLoad(() => {
getOrgWorkerList()
uni.$on('success', data => {
if (data) {
getOrgWorkerList();
}
});
});
</script>
<style lang="sass" scoped>
.sticky
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%)
backdrop-filter: blur(27.18px)
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1), 0 0.5px 0 0 rgba(0, 0, 0, 0.1)
padding-bottom: 20rpx
.container
margin: 10rpx
background-color: #f1f1f1
display: flex
flex-grow: 1
.u-menu-wrap
display: flex
overflow: hidden
.wrap
flex-grow: 1
background-color: #f1f1f1
padding: 0 0 20rpx 20rpx
font-size: 24rpx
.content
margin: 0 0 20rpx 0
padding: 20rpx
background-color: #ffffff
border-radius: 10rpx
.flex_1
flex: 1
.u-tab-view
width: 120rpx
height: 100%
.u-tab-item
height: 110rpx
background: #f6f6f6
box-sizing: border-box
display: flex
align-items: center
justify-content: center
font-size: 26rpx
color: #444
font-weight: 400
line-height: 1
.u-tab-item-active
position: relative
color: #000
font-size: 30rpx
font-weight: 600
background: #ffffff
.u-tab-item-active::before
border-left: 4px solid #3c9cff
content: ""
position: absolute
height: 32rpx
left: 0
top: 39rpx
.u-tab-view
height: 100%
.u-button
height: 60rpx
</style>

@ -1,169 +0,0 @@
<template>
<view>
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
title="工位安排"
bg-color="transparent"
:auto-back="true"
:title-style="{ fontWeight: 'bold' }"
safe-area-inset-top
placeholder
/>
<u-tabs
:list="menuList"
:current="current"
key-name="name"
:scrollable="false"
:active-style="{
color: '#0E85FF',
}"
@change="change"
>
</u-tabs>
</u-sticky>
<view class="u-m-l-20 u-m-r-20 u-m-t-20">
<uni-datetime-picker v-model="queryParams.workDate" type="datetimerange" :clear-icon="true" @change="getOrgWorkerList"/>
</view>
<view v-if="orgWorkerList.length" class="wrap">
<view>
<u-list>
<u-list-item
v-for="(item, index) in orgWorkerList"
:key="index"
>
<view class="content" @click="">
<view class="u-flex u-m-t-30 u-m-b-30">
<view class="u-flex flex_1">工作日期
<u-text type="success" :text="timestampToTime(item.workDate)" class="u-m-l-10" size="12"></u-text>
</view>
<view class="u-flex flex_1">工位
<u-text type="primary" :text="item.orgName" class="u-flex" size="12"> </u-text>
</view>
</view>
<view class="u-flex u-m-b-30">
<view class="u-flex flex_1">班别
<u-text type="success" :text="item.groupType" class="u-m-l-10" size="12"></u-text>
</view>
<view class="u-flex flex_1">工人
<u-text type="primary" :text="item.workerName" class="u-m-l-10" size="12"></u-text>
</view>
</view>
<view class="u-m-t-10 u-m-b-20"><u-line/></view>
<view class="u-flex justify-end">
<view class="u-m-r-20">
<u-button type="error" plain @click="handleDelete(item.id)"></u-button>
</view>
</view>
</view>
</u-list-item>
</u-list>
</view>
</view>
<view v-else class="flex_1"> <u-empty icon="http://cdn.uviewui.com/uview/empty/data.png" /></view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import tab from "@/plugins/tab";
import { deleteOrgWorker, getOrgWorkerPage } from "@/api/mes/deskArrangement";
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
import { showConfirm } from "@/utils/common";
import { modal } from "@/plugins";
import { timestampToTime } from "@/utils/dateUtil";
const queryParams = ref({
workDate: '',
groupType: '',
orgId: '',
orgType: '',
pageNo: 1,
pageSize: 10
})
const menuList = ref([
{
name: '所有',
value: ''
},
{
name: '白班',
value: '1'
},
{
name: '夜班',
value: '2'
},
{
name: '长白班',
value: '3'
}
])
const show = ref(false)
const current = ref(0)
const change = (index)=>{
current.value = index.index
queryParams.value.groupType = menuList.value[current.value].value
getOrgWorkerList()
}
const orgWorkerList = ref([])
const total = ref()
const getOrgWorkerList = ()=> {
getOrgWorkerPage(queryParams.value).then(response => {
orgWorkerList.value = response.data.list
total.value = response.data.total
})
}
onReachBottom(()=>{
if ((queryParams.value.pageNo - 1) * queryParams.value.pageSize >= total.value) {
return
}
queryParams.value.pageNo++
getOrgWorkerList()
})
const handleDelete = (id)=>{
showConfirm("确认删除工作安排吗?").then(res => {
if (res.confirm) {
deleteOrgWorker(id).then(() => {
queryParams.value.pageNo = 1
orgWorkerList.value = [];
getOrgWorkerList()
modal.msgSuccess("操作成功")
})
}
})
}
onLoad(() => {
queryParams.value.orgId = tab.getParams().orgId
getOrgWorkerList()
});
</script>
<style lang="sass" scoped>
.sticky
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%)
backdrop-filter: blur(27.18px)
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1), 0 0.5px 0 0 rgba(0, 0, 0, 0.1)
padding-bottom: 20rpx
.wrap
background-color: #f1f1f1
padding: 20rpx
font-size: 24rpx
.content
margin: 0 0 20rpx 0
padding: 20rpx
background-color: #ffffff
border-radius: 10rpx
.flex_1
flex: 1
</style>

@ -1,190 +0,0 @@
<template>
<view>
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
title="能源设备"
bg-color="transparent"
:auto-back="true"
:title-style="{ fontWeight: 'bold' }"
safe-area-inset-top
placeholder
/>
<view class="search">
<u-input
v-model="queryParams.name"
placeholder="搜索:设备名称"
border="surround"
:clear-icon="true"
suffixIcon="search"
class="u-m-r-30"
font-size="12"
@click="getEnergyDeviceList"
/>
<uni-data-select v-model="queryParams.deviceType" placeholder="请选择设备类型" :localdata="deviceTypes" @click="getEnergyDeviceList" >
</uni-data-select>
</view>
</u-sticky>
<view v-if="total" class="wrap">
<u-list>
<u-list-item
v-for="(item, index) in energy_device_list"
:key="index"
>
<view class="content" @click="navTo({name: item.name, id: item.id})">
<view class="header">
<view class="title">
<u-image
src="@/static/images/icon/product.png"
width="40rpx"
height="40rpx"
/>
<u-text type="primary" class="u-m-l-10" :text="item.name"></u-text>
</view>
<view><u-text type="info" :text="item.code"></u-text></view>
</view>
<view class="u-flex u-m-t-30 u-m-b-30">
<view class="u-flex flex_1">设备类型
<u-text type="success" :text="item.deviceType" class="u-m-l-10"></u-text>
</view>
<view class="flex_1">抄表周期
<u-text :text="item.checkCron" class="u-flex"> </u-text>
</view>
</view>
<view class="u-m-b-30">
<view class="u-flex flex_1">最后抄表时间
<u-text type="warning" :text="item.lastCheckTime ? timestampToTime(item.lastCheckTime) : ''" class="u-m-l-10"></u-text>
</view>
</view>
<view class="u-flex u-m-b-30">
<view class="u-flex flex_1">最后抄表值
<u-text type="success" :text="item.lastCheckValue" class="u-m-l-10"></u-text>
</view>
<view class="u-flex flex_1">单位
<u-text :text="item.unitName" class="u-m-l-10"></u-text>
</view>
</view>
<view class="u-flex u-m-b-30">
<view class="u-flex flex_1">设备资料
<u-text type="success" :text="item.info" class="u-m-l-10"></u-text>
</view>
<view class="u-flex flex_1">是否启用
<u-text :text="item.isEnable ? '是':'否'" class="u-m-l-10"></u-text>
</view>
</view>
<view class="u-m-t-30 u-m-b-30"><u-line/></view>
<view class="u-flex justify-end">
<view class="u-m-r-20" @click.stop="tab.navigateTo('/pages/application/components/energyEquipmentForm', { name: item.name, id: item.id, time: item.lastCheckTime, value: item.lastCheckValue })"> <u-button type="success" plain>抄表</u-button></view>
<view class="u-m-r-20" @click.stop="tab.navigateTo('/pages/application/components/energyEquipmentFormReset', { name: item.name, id: item.id, time: item.lastCheckTime, value: item.lastCheckValue, code: item.code, isEnable: item.isEnable })"> <u-button type="error" plain>重置</u-button></view>
<view class="u-m-r-20" @click.stop="tab.navigateTo('/pages/application/components/energyEquipmentCreateOrEdit', { id: item.id })"> <u-button type="primary" plain >编辑</u-button></view>
<view class="u-m-r-20" @click.stop="deleteEnergyDeviceById(item.id)"> <u-button type="error" plain>删除</u-button></view>
</view>
</view>
</u-list-item>
</u-list>
</view>
<u-empty v-else icon="http://cdn.uviewui.com/uview/empty/data.png" />
<uni-fab ref="fab" @fabClick="tab.navigateTo('/pages/application/components/energyEquipmentCreateOrEdit')" />
</view>
</template>
<script setup>
import { ref } from 'vue'
import { deleteEnergyDevice, getEnergyDevice} from "@/api/mes/application";
import {onLoad, onReachBottom} from "@dcloudio/uni-app";
import { timestampToTime } from "@/utils/dateUtil";
import tab from "@/plugins/tab";
import { showConfirm } from "@/utils/common";
import { modal } from "@/plugins";
import { deviceTypes } from "@/api/system/dict/data";
const queryParams = ref({
name: '',
deviceType: '',
pageNo: 1,
pageSize: 10
})
const total = ref()
const energy_device_list = ref([])
const getEnergyDeviceList = ()=> {
getEnergyDevice(queryParams.value).then(response => {
energy_device_list.value = response.data.list
total.value = response.data.total
})
}
const deleteEnergyDeviceById = (id)=>{
showConfirm("确认删除能源设备吗?").then(res => {
if (res.confirm) {
deleteEnergyDevice(id).then(() => {
queryParams.value.pageNo = 1
energy_device_list.value = [];
getEnergyDeviceList()
modal.msgSuccess("操作成功")
})
}
})
}
const navTo = (params) => {
tab.navigateTo('/pages/application/components/energyEquipmentDetail',params)
}
onReachBottom(()=>{
if (queryParams.value.pageNo * queryParams.value.pageSize >= total.value) {
return
}
queryParams.value.pageNo++
getEnergyDeviceList()
})
onLoad(() => {
getEnergyDeviceList()
uni.$on('success', data => {
if (data) {
getEnergyDeviceList();
}
});
});
</script>
<style lang="sass" scoped>
.sticky
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%)
backdrop-filter: blur(27.18px)
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1), 0 0.5px 0 0 rgba(0, 0, 0, 0.1)
.search
margin: 20rpx
padding: 20rpx
display: flex
align-items: center
.wrap
background-color: #f1f1f1
padding: 0 20rpx 20rpx 20rpx
.content
margin: 0 0 20rpx 0
padding: 20rpx
background-color: #ffffff
border-radius: 10rpx
.header
display: flex
align-items: center
justify-content: space-between
.title
display: flex
align-items: center
.u-button
height: 60rpx
.flex_1
flex: 1
.u-empty
background: #ffffff
margin: 20rpx
padding-bottom: 20rpx
border-radius: 10rpx
.u-input
height: 35px
</style>

@ -1,126 +0,0 @@
<template>
<view>
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
:title="id ? '编辑能源设备':'新增能源设备'"
bg-color="transparent"
:auto-back="true"
:title-style="{ fontWeight: 'bold' }"
safe-area-inset-top
placeholder
/>
</u-sticky>
<view class="container">
<!-- 自定义表单校验 -->
<uni-forms ref="customForm" :rules="customRules" labelWidth="105px" :modelValue="formData">
<uni-forms-item label="名称" required name="name">
<u-input v-model="formData.name" placholder="请输入名称"/>
</uni-forms-item>
<uni-forms-item label="编码">
<u-input v-model="formData.code" placholder="请输入编码"/>
</uni-forms-item>
<uni-forms-item label="设备类型" required name="deviceType">
<uni-data-checkbox v-model="formData.deviceType" :localdata="deviceTypes"/>
</uni-forms-item>
<uni-forms-item label="抄表周期cron">
<u-input v-model="formData.checkCron" placholder="请输入抄表周期cron"/>
</uni-forms-item>
<uni-forms-item label="单位">
<u-input v-model="formData.unitName" placholder="单位"/>
</uni-forms-item>
<uni-forms-item label="是否启用" required name="isEnable">
<uni-data-checkbox v-model="formData.isEnable" :localdata="isEnable"/>
</uni-forms-item>
<uni-forms-item label="信息资料">
<u-input v-model="formData.info" />
</uni-forms-item>
</uni-forms>
<view class="u-flex justify-end">
<view @click="submit()">
<u-button type="primary">
<uni-icons type="checkbox" class="u-m-r-10"/>
确定
</u-button></view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad, onReady } from "@dcloudio/uni-app";
import tab from "@/plugins/tab";
import {createEnergyDevice, getEnergyDeviceById, updateEnergyDevice} from "@/api/mes/application";
import modal from "@/plugins/modal";
import { deviceTypes, isEnable } from "@/api/system/dict/data";
const formData = ref({
id: undefined,
name: undefined,
code: undefined,
deviceType: undefined,
info: undefined,
checkCron: undefined,
lastCheckTime: undefined,
lastCheckValue: undefined,
unitName: undefined,
isEnable: undefined
})
const id = ref()
const customForm = ref()
//
const customRules = ref({
name: { rules: { required: true, errorMessage: '设备名称不能为空' }},
deviceType: { rules: { required: true, errorMessage: '设备类型不能为空'}},
isEnable: { rules: { required: true, errorMessage: '是否启用不能为空' }}
})
const submit = ()=>{
customForm.value.validate(async(valid)=>{
if(!valid && !id.value) {
await createEnergyDevice(formData.value)
modal.msgSuccess("保存成功")
} else {
await updateEnergyDevice(formData.value)
modal.msgSuccess("修改成功")
}
await tab.navigateBack()
uni.$emit('success', true)
})
}
onReady(()=>{
//
customForm.value.setRules(customRules.value)
})
onLoad(() => {
id.value = tab.getParams().id
if(id.value) {
getEnergyDeviceById(id.value).then(response => {
formData.value = response.data
})
}
});
</script>
<style lang="sass" scoped>
.sticky
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%)
backdrop-filter: blur(27.18px)
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1), 0 0.5px 0 0 rgba(0, 0, 0, 0.1)
.container
padding: 20rpx
margin: 20rpx
background-color: #ffffff
.uniui-checkbox:before
color: #ffffff
.u-button
height: 35px
:deep(.uni-forms-item__content)
display: flex
align-items: center
</style>

@ -1,132 +0,0 @@
<template>
<view>
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
:title="'抄表记录:' + name"
bg-color="transparent"
:auto-back="true"
:title-style="{ fontWeight: 'bold' }"
safe-area-inset-top
placeholder
/>
</u-sticky>
<view class="container">
<u-list>
<u-list-item
v-for="(item, index) in energy_device_detail_list"
:key="index"
>
<view class="content">
<view class="u-flex u-m-t-30 u-m-b-30">
<view class="u-flex flex_1">抄表时间
<u-text type="success" :text="timestampToTime(item.checkTime)" class="u-m-l-10" size="12"></u-text>
</view>
<view class="u-flex flex_1">抄表值
<u-text :text="item.checkValue" class="u-flex" size="12"> </u-text>
</view>
</view>
<view class="u-flex u-m-t-30 u-m-b-30">
<view class="u-flex flex_1">上次抄表时间
<u-text type="success" :text="timestampToTime(item.lastCheckTime)" class="u-m-l-10" size="12"></u-text>
</view>
<view class="u-flex flex_1">上次抄表值
<u-text :text="item.lastCheckValue" class="u-flex" size="12"> </u-text>
</view>
</view>
<view class="u-flex u-m-b-30">
<view class="u-flex flex_1">差值
<u-text type="success" :text="item.diffValue" class="u-m-l-10" size="12"></u-text>
</view>
<view class="u-flex flex_1">单价
<u-text type="success" :text="item.unitPrice" class="u-m-l-10" size="12"></u-text>
</view>
</view>
<view class="u-flex flex_1">备注
<u-text type="info" :text="item.remark" class="u-m-l-10" size="12"></u-text>
</view>
<view class="u-m-t-30 u-m-b-30"><u-line/></view>
<view class="u-flex justify-end">
<view> <u-button type="error" plain @click="deleteEnergyDeviceCheckRecord(item.id)"></u-button></view>
</view>
</view>
</u-list-item>
</u-list>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { deleteEnergyDeviceCheckRecordById, getEnergyDeviceCheckRecord } from "@/api/mes/application";
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
import { timestampToTime } from "@/utils/dateUtil";
import tab from "@/plugins/tab";
import { showConfirm } from "@/utils/common";
import { modal } from "@/plugins";
const queryParams = ref({
pageNo: 1,
pageSize: 10,
deviceId: undefined
})
const name = ref('')
const total = ref()
const energy_device_detail_list = ref([])
const getEnergyDeviceCheckRecordList = ()=> {
getEnergyDeviceCheckRecord(queryParams.value).then(response => {
energy_device_detail_list.value = [...energy_device_detail_list.value, ...response.data.list]
total.value = response.data.total
})
}
/** 删除 */
function deleteEnergyDeviceCheckRecord(id){
showConfirm("确认删除抄表记录吗?").then(res => {
if (res.confirm) {
deleteEnergyDeviceCheckRecordById(id).then(response => {
queryParams.value.pageNo = 1
energy_device_detail_list.value = [];
getEnergyDeviceCheckRecordList()
modal.msgSuccess("操作成功")
})
}
})
}
onReachBottom(()=>{
if ((queryParams.value.pageNo - 1) * queryParams.value.pageSize >= total.value) {
return
}
queryParams.value.pageNo++
getEnergyDeviceCheckRecordList()
})
onLoad(() => {
name.value = tab.getParams().name
queryParams.value.deviceId = tab.getParams().id
getEnergyDeviceCheckRecordList()
});
</script>
<style lang="sass" scoped>
.sticky
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%)
backdrop-filter: blur(27.18px)
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1), 0 0.5px 0 0 rgba(0, 0, 0, 0.1)
.container
background-color: #f1f1f1
padding: 20rpx
.content
margin: 0 0 20rpx 0
padding: 20rpx
background-color: #ffffff
border-radius: 10rpx
.u-button
height: 60rpx
.flex_1
flex: 1
</style>

@ -1,114 +0,0 @@
<template>
<view>
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
:title="'新增:'+name+'抄表'"
bg-color="transparent"
:auto-back="true"
:title-style="{ fontWeight: 'bold' }"
safe-area-inset-top
placeholder
/>
</u-sticky>
<view class="container">
<!-- 自定义表单校验 -->
<uni-forms ref="customForm" :rules="customRules" labelWidth="105px" :modelValue="formData">
<uni-forms-item label="抄表时间" name="checkTime">
<uni-datetime-picker v-model="formData.checkTime" type="date" :clear-icon="true" placeholder="选择抄表时间"/>
</uni-forms-item>
<uni-forms-item label="抄表值" required name="checkValue">
<u-number-box v-model="formData.checkValue" min="0" inputWidth="100%"></u-number-box>
</uni-forms-item>
<uni-forms-item label="上次抄表时间" name="lastCheckTime">
<uni-datetime-picker v-model="formData.lastCheckTime" type="date" :clear-icon="true" placeholder="上次抄表时间" disabled/>
</uni-forms-item>
<uni-forms-item label="上次抄表值" name="lastCheckValue">
<u-number-box v-model="formData.lastCheckValue" min="0" disabled inputWidth="100%"></u-number-box>
</uni-forms-item>
<uni-forms-item label="单价" name="unitPrice">
<u-number-box v-model="formData.unitPrice" min="0" inputWidth="100%"></u-number-box>
</uni-forms-item>
<uni-forms-item label="差值" name="diffValue">
<u-number-box v-model="formData.diffValue" min="0" disabled inputWidth="100%"></u-number-box>
</uni-forms-item>
<uni-forms-item label="备注" name="remark">
<u-input type="text" placeholder="请输入备注" />
</uni-forms-item>
</uni-forms>
<view class="u-flex justify-end">
<view @click="submit()">
<u-button type="success">
<uni-icons type="checkbox" class="u-m-r-10"/>
保存
</u-button></view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad, onReady } from "@dcloudio/uni-app";
import tab from "@/plugins/tab";
import { createEnergyDeviceCheckRecord } from "@/api/mes/application";
import modal from "@/plugins/modal";
const formData = ref({
checkTime: undefined,
checkValue: undefined,
deviceId: undefined,
lastCheckTime: undefined,
lastCheckValue: undefined,
diffValue: undefined,
unitPrice: undefined,
remark: undefined
})
const name = ref('')
const customForm = ref()
//
const customRules = ref({
checkValue: { rules: [{ required: true, errorMessage: '抄表值不能为空'}]},
})
const submit = ()=>{
customForm.value.validate(async(valid)=>{
if(!valid) {
formData.value.checkTime = new Date(formData.value.checkTime).getTime()
await createEnergyDeviceCheckRecord(formData.value)
modal.msgSuccess("保存成功")
await tab.navigateBack()
uni.$emit('success', true)
}
})
}
onReady(()=>{
//
customForm.value.setRules(customRules.value)
})
onLoad(() => {
name.value = tab.getParams().name
formData.value.deviceId = tab.getParams().id
formData.value.lastCheckTime = tab.getParams().time
formData.value.lastCheckValue = tab.getParams().value
});
</script>
<style lang="sass" scoped>
.sticky
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%)
backdrop-filter: blur(27.18px)
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1), 0 0.5px 0 0 rgba(0, 0, 0, 0.1)
.container
padding: 20rpx
margin: 20rpx
background-color: #ffffff
.uniui-checkbox:before
color: #ffffff
.u-button
height: 35px
</style>

@ -1,89 +0,0 @@
<template>
<view>
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
title="重置最后抄表值"
bg-color="transparent"
:auto-back="true"
:title-style="{ fontWeight: 'bold' }"
safe-area-inset-top
placeholder
/>
</u-sticky>
<view class="container">
<!-- 自定义表单校验 -->
<uni-forms labelWidth="105px" :modelValue="formData">
<uni-forms-item label="名称">
<u-input v-model="formData.name" disabled/>
</uni-forms-item>
<uni-forms-item label="编码">
<u-input v-model="formData.code" disabled/>
</uni-forms-item>
<uni-forms-item label="上次抄表时间" name="lastCheckTime">
<uni-datetime-picker v-model="formData.lastCheckTime" type="date" :clear-icon="true" placeholder="选择上次抄表时间"/>
</uni-forms-item>
<uni-forms-item label="最后抄表值" name="lastCheckValue">
<u-input v-model="formData.lastCheckValue"></u-input>
</uni-forms-item>
</uni-forms>
<view class="u-flex justify-end">
<view @click="submit()">
<u-button type="success">
<uni-icons type="checkbox" class="u-m-r-10"/>
确定
</u-button></view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import tab from "@/plugins/tab";
import { updateEnergyDeviceCheckRecord } from "@/api/mes/application";
import modal from "@/plugins/modal";
const formData = ref({
name: undefined,
code: undefined,
id: undefined,
lastCheckTime: undefined,
lastCheckValue: undefined,
})
const submit = async ()=>{
await updateEnergyDeviceCheckRecord(formData.value)
modal.msgSuccess("修改成功")
await tab.navigateBack()
//
uni.$emit('success', true)
}
onLoad(() => {
formData.value.name = tab.getParams().name
formData.value.id = tab.getParams().id
formData.value.lastCheckTime = tab.getParams().time
formData.value.lastCheckValue = tab.getParams().value
formData.value.code = tab.getParams().code
formData.value.isEnable = tab.getParams().isEnable
});
</script>
<style lang="sass" scoped>
.sticky
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%)
backdrop-filter: blur(27.18px)
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1), 0 0.5px 0 0 rgba(0, 0, 0, 0.1)
.container
padding: 20rpx
margin: 20rpx
background-color: #ffffff
.uniui-checkbox:before
color: #ffffff
.u-button
height: 35px
</style>

@ -1,119 +0,0 @@
<template>
<view>
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
title="我的应用"
bg-color="transparent"
:auto-back="false"
:title-style="{ fontWeight: 'bold' }"
left-icon=""
safe-area-inset-top
placeholder
/>
</u-sticky>
<view class="container-wrap">
<view
v-for="(item,index) in appList"
:key="item.title"
class="app-list"
>
<view
class="u-flex u-flex-between"
>
<view class="app-title">
{{ item.title }}
</view>
<u-icon
name="arrow-right"
color="#333333"
size="14"
/>
</view>
<u-row class="model-container">
<u-col v-for="(app, index) in item.list" :key="app.id" span="3">
<view class="u-flex u-flex-center" @click="navTo(path[index])">
<view class="item u-flex u-flex-column">
<u-image
:show-loading="true"
:src="app.img"
width="96rpx"
height="96rpx"
/>
<text class="text">
{{ app.text }}
</text>
</view>
</view>
</u-col>
</u-row>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import tab from "@/plugins/tab";
const appList = ref([
{
title: '基础数据',
list: [
{
id: 1,
img: '/static/images/icon/arrangement.png',
text: '工位安排'
},
{
id: 2,
img: '/static/images/icon/energy.png',
text: '能源设备'
}
]
}
])
const path = ['/pages/application/components/deskArrangement','/pages/application/components/energyEquipment']
const navTo = (url) => {
tab.navigateTo(url)
}
</script>
<style lang="sass" scoped>
.page
width: 100%
.sticky
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%)
backdrop-filter: blur(27.18px)
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1), 0 0.5px 0 0 rgba(0, 0, 0, 0.1)
.container-wrap
padding: 20rpx
.app-list
background-color: #ffffff
padding: 20rpx 10rpx
border-radius: 20rpx
.app-title
color: #343434
font-size: 28rpx
font-weight: bold
.model-container
flex-wrap: wrap
.item
box-sizing: border-box
padding: 24rpx
&:active
background-color: #f1f1f1
border-radius: 20rpx
.text
margin-top: 8rpx
color: #595959
font-size: 24rpx
+ .app-list
margin-top: 20rpx
</style>

@ -1,18 +1,6 @@
<template>
<view>
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
:title="title"
bg-color="transparent"
:auto-back="true"
:title-style="{ fontWeight: 'bold' }"
safe-area-inset-top
placeholder
/>
</u-sticky>
<NavBar :title="title" />
<uni-card class="view-title" :title="title">
<text class="uni-body view-content">{{ content }}</text>
</uni-card>
@ -20,7 +8,12 @@
</template>
<script>
import NavBar from '@/components/common/NavBar.vue'
export default {
components: {
NavBar
},
data() {
return {
title: '',
@ -30,23 +23,15 @@
onLoad(options) {
this.title = options.title
this.content = options.content
uni.setNavigationBarTitle({
title: options.title
})
}
}
</script>
<style scoped lang="scss">
<style scoped>
page {
background-color: #ffffff;
}
.sticky {
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%);
backdrop-filter: blur(27.18px);
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1),
0 0.5px 0 0 rgba(0, 0, 0, 0.1);
}
.view-title {
font-weight: bold;
}

@ -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>

@ -0,0 +1,141 @@
<template>
<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 { 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 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 { 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(() => {
scrollTop.value = 0
}, 0)
}
</script>
<style lang="scss" scoped>
.page-container {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f0f2f5;
}
.main-scroll {
flex: 1;
height: 100%;
}
.content-section {
padding: 0 24rpx 24rpx;
margin-top: -40rpx;
position: relative;
z-index: 1;
}
.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;
&:active {
transform: scale(0.95);
}
}
.go-top-icon {
font-size: 44rpx;
color: #ffffff;
font-weight: bold;
}
</style>

@ -1,170 +0,0 @@
let isReadyLogin = 1
let loginFlag = 1
export default {
//提示窗
tipMsg: function (title, icon, time, mask,callback) {
title = title == undefined ? "系统繁忙" : title;
icon = icon == undefined ? "none" : icon;
time = time == undefined ? 1300 : time;
mask = mask == undefined ? true : mask;
uni.showToast({
title: title,
icon: icon,
mask: mask,
duration: time,
success() {
if(callback){
setTimeout(()=>{
callback()
},time);
}
}
})
},
getTelephoneInfo(){
return new Promise((resolve, reject) => {
var data = uni.getStorageSync("telephoneInfo");
if(!data){
// 获取右上角胶囊的位置信息
//#ifndef H5
let btn = wx.getMenuButtonBoundingClientRect();
uni.getSystemInfo({
success: e => {
let info = {
screenHeight:e.screenHeight,
statusBarHeight:e.statusBarHeight,
windowWidth:e.windowWidth,
top:btn.top
}
uni.setStorageSync("telephoneInfo",info);
resolve(info);
},
fail: (err) => {
reject(err);
}
})
//#endif
}else{
resolve(data);
}
})
},
// 获取当前年月日
getNowDate(){
let date = new Date;
let now = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate();
return now;
},
// 获取上个月的年月日
getPreMonth(date) {
var arr = date.split('-');
var year = arr[0]; //获取当前日期的年份
var month = arr[1]; //获取当前日期的月份
var day = arr[2]; //获取当前日期的日
var days = new Date(year, month, 0);
days = days.getDate(); //获取当前日期中月的天数
var year2 = year;
var month2 = parseInt(month) - 1;
if (month2 == 0) {
year2 = parseInt(year2) - 1;
month2 = 12;
}
var day2 = day;
var days2 = new Date(year2, month2, 0);
days2 = days2.getDate();
if (day2 > days2) {
day2 = days2;
}
if (month2 < 10) {
month2 = '0' + month2;
}
var t2 = year2 + '-' + month2 + '-' + "01";
return t2;
},
//检测小程序更新
checkUpdateVersion(){
//新版本更新
if (uni.canIUse('getUpdateManager')) {
//判断当前微信版本是否支持版本更新
const updateManager = uni.getUpdateManager();
updateManager.onCheckForUpdate(function (res) {
if (res.hasUpdate) {
// 请求完新版本信息的回调
updateManager.onUpdateReady(function () {
uni.showModal({
title: '更新提示',
content: '已更新版本,是否重启小程序?',
showCancel:false,
cancelColor:'#eeeeee',
confirmColor:'#40A2ED',
success: function (res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate();
}
},
});
});
// 新的版本下载失败
updateManager.onUpdateFailed(function () {
uni.showModal({
title: '更新失败',
content: '请检查网络设置,若仍更新失败,重新搜索打开',
success(res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate();
}
}
});
});
}
});
} else {
uni.showModal({
// 如果希望用户在最新版本的客户端上体验您的小程序,可以这样子提示
title: '提示',
content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。',
});
}
},
/**
* @param {string} url 目标页面的路由
* @param {Object} param 传递给目标页面的参数
* @description 处理目标页面的参数转成json字符串传递给param字段在目标页面通过JSON.parse(options.param)接收
*/
navigateTo(url, param = {},flag) {
if(isReadyLogin<=0 && !flag){
this.loginTip();
}else{
let part = '';
for(var item in param){
part += '&' + item + '=' + param[item];
}
url = url + part.replace('&','?');
uni.navigateTo({
url: url,
fail:err=> {
this.tipMsg('页面正在火速开发中,敬请期待!');
},
})
}
},
navigateBack(url, param = {}) {
if (loginFlag <= 0) {
this.tipMsg("很抱歉,你没有权限!");
} else {
let part = '';
for (var item in param) {
part += '&' + item + '=' + param[item];
}
url = "/pages" + url + part.replace('&', '?');
uni.navigateBack({
url: url,
fail: err => {
this.tipMsg('暂未开放该功能!');
},
})
}
},
}

@ -1,79 +0,0 @@
const COLOR = [
"#EE6A66", "#6BC588", "#FFC300", "#24ABFD"
];
var ISCANVAS2D = true;
switch (uni.getSystemInfoSync().platform) {
case 'android':
ISCANVAS2D = true
break;
case 'ios':
ISCANVAS2D = true
break;
default:
ISCANVAS2D = false
break;
}
const RESPOND = {
success: 0,
warn: 301,
error: 500,
};
const TIMEARRAY = [
{
text: '当天',
value: 'today'
},
{
text: '昨天',
value: 'yesterday'
},
{
text: '本周',
value: 'week'
},
{
text: '上周',
value: 'weeklast'
},
{
text: '本月',
value: 'month'
},
{
text: '上月',
value: 'monthlast'
},
{
text: '指定日期',
value: 'auto'
}
];
const TABLIST = [
{name:"企业微信",type:"WECHAT"},
{name:"会员运营",type:"OPERATE"},
{name:"会员健康",type:"GJJK"},
{name:"会员服务",type:"SERVICE"},
];
const CARD_MENU = [
{title:"会员报表中心",author:"howcode",img:"https://s1.ax1x.com/2023/03/31/ppRp4iV.jpg",url:"/myPackageA/pages/main/index"},
{title:"智慧教育报表中心",author:"howcode",img:"https://s1.ax1x.com/2023/03/31/ppRp5GT.jpg",url:"/myPackageA/pages/school/index"},
{title:"差旅报表中心",author:"秋云",img:"https://s1.ax1x.com/2023/03/31/ppRpfI0.jpg",url:""},
{title:"运动报表中心",author:"howcode",img:"https://s1.ax1x.com/2023/03/31/ppRpWaq.jpg",url:"/myPackageA/pages/sport/index"},
{title:"财务报表中心",author:"howcode",img:"https://s1.ax1x.com/2023/03/31/ppRpozF.jpg",url:"/myPackageA/pages/finance/index"},
]
export default {
COLOR,
TIMEARRAY,
TABLIST,
RESPOND,
ISCANVAS2D,
CARD_MENU
}

@ -1,56 +0,0 @@
{
"sumNumber":{
"categories": [],
"series": [
{
"name": "总数",
"data": [],
"type": "line",
"style": "curve",
"color": "#4ECDB6",
"unit":""
}
],
"yAxis":[
{"calibration":true,"position":"left","titleFontSize":12,"unit":"","tofix":0,"min":0,"disableGrid":true}
]
},
"totalQualityNumber":{
"categories": [],
"series": [
{
"name": "合格数",
"data": [],
"type": "line",
"style": "curve",
"color": "#4ECDB6",
"unit":""
}
],
"yAxis":[
{"calibration":true,"position":"left","titleFontSize":12,"unit":"","tofix":0,"min":0,"disableGrid":true}
]
},
"totalWasteNumber":{
"categories": [
"1月",
"2月",
"2月",
"4月",
"5月"
],
"series": [
{
"name": "废品数",
"data": [],
"type": "line",
"style": "curve",
"color": "#4ECDB6",
"unit":""
}
],
"yAxis":[
{"calibration":true,"position":"left","titleFontSize":12,"unit":"","tofix":0,"min":0,"disableGrid":true}
]
}
}

@ -1,61 +1,74 @@
<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" data="">
</image>
<text class="title">生产运营管理系统</text>
</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" icon="" label="" max="" tip="" validator=""/>
<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>
<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" icon="" label="" max="" tip="" validator=""/>
<!-- 顶部装饰背景 -->
<view class="background-decoration">
<view class="wave-bg"></view>
</view>
<view v-if="captchaEnabled" class="input-item flex align-center" style="width: 60%;margin: 0;">
<view class="iconfont icon-code icon"></view>
<input v-model="loginForm.code" type="number" class="input" placeholder="请输入验证码" maxlength="4" icon="" label="" max="" tip="" validator=""/>
<view class="login-code">
<image :src="codeUrl" @click="getCode" class="login-code-img" data=""></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>
</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 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({
tenantName: '内蒙必硕',
username: "",
password: "",
username: "admin",
password: "admin123",
captchaVerification: '',
rememberMe: '',
code: '',
uuid: ''
});
//
function getCode() {
getCodeImg().then(res => {
captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled
@ -65,7 +78,7 @@ function getCode() {
}
})
}
// getCode();
async function handleLogin() {
if (loginForm.value.username === "") {
modal.msgError("请输入您的账号")
@ -75,10 +88,10 @@ async function handleLogin() {
modal.msgError("请输入验证码")
} else {
modal.loading("登录中,请耐心等待...")
await pwdLogin()
pwdLogin()
}
}
//
async function pwdLogin() {
userStore.login(loginForm.value).then(() => {
modal.closeLoading()
@ -92,83 +105,143 @@ async function pwdLogin() {
}
function loginSuccess(result) {
//
userStore.getInfo().then(res => {
uni.switchTab({
url: '/pages/application/index'
uni.reLaunch({
url: '/pages/index'
});
})
}
</script>
<style lang="scss" scoped>
.normal-login-container {
height: 100vh;
overflow: hidden;
background-image: url(@/static/images/login/background.svg);
background-size: cover;
.logo-content {
width: 100%;
font-size: 21px;
text-align: center;
padding-top: 50%;
image {
border-radius: 4px;
}
.title {
margin-left: 10px;
}
}
<style lang="scss">
page {
background-color: #ffffff;
min-height: 100vh;
}
.login-form-content {
text-align: center;
margin: 15% auto 20px;
width: 80%;
.input-item {
margin: 20px auto;
background-color: #f5f6f7;
height: 45px;
border-radius: 20px;
.icon {
font-size: 38rpx;
margin-left: 10px;
color: #999;
}
.scroll-container {
height: 100%;
width: 100%;
overflow: auto;
.input {
.login-container {
width: 100%;
min-height: 100vh;
position: relative;
box-sizing: border-box;
padding: 0 50rpx;
/* 顶部装饰背景 */
.background-decoration {
position: absolute;
top: 20rpx;
left: -9.2%;
right: -9.2%;
width: 118.4%;
height: 400rpx;
overflow: hidden;
.wave-bg {
width: 100%;
font-size: 14px;
line-height: 20px;
text-align: left;
padding-left: 15px;
height: 100%;
background-image: url(@/assets/img/backgroundshadow1.png);
background-size: 100% 100%;
background-repeat: no-repeat;
}
}
.login-btn {
margin-top: 40px;
height: 45px;
/* 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;
}
}
}
.xieyi {
color: #333;
margin-top: 20px;
}
/* 登录内容区 */
.login-content {
position: relative;
top: 110rpx;
.login-code {
height: 38px;
float: right;
/* 欢迎文案 */
.welcome-section {
text-align: left;
margin-top: 10rpx;
.welcome-title {
font-size: 44rpx;
font-weight: 600;
color: #333333;
display: block;
}
.welcome-subtitle {
font-size: 28rpx;
color: #999999;
margin-top: 16rpx;
display: block;
}
}
.login-code-img {
height: 38px;
position: absolute;
margin-left: 10px;
width: 200rpx;
/* 表单区域 */
.form-section {
margin-top: 80rpx;
.form-input {
margin-bottom: 32rpx;
&.password-input {
margin-bottom: 24rpx;
}
}
/* 忘记密码 */
.forgot-password {
text-align: right;
margin-bottom: 48rpx;
.forgot-link {
font-size: 26rpx;
color: #666666;
}
}
/* 登录按钮 */
.login-btn-wrapper {
margin-bottom: 60rpx;
.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,20 +1,6 @@
<template>
<view class="mine-container" :style="{ height: `${windowHeight}px` }">
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
title="个人中心"
bg-color="transparent"
:auto-back="false"
:left-icon="''"
:title-style="{ fontWeight: 'bold' }"
safe-area-inset-top
placeholder
/>
</u-sticky>
<!--顶部个人信息栏-->
<view class="mine-container">
<NavBar :title="pageTitle" />
<view class="header-section">
<view class="flex padding justify-between">
<view class="flex align-center">
@ -24,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>
@ -89,25 +56,29 @@
<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', () => {
@ -143,7 +114,6 @@ function handleLogout() {
popup.value.open();
};
function dialogConfirm() {
//console.log('----------------------------')
userStore.logOut().then(() => {
uni.reLaunch({
url: '/pages/login'
@ -151,7 +121,6 @@ function dialogConfirm() {
})
};
function dialogClose() {
//console.log('')
};
function handleToPwd() {
uni.navigateTo({
@ -162,7 +131,8 @@ function handleHelp() {
uni.navigateTo({
url: '/pages_mine/pages/help/index'
});
};
}
function handleAbout() {
uni.navigateTo({
url: '/pages_mine/pages/about/index'
@ -170,38 +140,90 @@ function handleAbout() {
}
function handleBuilding() {
uni.showToast({
title: '模块建设中~',
title: t('common.moduleBuilding'),
mask: false,
icon: "none",
duration: 1000
});
}
</script>
<style lang="scss" scoped>
.page {
<style lang="scss">
page {
background-color: #f5f6f7;
}
.sticky {
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%);
backdrop-filter: blur(27.18px);
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1),
0 0.5px 0 0 rgba(0, 0, 0, 0.1);
.mine-container {
width: 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%;
height: 100%;
min-height: 100vh;
.header-section {
margin: 10rpx 0;
padding: 40rpx 10rpx 80rpx 0;
box-sizing: border-box;
background-image: url('@/static/images/icon/background.png');
background-position: center center;
background-size: 100% 100%;
overflow: hidden;
color: #fff;
padding: 15px 15px 45px 15px;
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
color: white;
.login-tip {
font-size: 18px;
@ -229,11 +251,10 @@ function handleBuilding() {
.content-section {
position: relative;
top: -50px;
margin: 10rpx;
.mine-actions {
margin: 15px 15px;
padding: 20px 0;
padding: 20px 0px;
border-radius: 8px;
background-color: white;
@ -245,7 +266,7 @@ function handleBuilding() {
.text {
display: block;
font-size: 13px;
margin: 8px 0;
margin: 8px 0px;
}
}
}

@ -1,370 +0,0 @@
<template>
<view>
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
title="生产计划"
bg-color="transparent"
:auto-back="false"
:title-style="{ fontWeight: 'bold' }"
left-icon=""
safe-area-inset-top
placeholder
/>
<u-tabs
:list="menuList"
:current="current"
key-name="name"
:scrollable="false"
:active-style="{
color: '#0E85FF',
}"
@change="change"
>
</u-tabs>
</u-sticky>
<uni-notice-bar show-icon scrollable text="安全生产!有序生产!高效生产!" />
<!-- 开工中-->
<view v-if="current === 0" class="container" >
<u-list>
<u-list-item
v-for="item in startList"
:key="item"
>
<view class="content">
<view class="header">
<view class="title">
<u-image
src="@/static/images/icon/product.png"
width="40rpx"
height="40rpx"
/>
<u-text type="primary" :text=" item.productName" class="u-m-l-10"> </u-text>
</view>
<view><u-text type="success" :text="item.code"></u-text></view>
</view>
<view class="u-flex u-flex-between u-m-t-30">
<u-text :text="`计划数: ${ item.planNumber }`" size="13"></u-text>
<u-text :text="`入库数: ${ item.finishNumber }`" size="13"></u-text>
</view>
<view class="u-flex u-flex-between u-m-t-30">
<u-text :text="`计划开始时间: ${ timestampToTime(item.planStartTime) }`" size="13"></u-text>
<u-text :text="`计划结束时间: ${ timestampToTime(item.planEndTime) }`" size="13"></u-text>
</view>
<view class="text">
<u-text text="备注:" size="13"/>
</view>
<view class="remark">
<u-text :text="item.remark"></u-text>
</view>
<view class="u-m-t-30 u-m-b-30"><u-line/></view>
<view class="u-flex justify-end">
<view v-if="auth.hasPermi('mes:plan:update')" class="u-m-r-30">
<u-button type="success" plain @click="updatePlan(item.id, item.code,'end')">完工</u-button>
</view>
<view v-if="auth.hasPermi('mes:plan:update')" class="u-m-r-30">
<u-button type="error" plain @click="updatePlan(item.id,item.code,'pause')"></u-button>
</view>
<view>
<u-button type="info" plain @click="planProgress(item)"></u-button>
</view>
</view>
</view>
</u-list-item>
</u-list>
</view>
<!-- 派工中-->
<view v-if="current === 1" class="container" >
<u-list>
<u-list-item
v-for="item in dispatch"
:key="item"
>
<view class="content">
<view class="header">
<view class="title">
<u-image
src="@/static/images/icon/product.png"
width="40rpx"
height="40rpx"
/>
<u-text type="primary" :text=" item.productName" class="u-m-l-10"> </u-text>
</view>
<view><u-text type="success" :text="item.code"></u-text></view>
</view>
<view class="u-flex u-flex-between u-m-t-30">
<u-text :text="`计划数: ${ item.planNumber }`" size="13"></u-text>
<u-text :text="`入库数: ${ item.finishNumber }`" size="13"></u-text>
</view>
<view class="u-flex u-flex-between u-m-t-30">
<u-text :text="`计划开始时间: ${ timestampToTime(item.planStartTime) }`" size="13"></u-text>
<u-text :text="`计划结束时间: ${ timestampToTime(item.planEndTime) }`" size="13"></u-text>
</view>
<view class="text">
<u-text text="备注:" size="13"/>
</view>
<view class="remark">
<u-text :text="item.remark"></u-text>
</view>
<view class="u-m-t-30 u-m-b-30"><u-line/></view>
<view class="u-flex justify-end">
<view v-if="auth.hasPermi('mes:plan:update')">
<u-button type="info" plain @click="updatePlan(item.id,item.code, 'start')">开工</u-button>
</view>
</view>
</view>
</u-list-item>
</u-list>
</view>
<!--计划中-->
<view v-if="current === 2" class="container" >
<u-list>
<u-list-item
v-for="item in planList"
:key="item"
>
<view class="content">
<view class="header">
<view class="title">
<u-image
src="@/static/images/icon/product.png"
width="40rpx"
height="40rpx"
/>
<u-text type="primary" :text=" item.productName" class="u-m-l-10"> </u-text>
</view>
<view><u-text type="success" :text="item.code"></u-text></view>
</view>
<view class="u-flex u-flex-between u-m-t-30">
<u-text :text="`计划数: ${ item.planNumber }`" size="13"></u-text>
<u-text :text="`入库数: ${ item.finishNumber }`" size="13"></u-text>
</view>
<view class="u-flex u-flex-between u-m-t-30">
<u-text :text="`计划开始时间: ${ timestampToTime(item.planStartTime) }`" size="13"></u-text>
<u-text :text="`计划结束时间: ${ timestampToTime(item.planEndTime) }`" size="13"></u-text>
</view>
<view class="text">
<u-text text="备注:" size="13"/>
</view>
<view class="remark">
<u-text :text="item.remark"></u-text>
</view>
<view class="u-m-t-30 u-m-b-30"><u-line/></view>
<view class="u-flex justify-end">
<view v-if="auth.hasPermi('mes:plan:update')">
<u-button type="primary" plain>派工</u-button>
</view>
</view>
</view>
</u-list-item>
</u-list>
</view>
<!-- 暂停中 -->
<view v-if="current === 3" class="container" >
<u-list>
<u-list-item
v-for="item in pauseList"
:key="item"
>
<view class="content">
<view class="header">
<view class="title">
<u-image
src="@/static/images/icon/product.png"
width="40rpx"
height="40rpx"
/>
<u-text type="primary" :text=" item.productName" class="u-m-l-10"> </u-text>
</view>
<view><u-text type="success" :text="item.code"></u-text></view>
</view>
<view class="u-flex u-flex-between u-m-t-30">
<u-text :text="`计划数: ${ item.planNumber }`" size="13"></u-text>
<u-text :text="`入库数: ${ item.finishNumber }`" size="13"></u-text>
</view>
<view class="u-flex u-flex-between u-m-t-30">
<u-text :text="`计划开始时间: ${ timestampToTime(item.planStartTime) }`" size="13"></u-text>
<u-text :text="`计划结束时间: ${ timestampToTime(item.planEndTime) }`" size="13"></u-text>
</view>
<view class="text">
<u-text text="备注:" size="13"/>
</view>
<view class="remark">
<u-text :text="item.remark"></u-text>
</view>
<view class="u-m-t-30 u-m-b-30"><u-line/></view>
<view class="u-flex justify-end">
<view v-if="auth.hasPermi('mes:plan:update')" :span="4" class="u-m-r-30">
<u-button type="info" plain @click="updatePlan(item.id, item.code,'start')">开工</u-button>
</view>
<view v-if="auth.hasPermi('mes:plan:update')" :span="4">
<u-button type="success" plain @click="updatePlan(item.id,item.code, 'end')">完工</u-button>
</view>
</view>
</view>
</u-list-item>
</u-list>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { updateStatus, getByStatus } from "@/api/mes/plan"
import { timestampToTime } from "@/utils/dateUtil";
import modal from "@/plugins/modal";
import tab from "@/plugins/tab";
import { showConfirm } from "@/utils/common";
import auth from "@/plugins/auth";
const startList = ref([]);
const dispatch = ref([]);
const pauseList = ref([]);
const planList = ref([]);
const chartData = ref({});
const menuList = ref([
{
name: '开工中'
},
{
name: '派工中'
},
{
name: '计划中'
},
{
name: '暂停中'
}
])
const current = ref(0)
const change = (index)=>{
current.value = index.index
}
onMounted(() => {
getServerData()
getPlanList()
});
function getServerData() {
//
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);
}
//
function getPlanList() {
getByStatus(0).then(response => {
planList.value = response.data
})
getByStatus(1).then(response => {
dispatch.value = response.data
})
getByStatus(2).then(response => {
startList.value = response.data
})
getByStatus(3).then(response => {
pauseList.value = response.data
})
}
/** 开工 */
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 => {
getPlanList()
})
modal.msgSuccess("操作成功")
}
})
}
//
function planProgress(plan){
tab.navigateTo('/page_report/planProgress',plan)
}
</script>
<style lang="scss" scoped>
.page {
width: 100%
}
.sticky {
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%);
backdrop-filter: blur(27.18px);
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1),
0 0.5px 0 0 rgba(0, 0, 0, 0.1);
}
.title {
font-size: 36rpx;
color: #8f8f94;
}
.charts-box {
width: 100%;
height: 300px;
}
.container {
background-color: #f1f1f1;
padding: 0 20rpx 20rpx 20rpx;
.content {
margin: 0 0 20rpx 0;
padding: 20rpx;
background-color: #ffffff;
border-radius: 10rpx;
.header {
display: flex;
align-items: center;
justify-content: space-between;
.title {
display: flex;
align-items: center;
}
}
.flex-box {
margin-top: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 24rpx;
}
.text {
margin-top: 20rpx;
}
.remark {
margin-top: 20rpx;
display: flex;
align-items: center;
padding: 20rpx;
background-color: #f5f7f9;
border-radius: 10rpx;
font-size: 24rpx;
color: #555555;
}
.u-button {
height: 60rpx
}
}
}
</style>

@ -1,125 +0,0 @@
<template>
<view>
<view v-if="copyContent.length > 0" class="ranking">
<view class="ranking-item" v-for="(content,index) in copyContent" :key="index" :style="{padding:progressPadding+'rpx'}">
<view class="name">{{content.name}}</view>
<view class="progress" >
<text :style="{background:content.background,width:content.width + '%',height:progressWidth+'rpx'}"></text>
</view>
<view class="num">{{content.num}}</view>
</view>
</view>
</view>
</template>
<script>
export default{
name:'ranking-list',
props:{
content:{
type: Array,
default() {
return []
}
},
isPC:{
type:Boolean,
default:false
},
isRank:{
type:Boolean,
default:false
}
},
data(){
return{
progressWidth:24,
progressPadding:10,
maxNumber:0,
culCount:0,
copyContent:[]
}
},
watch:{
content(newV){
this.init()
}
},
methods:{
init(){
this.copyContent = this.deepClone(this.content)
if(this.copyContent && this.copyContent.length >0){
if(this.isRank){
this.copyContent = this.copyContent.sort((a,b) => b.num - a.num);
this.maxNumber = this.copyContent[0].num;
}else{
this.maxNumber = Math.max.apply(Math,this.copyContent.map(item => { return item.num }));
}
this.copyContent.map((item,index) =>{
item.width = this.computeWidth(this.maxNumber,item.num);
});
}
},
computeWidth(max,current){
let num = (current / max) * 100;
return num.toFixed(2);
},
deepClone(obj) {
var cloneObj = new obj.constructor()
if(obj === null) return obj
if(obj instanceof Date) return new Date(obj)
if(obj instanceof RegExp) return new RegExp(obj)
if (typeof obj !== 'object') return obj
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
cloneObj[i] = this.deepClone(obj[i])
}
}
return cloneObj
}
},
mounted() {
if(this.isPC){
this.progressWidth = 40;
this.progressPadding = 30;
}
this.init();
}
}
</script>
<style scoped lang="scss">
.ranking-item{
display: flex;
margin-bottom: 13rpx;
align-content: center;
height: 50rpx;
.name{
padding-right: 10rpx;
color: #868688;
font-size: 20rpx;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.progress{
flex:5;
text-align: left;
padding-right: 10rpx;
text{
display: inline-block;
border-radius: 30rpx;
vertical-align:top;
}
}
.num{
font-size: 26rpx;
color: #3EB2F5;
flex: 1;
}
}
</style>

@ -1,374 +1,38 @@
<template>
<view>
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
title="生产报工"
bg-color="transparent"
:title-style="{ fontWeight: 'bold' }"
:auto-back="false"
left-icon=""
safe-area-inset-top
placeholder
/>
<u-tabs
:list="menuList"
:current="current"
key-name="name"
:scrollable="false"
:active-style="{
color: '#0E85FF',
}"
@change="change"
>
</u-tabs>
</u-sticky>
<view class="container">
<view v-if="current === 0" >
<uni-list :border="true">
<uni-list-item v-for="(item, index) in reportList" :key="index">
<!-- 自定义 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>
<u-button v-if="item.reportStatus === 0" type="primary" icon="edit-pen" @click="editOrAddReport(item.id)"/>
<u-button v-if="item.reportStatus === 0" type="success" icon="share-square" @click="updateReport(item.id,1,'提交')"/>
<u-button v-if="item.reportStatus === 0" type="error" icon="trash" @click="deleteReport(item.id)"/>
<u-button v-if="item.reportStatus >= 1" type="primary" icon="view" @click="handleView(item.id)"/>
</template>
</uni-list-item>
</uni-list>
<uni-fab ref="fab" @fabClick="editOrAddReport" />
</view>
<view v-if="auth('mes:produce-report-detail:replace') && current === 1">
<uni-fab :pattern="pattern" ref="fabReplace" @fabClick="addReplaceReport" />
<uni-forms ref="valiReplaceForm" :model="valiFormData">
<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-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-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" :key="index">
<!-- 自定义 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>
<u-button v-if="item.reportStatus < 2" type="primary" icon="edit-pen" @click="addReplaceReport(item.id)"/>
<u-button v-if="item.reportStatus === 0" type="success" icon="share-square" @click="updateReport(item.id,1,'提交')"/>
<u-button v-if="item.reportStatus === 1" type="success" icon="checkmark" @click="updateReport(item.id,2,'通过')"/>
<u-button v-if="item.reportStatus < 2" type="error" icon="trash" @click="deleteReport(item.id)"/>
</template>
</uni-list-item>
</uni-list>
</view>
<view v-if="current === 2 && auth('mes:produce-report-detail:replace')">
<uni-forms ref="valiForm" :model="valiFormData">
<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-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-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" :key="index">
<!-- 自定义 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>
<u-button v-if="item.reportStatus === 0" type="primary" icon="edit-pen" @click="addReplaceReport(item.id)"/>
<u-button v-if="item.reportStatus === 0" type="success" icon="share-square" @click="updateReport(item.id,1,'提交')"/>
<u-button v-if="item.reportStatus === 1" type="success" icon="checkmark" @click="updateReport(item.id,2,'通过')"/>
<u-button v-if="item.reportStatus === 1" type="warning" icon="close" @click="updateReport(item.id,3,'驳回')"/>
<u-button v-if="item.reportStatus < 2" type="error" icon="trash" @click="deleteReport(item.id)"/>
</template>
</uni-list-item>
</uni-list>
</view>
<view v-if="current === 3" ><WorkReport /></view>
</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, updateStatus,deleteByReportId } from "@/api/mes/report";
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";
import WorkReport from "@/pages/workReport.vue";
export default {
components: { WorkReport },
data() {
return {
currentPaneName: '',
loading: false,
reportList: [],
otherReportList: [],
replaceReportList: [],
userList: [],
pattern: {
color: '#7A7E83',
backgroundColor: '#fff',
selectedColor: '#007AFF',
buttonColor: '#e66126',
iconColor: '#fff'
},
valiFormData: {
reportDate: [],
reportDateString: getCurrentDate(),
userId: '',
orgType: 'chengxing',
reportType: '个人',
},
menuList: [
{
name: '个人报工'
},
{
name: '代报工'
},
{
name: '个人报工审核'
},
{
name: '报工报表'
}
],
current: 0
};
},
onLoad() {
this.loading = true
this.getList();
uni.$on('handleSuccess', data => {
if (data) {
this.getList();
}
});
this.getReplaceReportList()
uni.$on('success', data => {
if (data) {
this.getReplaceReportList();
}
});
this.getOtherReportList()
this.getUserList()
this.loading = false
},
methods: {
change(index) {
this.current = index.index
},
auth(permission) {
return auth.hasPermi(permission)
},
processTypes() {
return processTypes
},
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.current === 0) {
this.getList()
}
if (this.current === 1) {
this.getReplaceReportList()
}
if (this.current === 2) {
this.getOtherReportList()
}
})
}
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.current === 0) {
this.getList()
}
if (this.current === 1) {
this.getReplaceReportList()
}
if (this.current === 2) {
this.getOtherReportList()
}
})
}
this.loading = false
})
},
editOrAddReport(id) {
if (id === null || id === undefined) tab.navigateTo('/page_report/reportForm')
else tab.navigateTo('/page_report/reportForm', 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.current === 0) {
this.getList()
}
if (this.current === 1) {
this.getReplaceReportList()
}
if (this.current === 2) {
this.getOtherReportList()
}
},
handleOtherReportChange() {
this.loading = true
//
this.getOtherReportList()
this.loading = false
},
handleReplaceReportChange() {
this.loading = true
//
this.getReplaceReportList()
this.loading = false
},
handleView(id) {
tab.navigateTo('/page_report/ViewForm', id)
}
}
};
<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>
.container {
padding: 15px;
background-color: #ffffff;
}
.sticky {
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%);
backdrop-filter: blur(27.18px);
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1),
0 0.5px 0 0 rgba(0, 0, 0, 0.1);
}
.addSite {
.page-container {
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 {
display: flex;
align-items: center;
color: #ffffff;
.icon {
margin-right: 10rpx;
}
}
flex-direction: column;
height: 100vh;
background-color: #f5f6f7;
}
.u-button {
width: 50rpx;
height: 50rpx;
border-radius: 50%;
margin-right: 10rpx;
}
:deep(.u-icon__icon) {
font-size: 30rpx !important;
margin: 0 !important;
.flex-fill {
flex: 1;
overflow: visible;
}
</style>

@ -1,266 +1,33 @@
<template>
<view >
<u-sticky
class="sticky"
:custom-nav-height="0"
>
<u-navbar
title="近三天投料"
bg-color="transparent"
:title-style="{ fontWeight: 'bold' }"
:auto-back="false"
left-icon=""
safe-area-inset-top
placeholder
/>
<u-tabs
:list="menuList"
:current="current"
key-name="name"
:scrollable="false"
:active-style="{
color: '#0E85FF',
}"
@change="change"
/>
</u-sticky>
<uni-notice-bar show-icon scrollable
text="安全生产!有序生产!高效生产!" />
<!-- 草稿状态-->
<view v-if="current === 0" class="container" >
<u-list>
<u-list-item
v-for="(item, index) in draftList"
:key="item"
>
<view class="content">
<view class="header">
<view class="title">
<u-image
src="@/static/images/icon/product.png"
width="40rpx"
height="40rpx"
/>
<u-text class="u-m-l-10" type="primary" :text="timestampToTime(item.feedingTime)"></u-text>
</view>
<view><u-text type="warning" :text="findTextByValue(pipelineTypes, item.feedingPipeline)"></u-text></view>
<view><u-text type="info" :text="findTextByValue(feedingTypes, item.feedingType)"></u-text></view>
</view>
<view class="u-m-t-30"><u-text :text="item.feedingRecordCode"></u-text></view>
<view class="u-flex u-flex-between u-m-t-30 u-m-b-30">
<view class="u-flex">记录人
<u-text type="success" :text="item.userName"></u-text>
</view>
<view class="u-flex" v-if="item.feedingType !=='org'">/kg
<u-text :text="item.weight"></u-text>
</view>
</view>
<view v-for="(item2, index2) in draftList[index].detailList" :key="index2" class="u-flex u-flex-between u-m-b-30">
<view class="u-flex flex_1">原料
<u-text type="warning" :text="item2.itemName"></u-text>
</view>
<view class="u-flex flex_1">数量
<u-text type="success" :text="item2.weight"></u-text>
</view>
<view class="u-flex flex_1">单位
<u-text :text="item2.unitName"></u-text>
</view>
</view>
<view class="u-m-t-30 u-m-b-30"><u-line/></view>
<view class="u-flex justify-end">
<text class="u-m-r-30">
<u-button type="info" @click="handleUpdate(item.id)"></u-button>
</text>
<text>
<u-button type="error" @click="handleDelete(item.id)"></u-button>
</text>
</view>
</view>
</u-list-item>
</u-list>
</view>
<!-- 已提交-->
<view v-if="current === 1" class="container" >
<u-list>
<u-list-item
v-for="(item, index) in finishList"
:key="item"
>
<view class="content">
<view class="header">
<view class="title">
<u-image
src="@/static/images/icon/product.png"
width="40rpx"
height="40rpx"
/>
<u-text type="primary" class="u-m-l-10" :text="timestampToTime(item.feedingTime)"></u-text>
</view>
<view><u-text type="warning" :text="findTextByValue(pipelineTypes, item.feedingPipeline)"></u-text></view>
<view><u-text type="info" :text="findTextByValue(feedingTypes, item.feedingType)"></u-text></view>
</view>
<view class="u-m-t-30">
<u-text :text="item.feedingRecordCode"></u-text>
</view>
<view class="u-flex u-flex-between u-m-t-30 u-m-b-30">
<view class="u-flex">记录人
<u-text type="success" :text="item.userName" class="u-m-l-10"></u-text>
</view>
<view v-if="item.feedingType !=='org'" class="u-flex">/kg
<u-text :text="item.weight" class="u-flex"> </u-text>
</view>
</view>
<view v-for="(item2, index2) in finishList[index].detailList" :key="index2" class="u-flex u-flex-between u-m-b-30">
<view class="u-flex flex_1">原料
<u-text type="warning" :text="item2.itemName" class="u-m-l-10"></u-text>
</view>
<view class="u-flex flex_1">数量
<u-text type="success" :text="item2.weight" class="u-m-l-10"></u-text>
</view>
<view class="u-flex flex_1">单位
<u-text :text="item2.unitName" class="u-m-l-10"></u-text>
</view>
</view>
</view>
</u-list-item>
</u-list>
</view>
<uni-fab v-if="auth.hasPermi('mes:feeding-record:create') && current===0" 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 } from 'vue';
import { getRecordList, updateStatus, deleteById, getDetailByRecordId } from "@/api/mes/record"
import { timestampToTime } from "@/utils/dateUtil";
import { pipelineTypes,feedingTypes, findTextByValue} from "@/api/system/dict/data";
import modal from "@/plugins/modal";
import tab from "@/plugins/tab";
import { showConfirm } from "@/utils/common";
import auth from "@/plugins/auth";
import { onLoad } from "@dcloudio/uni-app";
const menuList = ref([
{
name: '草稿'
},
{
name: '已提交'
}
])
const current = ref(0)
const change = (index)=>{
current.value = index.index
if(current.value === 0) {
getDraftList()
} else {
getFinishList()
}
}
//
const finishList = ref([]);
//稿
const draftList = ref([]);
const pattern = {
color: '#7A7E83',
backgroundColor: '#fff',
selectedColor: '#007AFF',
buttonColor: '#f4c7c7',
iconColor: '#fff'
}
//
function getFinishList() {
getRecordList("2").then(response => {
finishList.value = response.data
finishList.value.forEach((item, index) => {
getDetailByRecordId(item.id).then(response => {
finishList.value[index].detailList = response.data
})
})
})
}
function getDraftList(){
getRecordList("1").then(response => {
draftList.value = response.data
draftList.value.forEach((item,index)=>{
getDetailByRecordId(item.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("操作成功")
getDraftList()
})
}
})
}
/** 提交 */
function handleDelete(id){
showConfirm("确认删除投料记录吗?").then(res => {
if (res.confirm) {
deleteById(id).then(response => {
modal.msgSuccess("操作成功")
getDraftList()
})
}
})
}
//
function handleAdd(){
tab.navigateTo('/page_record/feedingRecordForm')
}
onLoad(() => {
getDraftList()
uni.$on('saveDraft', data => {
if (data) {
getDraftList();
}
});
});
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>
<style scoped lang="scss">
.sticky {
background: linear-gradient(180deg, #d4e9ff 0%, #f3f9ff 100%);
backdrop-filter: blur(27.18px);
box-shadow: 0 1px 1px 0 rgba(0, 72, 145, 0.1),
0 0.5px 0 0 rgba(0, 0, 0, 0.1);
}
.container {
background-color: #f1f1f1;
padding: 0 20rpx 20rpx 20rpx;
.content {
margin: 0 0 20rpx 0;
padding: 20rpx;
background-color: #ffffff;
border-radius: 10rpx;
.header {
display: flex;
align-items: center;
justify-content: space-between;
.title {
display: flex;
align-items: center;
}
}
}
<style lang="scss" scoped>
.page-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f6f7;
}
.u-button {
height: 60rpx
}
.flex_1 {
.flex-fill {
flex: 1;
overflow: visible;
}
</style>

@ -1,511 +0,0 @@
<template>
<view class="body">
<view class="top_head">
<view class="text_des">
<text class="month_num">{{ nowTime.month }}</text>
<text class="month_text"></text>
<text class="month_year">{{ nowTime.year }}</text>
<text class="point">.</text>
<text class="title">报工报表</text>
</view>
<uni-data-select v-if="auth.hasPermi('mes:produce-report-detail:replace')" class="select" v-model="userId" :localdata="userList" placement="top">
</uni-data-select>
<view class="top_desc">
<view>
<view class="text">上月报工汇总</view>
<view class="text-gray">总数</view>
<view class="remaining">{{ lastMonthSum.sumNumber }}</view>
<view class="row head_block">
<view class="flex_1">
<text class="text-gray">合格数</text>
<text class="text_green">{{ lastMonthSum.totalQualityNumber }}</text>
</view>
<view class="flex_1">
<text class="text-gray">废品数</text>
<text class="income">{{ lastMonthSum.totalWasteNumber }}</text>
</view>
</view>
</view>
<view>
<view class="text">本月报工汇总</view>
<view class="text-gray">总数</view>
<view class="remaining">{{ thisMonthSum.sumNumber }}</view>
<view class="row head_block">
<view class="flex_1">
<text class="text-gray">合格数</text>
<text class="text_green">{{ thisMonthSum.totalQualityNumber }}</text>
</view>
<view class="flex_1">
<text class="text-gray">废品数</text>
<text class="income">{{ thisMonthSum.totalWasteNumber }}</text>
</view>
</view>
</view>
</view>
</view>
<view class="main">
<view class="row_block">
<view class="the_title" style="justify-content: space-between;">
<view class="left_title">
<view class="title_icon"></view>
<text class="margin_stand-samll font-big wide">30天报工数据</text>
</view>
<view class="right_btn">
<view v-for="(item, index) in historyBtn" :key="index" :class="item.state ? 'active_btn' : ''"
@click="changeHistoryBtn(item.type)">{{ item.name }}</view>
</view>
</view>
<view class="charts-box" style="height: 200px;">
<qiun-data-charts type="line" canvasId="finance_a" :canvas2d="isCanvas2d" :reshow="delayload"
:opts="{ xAxis: { itemCount: historyData.length, disableGrid: true, labelCount: 5 }, yAxis: { disableGrid: true, data: [{ disabled: true }] } }"
:chartData="historyData"/>
</view>
</view>
<view class="row_block">
<view class="the_title">
<view class="title_icon"></view>
<text class="margin_stand-samll font-big wide">最近30天计时列表</text>
</view>
<view class="detail_list">
<view v-for="(item, index) in detailList" :key="index" class="detail_item">
<view>
<view class="font-middle">报工日期</view>
<view class="font-small">{{ item.reportDay }}</view>
</view>
<view class="right_content">
<view class="hour">计时时长</view>
<view class="text-gray font-middle">{{ item.reportTime }}</view>
</view>
</view>
</view>
</view>
<view class="end_block">
<view class="the_title" style="margin-bottom: 40rpx;">
<view class="title_icon"></view>
<text class="margin_stand-samll font-big wide">本月上月计时汇总</text>
</view>
<view class="flex_wrap">
<view>
<text class="text-gray" style="margin-right: 10px">上月总计时</text>
<text class="text_green">{{ sumReportTime.totalWasteNumber }}</text>
</view>
<view>
<text class="text-gray" style="margin-right: 10px">本月总计时</text>
<text class="text_green">{{ sumReportTime.totalQualityNumber }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import dataOne from '@/pages/json/1.json';
import { getDayReportTime, getLastMonthSum, getReportTime, getSumReportTime, getThisMonthSum } from "@/api/mes/report";
import useUserStore from "@/store/modules/user";
import Config from '@/pages/js/config'
import { ref, watch } from 'vue';
import { onLoad } from "@dcloudio/uni-app";
import auth from "@/plugins/auth";
import {getOtherPersonalUser} from "@/api/mes/organization";
const now = new Date();
const nowTime = {
year: now.getFullYear(),
month: now.getMonth() + 1,
day: now.getDate()
};
const useStore = useUserStore()
const userId = ref()
userId.value = useStore.userId
const isCanvas2d = ref(Config.ISCANVAS2D)
let delayload = ref(false);
let historyData = ref({});
const reportTime = ref([])
const lastMonthSum = ref({});
const thisMonthSum = ref({});
const sumReportTime = ref({})
const detailList = ref([]);
const historyBtn = ref([
{
name: "总数",
state: true,
type: "sumNumber"
},
{
name: "合格数",
state: false,
type: "totalQualityNumber"
},
{
name: "废品数",
state: false,
type: "totalWasteNumber"
}
]);
const userList = ref([])
const getUserList = ()=> {
getOtherPersonalUser().then(response => {
userList.value = response.data
})
}
const handleChange = ()=>{
getLastMonthSumList()
getThisMonthSumList()
getReportTimeList()
getSumReportTimeList()
getDayReportTimeList()
}
watch(()=>userId.value, (newValue)=> {
if (newValue) {
handleChange()
}
})
const changeHistoryBtn = (type) => {
historyBtn.value.forEach(btn => {
btn.state = false;
});
for (let i = 0; i < historyBtn.value.length; i++) {
historyBtn.value[i].state = historyBtn.value[i].type === type;
}
if(type === 'sumNumber') {
dataOne.sumNumber.categories = []
dataOne.sumNumber.series[0].data = []
reportTime.value.forEach(item => {
dataOne.sumNumber.categories.push(`${new Date(item.reportDay).getMonth() + 1}${new Date(item.reportDay).getDate()}`)
dataOne.sumNumber.series[0].data.push(item.sumNumber)
})
historyData.value = dataOne.sumNumber
} else if (type === 'totalQualityNumber') {
dataOne.totalQualityNumber.categories = []
dataOne.totalQualityNumber.series[0].data = []
reportTime.value.forEach(item => {
dataOne.totalQualityNumber.categories.push(`${new Date(item.reportDay).getMonth() + 1}${new Date(item.reportDay).getDate()}`)
dataOne.totalQualityNumber.series[0].data.push(item.totalQualityNumber)
})
historyData.value = dataOne.totalQualityNumber
} else {
dataOne.totalWasteNumber.categories = []
dataOne.totalWasteNumber.series[0].data = []
reportTime.value.forEach(item => {
dataOne.totalWasteNumber.categories.push(`${new Date(item.reportDay).getMonth() + 1}${new Date(item.reportDay).getDate()}`)
dataOne.totalWasteNumber.series[0].data.push(item.totalWasteNumber)
})
historyData.value = dataOne.totalWasteNumber
}
}
// 30
const defaultBtnIndex = ref(0)
const getReportTimeList = async () => {
defaultBtnIndex.value = 0;
historyBtn.value.forEach((btn, index) => {
btn.state = (index === defaultBtnIndex.value);
});
historyData.value= {}
reportTime.value = []
dataOne.sumNumber.categories = [];
dataOne.sumNumber.series[0].data = [];
const response = await getReportTime(userId.value);
reportTime.value = response.data;
reportTime.value.forEach(item => {
const month = new Date(item.reportDay).getMonth() + 1;
const day = new Date(item.reportDay).getDate();
dataOne.sumNumber.categories.push(`${month}${day}`);
dataOne.sumNumber.series[0].data.push(item.sumNumber);
});
historyData.value = dataOne.sumNumber;
}
//
const getLastMonthSumList = async () => {
const response = await getLastMonthSum(userId.value);
lastMonthSum.value = response.data;
}
//
const getThisMonthSumList = async () => {
const response = await getThisMonthSum(userId.value);
thisMonthSum.value = response.data;
}
//
const getSumReportTimeList = async () => {
const response = await getSumReportTime(userId.value);
sumReportTime.value = response.data;
}
// 30
const getDayReportTimeList = async () => {
const response = await getDayReportTime(userId.value);
detailList.value = response.data;
}
onLoad(() => {
getUserList()
getReportTimeList()
getLastMonthSumList()
getThisMonthSumList()
getSumReportTimeList()
getDayReportTimeList()
});
</script>
<style scoped lang="scss">
.body {
height: 100%;
background-color: #f1f1f1;
margin: 0;
padding-bottom: 20rpx;
.text_green {
color: #4ECDB6;
}
.main {
width: 100%;
padding: 0 10rpx;
box-sizing: border-box;
margin-top: 20rpx;
.detail_list {
height: 700rpx;
overflow: auto;
color: #9E9E9E;
.detail_item {
display: flex;
margin: 20rpx 0;
align-items: center;
.right_content {
width: 75%;
text-align: center;
}
.hour {
color: #000;
}
}
}
.right_btn {
float: right;
display: flex;
color: #ccc;
font-size: 22rpx;
view {
line-height: 50rpx;
height: 50rpx;
margin: 0 20rpx;
}
.active_btn {
padding: 0 20rpx;
border: 1px solid #ccc;
border-radius: 40rpx;
}
}
.end_block {
width: 100%;
box-sizing: border-box;
background-color: #fff;
border-radius: 12rpx;
position: relative;
padding: 20rpx;
.flex_wrap {
display: flex;
justify-content: space-between;
}
}
.row_block {
width: 100%;
box-sizing: border-box;
background-color: #fff;
border-radius: 12rpx;
position: relative;
padding: 20rpx;
&::after {
content: "";
height: 0;
width: 92%;
position: absolute;
transform: translateX(-50%);
left: 50%;
bottom: 0;
border-top: 1px dashed #ccc;
}
}
.the_title {
display: flex;
align-items: center;
.left_title {
display: flex;
align-items: center;
}
.title_icon {
background-color: #7E7E7E;
height: 40rpx;
width: 10rpx;
border-radius: 10rpx;
margin-right: 20rpx;
font-size: 16px;
font-weight: 600;
}
}
.extend_rank {
width: 100%;
background-color: #F5F5F5;
box-sizing: border-box;
padding: 10rpx;
.rank_item {
width: 100%;
margin: 20rpx 0;
box-sizing: border-box;
display: flex;
font-size: 26rpx;
justify-content: space-between;
align-items: center;
image {
width: 10%;
}
text {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: block;
}
.name {
margin: 0 10rpx;
color: #7D7D7D;
width: 20%;
}
.desc {
width: 50%;
color: #ccc;
}
.money {
width: 20%;
text-align: right;
}
}
}
}
.top_head {
position: relative;
height: 495rpx;
width: 100%;
padding: 110rpx 10rpx 0 10rpx;
background: url('@/static/images/icon/background.png') no-repeat center 0;
box-sizing: border-box;
.select {
position: absolute;
width: 240rpx;
top: 155rpx;
right: 30rpx;
:deep(.uni-select__input-text) {
color: #ffffff;
font-weight: bold;
}
}
.top_desc {
width: 100%;
border-radius: 20rpx;
background-color: #fff;
margin-top: 20rpx;
padding: 20rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
.text {
font-size: 30rpx;
font-weight: bold;
color: #1783f2;
margin-bottom: 10px
}
.text-gray {
font-size: 28rpx;
color: #ccc;
margin-right: 10rpx;
}
.remaining {
font-size: 46rpx;
}
.flex_1 {
flex: 1;
}
.head_block {
margin-top: 20rpx;
.income {
color: #E34B5E;
}
}
}
.text_des {
height: 100rpx;
color: #fff;
font-weight: 900;
position: relative;
margin-left: 60rpx;
margin-bottom: 40rpx;
text {
display: inline-block;
height: 100%;
}
.month_num {
font-size: 90rpx;
}
.month_text {
font-size: 56rpx;
}
.month_year {
font-size: 22rpx;
position: absolute;
left: 60rpx;
top: 20rpx;
}
.point {
font-size: 40rpx;
}
.title {
font-size: 40rpx;
}
}
}
}
</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>

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

Loading…
Cancel
Save