Compare commits
77 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
c33d37ea23 | 4 weeks ago |
|
|
b2cb9aeee0 | 1 month ago |
|
|
d0d4430b56 | 1 month ago |
|
|
8392b5d63a | 1 month ago |
|
|
b5f7848ede | 1 month ago |
|
|
c2151b7c2f | 1 month ago |
|
|
55796a6a41 | 1 month ago |
|
|
1846e783a3 | 1 month ago |
|
|
bdd15e5104 | 1 month ago |
|
|
4bc2589ced | 1 month ago |
|
|
af78c2ffa3 | 1 month ago |
|
|
b849d644ee | 1 month ago |
|
|
b0004e30c7 | 1 month ago |
|
|
e632a6dd8f | 1 month ago |
|
|
a97d9223b6 | 1 month ago |
|
|
d16f397336 | 1 month ago |
|
|
15912db748 | 1 month ago |
|
|
bcd9a4e734 | 1 month ago |
|
|
298f8743da | 1 month ago |
|
|
3a3ac5de5a | 1 month ago |
|
|
1caaeb935b | 1 month ago |
|
|
ab45f7708c | 1 month ago |
|
|
24b91e495c | 1 month ago |
|
|
6c8eef9771 | 2 months ago |
|
|
1df0eda266 | 2 months ago |
|
|
7741a50eac | 2 months ago |
|
|
f36f51b2f7 | 2 months ago |
|
|
a7ac7d8ad7 | 2 months ago |
|
|
220489ef43 | 2 months ago |
|
|
2faec8ef4c | 2 months ago |
|
|
bf62c5df31 | 2 months ago |
|
|
a3c8c7e412 | 2 months ago |
|
|
38ab4c77b2 | 2 months ago |
|
|
7b5b45d8f8 | 2 months ago |
|
|
9c47239a9f | 2 months ago |
|
|
61e06971f1 | 2 months ago |
|
|
3f496dea47 | 2 months ago |
|
|
493fd0125f | 2 months ago |
|
|
929dea162b | 2 months ago |
|
|
b4b7c1359c | 2 months ago |
|
|
c89ce1b06b | 2 months ago |
|
|
d3565c0b44 | 2 months ago |
|
|
6dbe599eda | 2 months ago |
|
|
a5291165db | 2 months ago |
|
|
99bf9b3b4d | 2 months ago |
|
|
a9fa9b4f02 | 2 months ago |
|
|
6ff98266bc | 2 months ago |
|
|
65fff2a1c8 | 2 months ago |
|
|
853472a7da | 2 months ago |
|
|
6cce45cb0e | 2 months ago |
|
|
5244e54162 | 2 months ago |
|
|
6bf11ff41c | 2 months ago |
|
|
e41283295b | 2 months ago |
|
|
b1e6d3af72 | 2 months ago |
|
|
d3dfc45074 | 2 months ago |
|
|
22179e1eac | 3 months ago |
|
|
d537a23660 | 3 months ago |
|
|
5aab6cad57 | 3 months ago |
|
|
f45f518c2a | 3 months ago |
|
|
e3dec8568e | 3 months ago |
|
|
3f17d6317f | 3 months ago |
|
|
0ea15894bd | 3 months ago |
|
|
1514cdf7b4 | 3 months ago |
|
|
f8082bd587 | 3 months ago |
|
|
207ff2ce3b | 3 months ago |
|
|
808f0838ab | 3 months ago |
|
|
6c4e9d5e8c | 3 months ago |
|
|
d5710f1e87 | 3 months ago |
|
|
41fd0348bf | 3 months ago |
|
|
878304b2c9 | 3 months ago |
|
|
1fd1cf21f8 | 3 months ago |
|
|
ec7962d050 | 3 months ago |
|
|
f181fa9986 | 3 months ago |
|
|
0d173acd51 | 3 months ago |
|
|
93da020626 | 3 months ago |
|
|
b943caa4f9 | 3 months ago |
|
|
15025df2f5 | 3 months ago |
@ -0,0 +1 @@
|
||||
VITE_APP_BASE_URL=http://192.168.5.106:48081
|
||||
@ -0,0 +1 @@
|
||||
VITE_APP_BASE_URL=
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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'
|
||||
})
|
||||
}
|
||||
@ -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(',') }
|
||||
})
|
||||
}
|
||||
@ -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 }
|
||||
})
|
||||
}
|
||||
@ -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,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,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,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>
|
||||
@ -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>
|
||||
@ -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,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,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,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,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,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>
|
||||
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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue