刀具
This commit is contained in:
parent
97d043a9b7
commit
423facda3c
127
src/api/tool.ts
127
src/api/tool.ts
@ -1,77 +1,78 @@
|
|||||||
import { request } from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
export interface Tool {
|
|
||||||
id?: number
|
|
||||||
toolCode: string
|
|
||||||
toolName: string
|
|
||||||
toolType: string
|
|
||||||
toolTypeId?: number
|
|
||||||
specModel: string
|
|
||||||
material: string
|
|
||||||
manufacturer: string
|
|
||||||
purchaseDate?: string | number
|
|
||||||
lifeCycle: number
|
|
||||||
usedCount?: number
|
|
||||||
status: number
|
|
||||||
remark: string
|
|
||||||
createTime?: string
|
|
||||||
updateTime?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const toolApi = {
|
export const toolApi = {
|
||||||
page(params: {
|
page(params: { page: number; pageSize: number; toolCode?: string; toolName?: string; toolType?: number; brand?: string }) {
|
||||||
page: number
|
return request({ url: '/biz/tool/page', method: 'get', params })
|
||||||
pageSize: number
|
|
||||||
toolCode?: string
|
|
||||||
toolName?: string
|
|
||||||
status?: number
|
|
||||||
}) {
|
|
||||||
return request({
|
|
||||||
url: '/biz/tool/page',
|
|
||||||
method: 'get',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
create(data: Tool) {
|
detail(id: number) {
|
||||||
return request({
|
return request({ url: `/biz/tool/${id}`, method: 'get' })
|
||||||
url: '/biz/tool',
|
|
||||||
method: 'post',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
update(data: Tool) {
|
create(data: any) {
|
||||||
return request({
|
return request({ url: '/biz/tool', method: 'post', data })
|
||||||
url: '/biz/tool',
|
},
|
||||||
method: 'put',
|
|
||||||
data
|
update(data: any) {
|
||||||
})
|
return request({ url: '/biz/tool', method: 'put', data })
|
||||||
},
|
},
|
||||||
|
|
||||||
delete(ids: number[]) {
|
delete(ids: number[]) {
|
||||||
return request({
|
return request({ url: `/biz/tool/${ids.join(',')}`, method: 'delete' })
|
||||||
url: `/biz/tool/${ids.join(',')}`,
|
},
|
||||||
method: 'delete'
|
|
||||||
|
export(params: { toolCode?: string; toolName?: string }) {
|
||||||
|
return request({ url: '/biz/tool/export', method: 'get', params, responseType: 'blob' })
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadTemplate() {
|
||||||
|
return request({ url: '/biz/tool/template', method: 'get', responseType: 'blob' })
|
||||||
|
},
|
||||||
|
|
||||||
|
importData(file: File) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
return request<{ success: number; fail: number; errors: string[] }>({
|
||||||
|
url: '/biz/tool/import',
|
||||||
|
method: 'post',
|
||||||
|
data: formData,
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
export(params?: {
|
usagePage(params: { page: number; pageSize: number; toolId?: number; stationId?: number; sourceType?: string }) {
|
||||||
ids?: number[]
|
return request({ url: '/biz/tool/usage/page', method: 'get', params })
|
||||||
toolCode?: string
|
},
|
||||||
toolName?: string
|
|
||||||
status?: number
|
usageDetail(id: number) {
|
||||||
}) {
|
return request({ url: `/biz/tool/usage/${id}`, method: 'get' })
|
||||||
const p: Record<string, any> = {}
|
},
|
||||||
if (params?.ids?.length) p.ids = params.ids.join(',')
|
|
||||||
if (params?.toolCode) p.toolCode = params.toolCode
|
usageCreate(data: any) {
|
||||||
if (params?.toolName) p.toolName = params.toolName
|
return request({ url: '/biz/tool/usage', method: 'post', data })
|
||||||
if (params?.status != null) p.status = params.status
|
},
|
||||||
return request({
|
|
||||||
url: '/biz/tool/report',
|
usageUpdate(data: any) {
|
||||||
method: 'get',
|
return request({ url: '/biz/tool/usage', method: 'put', data })
|
||||||
params: p,
|
},
|
||||||
responseType: 'blob'
|
|
||||||
})
|
usageDelete(ids: number[]) {
|
||||||
|
return request({ url: `/biz/tool/usage/${ids.join(',')}`, method: 'delete' })
|
||||||
|
},
|
||||||
|
|
||||||
|
usageSync(handoverId: number) {
|
||||||
|
return request({ url: `/biz/tool/usage/sync/${handoverId}`, method: 'post' })
|
||||||
|
},
|
||||||
|
|
||||||
|
usageExport(params: { toolId?: number; stationId?: number; sourceType?: string }) {
|
||||||
|
return request({ url: '/biz/tool/usage/export', method: 'get', params, responseType: 'blob' })
|
||||||
|
},
|
||||||
|
|
||||||
|
deviceList() {
|
||||||
|
return request<{ value: number; label: string }[]>({ url: '/biz/tool/device/list', method: 'get' })
|
||||||
|
},
|
||||||
|
|
||||||
|
userList() {
|
||||||
|
return request<{ value: number; label: string }[]>({ url: '/biz/tool/user/list', method: 'get' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
||||||
import { useUserStore } from '@/stores/user'
|
import type { MenuInfo } from '@/api/auth'
|
||||||
//import type { MenuInfo } from '@/api/auth'
|
|
||||||
|
|
||||||
// 动态导入所有页面组件
|
// 动态导入所有页面组件
|
||||||
const modules = import.meta.glob('/src/views/**/*.vue')
|
const modules = import.meta.glob('/src/views/**/*.vue')
|
||||||
@ -16,18 +15,13 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/login/index.vue'),
|
component: () => import('@/views/login/index.vue'),
|
||||||
meta: { title: '登录', requiresAuth: false }
|
meta: { title: '登录', requiresAuth: false }
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/register',
|
path: '/register',
|
||||||
name: 'Register',
|
name: 'Register',
|
||||||
component: () => import('@/views/register/index.vue'),
|
component: () => import('@/views/register/index.vue'),
|
||||||
meta: { title: '注册', requiresAuth: false }
|
meta: { title: '注册', requiresAuth: false }
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/proterminal',
|
|
||||||
name: 'proterminal',
|
|
||||||
component: () => import('@/views/proterminal/index.vue'),
|
|
||||||
meta: { title: '生产终端',requiresAuth: true }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'Layout',
|
name: 'Layout',
|
||||||
@ -40,6 +34,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/dashboard/index.vue'),
|
component: () => import('@/views/dashboard/index.vue'),
|
||||||
meta: { title: '首页', icon: 'HomeOutline' }
|
meta: { title: '首页', icon: 'HomeOutline' }
|
||||||
},
|
},
|
||||||
|
|
||||||
// 个人中心
|
// 个人中心
|
||||||
{
|
{
|
||||||
path: 'profile',
|
path: 'profile',
|
||||||
@ -53,6 +48,33 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'SystemUser',
|
name: 'SystemUser',
|
||||||
component: () => import('@/views/system/user/index.vue'),
|
component: () => import('@/views/system/user/index.vue'),
|
||||||
meta: { title: '用户管理', icon: 'PersonOutline' }
|
meta: { title: '用户管理', icon: 'PersonOutline' }
|
||||||
|
},
|
||||||
|
// 刀具管理(静态路由)
|
||||||
|
{
|
||||||
|
path: 'biz/tool',
|
||||||
|
name: 'BizTool',
|
||||||
|
component: () => import('@/views/biz/tool/index.vue'),
|
||||||
|
meta: { title: '刀具管理', icon: 'BuildOutline' }
|
||||||
|
},
|
||||||
|
// 刀具生产管理(静态路由,确保稳定访问)
|
||||||
|
{
|
||||||
|
path: 'biz/tool/usage',
|
||||||
|
name: 'BizToolUsage',
|
||||||
|
component: () => import('@/views/biz/tool/usage/index.vue'),
|
||||||
|
meta: { title: '刀具生产管理', icon: 'BuildOutline' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'biz/toolUsage',
|
||||||
|
name: 'BizToolUsageAlias',
|
||||||
|
component: () => import('@/views/biz/tool/usage/index.vue'),
|
||||||
|
meta: { title: '刀具生产管理', icon: 'BuildOutline' }
|
||||||
|
},
|
||||||
|
// NC代码库
|
||||||
|
{
|
||||||
|
path: 'biz/ncCode',
|
||||||
|
name: 'BizNcCode',
|
||||||
|
component: () => import('@/views/biz/ncCode/index.vue'),
|
||||||
|
meta: { title: 'NC代码库', icon: 'DocumentOutline' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'system/role',
|
path: 'system/role',
|
||||||
@ -122,80 +144,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/org/post/index.vue'),
|
component: () => import('@/views/org/post/index.vue'),
|
||||||
meta: { title: '岗位管理', icon: 'IdCardOutline' }
|
meta: { title: '岗位管理', icon: 'IdCardOutline' }
|
||||||
},
|
},
|
||||||
// 项目管理
|
|
||||||
{
|
|
||||||
path: 'biz/orderProject',
|
|
||||||
name: 'OrderProject',
|
|
||||||
component: () => import('@/views/biz/orderProject/index.vue'),
|
|
||||||
meta: { title: '项目管理', icon: 'PersonOutline' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'biz/orderProject/gantt',
|
|
||||||
name: 'OrderProjectGantt',
|
|
||||||
component: () => import('@/views/biz/orderProject/components/GanttSchedule.vue'),
|
|
||||||
meta: { title: '甘特图排产', icon: 'CalendarOutline', activeMenu: '/biz/orderProject' }
|
|
||||||
},
|
|
||||||
//运营管理
|
|
||||||
{
|
|
||||||
path: 'biz/orderItem',
|
|
||||||
name: 'orderItem',
|
|
||||||
component: () => import('@/views/biz/orderItem/index.vue'),
|
|
||||||
meta: { title: '生产订单', icon: 'ListOutline' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'biz/orderProcessPlan',
|
|
||||||
name: 'orderProcessPlan',
|
|
||||||
component: () => import('@/views/biz/orderProcessPlan/index.vue'),
|
|
||||||
meta: { title: '工序计划', icon: 'ListOutline' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'biz/orderProcessPlan/board',
|
|
||||||
name: 'orderProcessPlanBoard',
|
|
||||||
component: () => import('@/views/biz/orderProcessPlan/board.vue'),
|
|
||||||
meta: { title: '工序计划看板', icon: 'ListOutline', activeMenu: '/biz/orderProcessPlan' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'biz/device',
|
|
||||||
name: 'device',
|
|
||||||
component: () => import('@/views/biz/device/index.vue'),
|
|
||||||
meta: { title: '设备管理', icon: 'HardwareChipOutline' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'biz/device/board',
|
|
||||||
name: 'deviceRealtimeBoard',
|
|
||||||
component: () => import('@/views/biz/device/board.vue'),
|
|
||||||
meta: { title: '设备状态看板', icon: 'GridOutline', activeMenu: '/biz/device' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'biz/user',
|
|
||||||
name: 'user',
|
|
||||||
component: () => import('@/views/biz/user/index.vue'),
|
|
||||||
meta: { title: '客户管理', icon: 'ListOutline' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'biz/order',
|
|
||||||
name: 'order',
|
|
||||||
component: () => import('@/views/biz/order/index.vue'),
|
|
||||||
meta: { title: '订单管理', icon: 'ListOutline' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'biz/orderProject',
|
|
||||||
name: 'orderProject',
|
|
||||||
component: () => import('@/views/biz/orderProject/index.vue'),
|
|
||||||
meta: { title: '项目管理', icon: 'ListOutline' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'biz/orderProject/gantt',
|
|
||||||
name: 'OrderProjectGantt',
|
|
||||||
component: () => import('@/views/biz/orderProject/components/GanttSchedule.vue'),
|
|
||||||
meta: { title: '甘特图排产', icon: 'CalendarOutline', activeMenu: '/biz/orderProject' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'biz/code',
|
|
||||||
name: 'code',
|
|
||||||
component: () => import('@/views/biz/code/index.vue'),
|
|
||||||
meta: { title: '邀请码配置', icon: 'SettingsSharp' }
|
|
||||||
},
|
|
||||||
// 系统日志
|
// 系统日志
|
||||||
{
|
{
|
||||||
path: 'log/operlog',
|
path: 'log/operlog',
|
||||||
@ -258,57 +206,8 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/test/test/index.vue'),
|
component: () => import('@/views/test/test/index.vue'),
|
||||||
meta: { title: '测试菜单', icon: 'StarOutline' }
|
meta: { title: '测试菜单', icon: 'StarOutline' }
|
||||||
},
|
},
|
||||||
|
// 开发工具
|
||||||
{
|
{
|
||||||
path: 'biz/submitLog',
|
|
||||||
name: 'submitLog',
|
|
||||||
component: () => import('@/views/biz/submitLog/index.vue'),
|
|
||||||
meta: { title: '派工工单汇报记录', icon: 'ListOutline' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'biz/processRoute',
|
|
||||||
name: 'processRoute',
|
|
||||||
component: () => import('@/views/biz/processRoute/index.vue'),
|
|
||||||
meta: { title: '工艺路线主表', icon: 'ListOutline' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'biz/processRouteStep',
|
|
||||||
name: 'processRouteStep',
|
|
||||||
component: () => import('@/views/biz/processRouteStep/index.vue'),
|
|
||||||
meta: { title: '工艺路线工序明细', icon: 'ListOutline' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'biz/flowingAround',
|
|
||||||
name: 'flowingAround',
|
|
||||||
component: () => import('@/views/biz/flowingAround/index.vue'),
|
|
||||||
meta: { title: '流转记录表', icon: 'ListOutline' }
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
path: 'biz/outsourcing',
|
|
||||||
name: 'outsourcing',
|
|
||||||
component: () => import('@/views/biz/outsourcing/index.vue'),
|
|
||||||
meta: { title: '工序委外', icon: 'ListOutline' }
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// path: 'biz/tool',
|
|
||||||
// name: 'tool',
|
|
||||||
// component: () => import('@/views/biz/tool/index.vue'),
|
|
||||||
// meta: { title: '刀具管理', icon: 'ToolOutline' }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: 'biz/tool/usage',
|
|
||||||
// name: 'toolUsage',
|
|
||||||
// component: () => import('@/views/biz/tool/usage.vue'),
|
|
||||||
// meta: { title: '刀具使用记录', icon: 'ToolOutline', activeMenu: '/biz/tool' }
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
path: 'biz/errorNotification',
|
|
||||||
name: 'errorNotification',
|
|
||||||
component: () => import('@/views/biz/errorNotification/index.vue'),
|
|
||||||
meta: { title: 'ErrorNotification', icon: 'ListOutline' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// 开发工具
|
|
||||||
path: 'tool/gen',
|
path: 'tool/gen',
|
||||||
name: 'ToolGen',
|
name: 'ToolGen',
|
||||||
component: () => import('@/views/tool/gen/index.vue'),
|
component: () => import('@/views/tool/gen/index.vue'),
|
||||||
@ -323,16 +222,11 @@ const routes: RouteRecordRaw[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// path: '/:pathMatch(.*)',
|
|
||||||
// name: 'NotFound',
|
|
||||||
// component: () => import('@/views/error/404.vue'),
|
|
||||||
// meta: { title: '404', requiresAuth: false }
|
|
||||||
// }
|
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)',
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'NotFound',
|
||||||
component: () => import('@/views/error/404.vue'),
|
component: () => import('@/views/error/404.vue'),
|
||||||
// meta: { title: '404', requiresAuth: false }
|
meta: { title: '404', requiresAuth: false }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -344,29 +238,44 @@ const router = createRouter({
|
|||||||
// 已添加的动态路由
|
// 已添加的动态路由
|
||||||
const addedRouteNames = new Set<string>()
|
const addedRouteNames = new Set<string>()
|
||||||
|
|
||||||
|
// 动态路由是否已初始化
|
||||||
|
let dynamicRoutesInitialized = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据菜单动态添加新路由(只添加静态路由中没有的)
|
* 根据菜单动态添加新路由(只添加静态路由中没有的)
|
||||||
*/
|
*/
|
||||||
export function addDynamicRoutes(menus: any) { //MenuInfo[]
|
export function addDynamicRoutes(menus: MenuInfo[]) {
|
||||||
//console.log('[动态路由] 开始处理菜单:', menus)
|
console.log('[动态路由] 开始处理菜单:', menus)
|
||||||
const addRoutes = (menuList: any) => { //MenuInfo[]
|
|
||||||
|
// 获取 Layout 路由的现有子路由路径
|
||||||
|
const getLayoutChildPaths = () => {
|
||||||
|
const layoutRoute = router.getRoutes().find(r => r.name === 'Layout')
|
||||||
|
if (layoutRoute && layoutRoute.children) {
|
||||||
|
return new Set(layoutRoute.children.map(c => c.path))
|
||||||
|
}
|
||||||
|
return new Set<string>()
|
||||||
|
}
|
||||||
|
|
||||||
|
const addRoutes = (menuList: MenuInfo[]) => {
|
||||||
for (const menu of menuList) {
|
for (const menu of menuList) {
|
||||||
// 只处理菜单类型(type=2),且有 path
|
// 只处理菜单类型(type=2),且有 path
|
||||||
if (menu.type === 2 && menu.path) {
|
if (menu.type === 2 && menu.path) {
|
||||||
const routeName = 'Dynamic-' + menu.id
|
const routeName = 'Dynamic-' + menu.id
|
||||||
|
|
||||||
// 检查是否已经有同路径的静态路由
|
// 规范化路径,去掉开头的 /
|
||||||
const existingRoutes = router.getRoutes()
|
|
||||||
const menuPath = menu.path.startsWith('/') ? menu.path.slice(1) : menu.path
|
const menuPath = menu.path.startsWith('/') ? menu.path.slice(1) : menu.path
|
||||||
const pathExists = existingRoutes.some(r => r.path === '/' + menuPath || r.path === menuPath)
|
|
||||||
|
// 检查是否已经有同路径的路由(检查 Layout 的子路由)
|
||||||
|
const layoutChildPaths = getLayoutChildPaths()
|
||||||
|
const pathExists = layoutChildPaths.has(menuPath) || layoutChildPaths.has('/' + menuPath)
|
||||||
|
|
||||||
if (pathExists) {
|
if (pathExists) {
|
||||||
// console.log(`[动态路由] 跳过(已存在): ${menuPath}`)
|
console.log(`[动态路由] 跳过(已存在): ${menuPath}`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addedRouteNames.has(routeName)) {
|
if (addedRouteNames.has(routeName)) {
|
||||||
//console.log(`[动态路由] 跳过(已添加): ${menuPath}`)
|
console.log(`[动态路由] 跳过(已添加): ${menuPath}`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -389,10 +298,35 @@ export function addDynamicRoutes(menus: any) { //MenuInfo[]
|
|||||||
} else if (menu.component) {
|
} else if (menu.component) {
|
||||||
// 普通菜单,加载组件
|
// 普通菜单,加载组件
|
||||||
const componentName = menu.component.startsWith('/') ? menu.component.slice(1) : menu.component
|
const componentName = menu.component.startsWith('/') ? menu.component.slice(1) : menu.component
|
||||||
const componentPath = `/src/views/${componentName}.vue`
|
|
||||||
const component = modules[componentPath]
|
|
||||||
|
|
||||||
//console.log(`[动态路由] 处理: path=${menuPath}, component=${componentPath}, 组件存在=${!!component}`)
|
// 组件路径映射表(解决菜单component与实际文件路径不匹配的问题)
|
||||||
|
const componentMapping: Record<string, string> = {
|
||||||
|
'biz/toolUsage': 'biz/tool/usage/index',
|
||||||
|
'biz/toolUsage/index': 'biz/tool/usage/index',
|
||||||
|
'biz/tool/usage': 'biz/tool/usage/index',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找映射关系
|
||||||
|
const mappedComponent = componentMapping[componentName] || componentName
|
||||||
|
|
||||||
|
// 尝试多种路径加载组件
|
||||||
|
let component = null
|
||||||
|
const possiblePaths = [
|
||||||
|
`/src/views/${mappedComponent}.vue`,
|
||||||
|
`/src/views/${mappedComponent}/index.vue`,
|
||||||
|
`/src/views/${componentName}.vue`,
|
||||||
|
`/src/views/${componentName}/index.vue`,
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const path of possiblePaths) {
|
||||||
|
if (modules[path]) {
|
||||||
|
component = modules[path]
|
||||||
|
console.log(`[动态路由] 找到组件: ${path}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[动态路由] 处理: path=${menuPath}, componentName=${componentName}, mappedComponent=${mappedComponent}, 组件存在=${!!component}`)
|
||||||
|
|
||||||
if (component) {
|
if (component) {
|
||||||
router.addRoute('Layout', {
|
router.addRoute('Layout', {
|
||||||
@ -406,9 +340,9 @@ export function addDynamicRoutes(menus: any) { //MenuInfo[]
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
addedRouteNames.add(routeName)
|
addedRouteNames.add(routeName)
|
||||||
//console.log(`[动态路由] ✓ 添加成功: ${menuPath}`)
|
console.log(`[动态路由] ✓ 添加成功: ${menuPath}`)
|
||||||
} else {
|
} else {
|
||||||
//console.warn(`[动态路由] ✗ 组件不存在: ${componentPath}`)
|
console.warn(`[动态路由] ✗ 组件不存在: ${componentName}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -421,7 +355,8 @@ export function addDynamicRoutes(menus: any) { //MenuInfo[]
|
|||||||
}
|
}
|
||||||
|
|
||||||
addRoutes(menus)
|
addRoutes(menus)
|
||||||
//console.log('[动态路由] 当前所有路由:', router.getRoutes().map(r => r.path))
|
dynamicRoutesInitialized = true
|
||||||
|
console.log('[动态路由] 当前所有路由:', router.getRoutes().map(r => r.path))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -434,15 +369,16 @@ export function resetRouter() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
addedRouteNames.clear()
|
addedRouteNames.clear()
|
||||||
|
dynamicRoutesInitialized = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 路由守卫
|
// 路由守卫
|
||||||
router.beforeEach(async (to, _from, next) => {
|
router.beforeEach(async (to, _from, next) => {
|
||||||
console.log('路由测试');
|
const { useUserStore } = await import('@/stores/user')
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
//document.title = `${to.meta.title || ''} - CSY Admin`
|
document.title = `${to.meta.title || ''} - mes Admin`
|
||||||
|
|
||||||
if (to.meta.requiresAuth === false) {
|
if (to.meta.requiresAuth === false) {
|
||||||
next()
|
next()
|
||||||
return
|
return
|
||||||
@ -453,47 +389,25 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!userStore.user || !dynamicRoutesInitialized) {
|
||||||
if (userStore.init) {
|
|
||||||
try {
|
try {
|
||||||
|
if (!userStore.user) {
|
||||||
await userStore.getInfo()
|
await userStore.getInfo()
|
||||||
|
}
|
||||||
// 添加动态路由(只添加新的,不影响已有的)
|
// 添加动态路由(只添加新的,不影响已有的)
|
||||||
addDynamicRoutes(userStore.menus)
|
addDynamicRoutes(userStore.menus)
|
||||||
userStore.setinit(false)
|
console.log('[路由守卫] 动态路由已添加,重新导航到:', to.path)
|
||||||
// userStore.init = false
|
|
||||||
next({ ...to, replace: true })
|
next({ ...to, replace: true })
|
||||||
//return
|
return
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[路由守卫] 获取用户信息失败:', error)
|
||||||
userStore.logout()
|
userStore.logout()
|
||||||
//userStore.init = false
|
|
||||||
userStore.setinit(true)
|
|
||||||
next({ name: 'Login' })
|
next({ name: 'Login' })
|
||||||
//return
|
return
|
||||||
}
|
}
|
||||||
}else{
|
|
||||||
next()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (!userStore.user) {
|
next()
|
||||||
// try {
|
|
||||||
// await userStore.getInfo()
|
|
||||||
// // 添加动态路由(只添加新的,不影响已有的)
|
|
||||||
// addDynamicRoutes(userStore.menus)
|
|
||||||
// next({ ...to, replace: true })
|
|
||||||
// return
|
|
||||||
// } catch (error) {
|
|
||||||
// userStore.logout()
|
|
||||||
// next({ name: 'Login' })
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (to.meta.requiresAuth === false) {
|
|
||||||
// next()
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { authApi, type LoginParams, type UserInfo, type MenuInfo } from '@/api/auth'
|
import { authApi, type LoginParams, type UserInfo, type MenuInfo } from '@/api/auth'
|
||||||
import router, { resetRouter } from '@/router'
|
|
||||||
|
|
||||||
export const useUserStore = defineStore('user', () => {
|
export const useUserStore = defineStore('user', () => {
|
||||||
// 状态
|
// 状态
|
||||||
@ -11,21 +10,11 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
const permissions = ref<string[]>([])
|
const permissions = ref<string[]>([])
|
||||||
const menus = ref<MenuInfo[]>([])
|
const menus = ref<MenuInfo[]>([])
|
||||||
|
|
||||||
const init = ref<any>(true)
|
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const isLogin = computed(() => !!token.value)
|
const isLogin = computed(() => !!token.value)
|
||||||
const nickname = computed(() => user.value?.nickname || user.value?.username || '')
|
const nickname = computed(() => user.value?.nickname || user.value?.username || '')
|
||||||
const avatar = computed(() => user.value?.avatar || '')
|
const avatar = computed(() => user.value?.avatar || '')
|
||||||
|
|
||||||
const postNames = computed(() => user.value?.postNames || '')
|
|
||||||
|
|
||||||
const userid = computed(() => user.value?.id || '')
|
|
||||||
|
|
||||||
function setinit(v:any) {
|
|
||||||
init.value = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// 登录
|
// 登录
|
||||||
async function login(params: LoginParams) {
|
async function login(params: LoginParams) {
|
||||||
const res = await authApi.login(params)
|
const res = await authApi.login(params)
|
||||||
@ -56,11 +45,6 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
permissions.value = []
|
permissions.value = []
|
||||||
menus.value = []
|
menus.value = []
|
||||||
|
|
||||||
init.value = true
|
|
||||||
|
|
||||||
// 重置路由
|
|
||||||
resetRouter()
|
|
||||||
|
|
||||||
// 只有之前有 token 时才发送 logout 请求
|
// 只有之前有 token 时才发送 logout 请求
|
||||||
if (hadToken) {
|
if (hadToken) {
|
||||||
try {
|
try {
|
||||||
@ -70,6 +54,9 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 动态导入 router 避免循环依赖
|
||||||
|
const { default: router, resetRouter } = await import('@/router')
|
||||||
|
resetRouter()
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,10 +82,6 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
isLogin,
|
isLogin,
|
||||||
nickname,
|
nickname,
|
||||||
avatar,
|
avatar,
|
||||||
init,
|
|
||||||
postNames,
|
|
||||||
userid,
|
|
||||||
setinit,
|
|
||||||
login,
|
login,
|
||||||
getInfo,
|
getInfo,
|
||||||
logout,
|
logout,
|
||||||
@ -107,7 +90,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
persist: {
|
persist: {
|
||||||
key: 'mars-user',
|
key: 'mes-user',
|
||||||
paths: ['token']
|
paths: ['token']
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
|
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
|
||||||
import { useUserStore } from '@/stores/user'
|
|
||||||
|
|
||||||
const BASE_API = import.meta.env.VITE_APP_BASE_API
|
|
||||||
|
|
||||||
// API响应结构
|
// API响应结构
|
||||||
interface ApiResponse<T = any> {
|
interface ApiResponse<T = any> {
|
||||||
@ -14,12 +11,24 @@ interface ApiResponse<T = any> {
|
|||||||
interface CryptoConfig {
|
interface CryptoConfig {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
publicKey: string
|
publicKey: string
|
||||||
aesKey: string // AES密钥的Base64编码
|
aesKey: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加密配置缓存
|
// 加密配置缓存
|
||||||
let cryptoConfigCache: CryptoConfig | null = null
|
let cryptoConfigCache: CryptoConfig | null = null
|
||||||
|
|
||||||
|
// 从 localStorage 安全读取 token(避免循环依赖)
|
||||||
|
function getToken(): string | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem('mes-user')
|
||||||
|
if (raw) {
|
||||||
|
const data = JSON.parse(raw)
|
||||||
|
return data?.token || null
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
// 获取加密配置
|
// 获取加密配置
|
||||||
export async function fetchCryptoConfig(): Promise<CryptoConfig> {
|
export async function fetchCryptoConfig(): Promise<CryptoConfig> {
|
||||||
if (cryptoConfigCache) {
|
if (cryptoConfigCache) {
|
||||||
@ -42,7 +51,7 @@ export function clearCryptoConfigCache() {
|
|||||||
cryptoConfigCache = null
|
cryptoConfigCache = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否是AES加密的响应数据(格式:iv.encryptedData)
|
// 判断是否是AES加密的响应数据
|
||||||
function isAesEncryptedData(data: any): boolean {
|
function isAesEncryptedData(data: any): boolean {
|
||||||
if (typeof data !== 'string') {
|
if (typeof data !== 'string') {
|
||||||
return false
|
return false
|
||||||
@ -51,11 +60,9 @@ function isAesEncryptedData(data: any): boolean {
|
|||||||
if (parts.length !== 2) {
|
if (parts.length !== 2) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// 检查两部分是否都是有效的Base64,且IV长度正确
|
|
||||||
try {
|
try {
|
||||||
atob(parts[0])
|
atob(parts[0])
|
||||||
atob(parts[1])
|
atob(parts[1])
|
||||||
// IV是12字节,Base64后是16字符
|
|
||||||
return parts[0].length === 16 && parts[1].length > 10
|
return parts[0].length === 16 && parts[1].length > 10
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
@ -73,7 +80,6 @@ async function aesDecrypt(encryptedData: string, aesKeyBase64: string): Promise<
|
|||||||
const data = Uint8Array.from(atob(parts[1]), c => c.charCodeAt(0))
|
const data = Uint8Array.from(atob(parts[1]), c => c.charCodeAt(0))
|
||||||
const keyBytes = Uint8Array.from(atob(aesKeyBase64), c => c.charCodeAt(0))
|
const keyBytes = Uint8Array.from(atob(aesKeyBase64), c => c.charCodeAt(0))
|
||||||
|
|
||||||
// 导入AES密钥
|
|
||||||
const aesKey = await crypto.subtle.importKey(
|
const aesKey = await crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
keyBytes,
|
keyBytes,
|
||||||
@ -82,7 +88,6 @@ async function aesDecrypt(encryptedData: string, aesKeyBase64: string): Promise<
|
|||||||
['decrypt']
|
['decrypt']
|
||||||
)
|
)
|
||||||
|
|
||||||
// 解密
|
|
||||||
const decrypted = await crypto.subtle.decrypt(
|
const decrypted = await crypto.subtle.decrypt(
|
||||||
{ name: 'AES-GCM', iv: iv },
|
{ name: 'AES-GCM', iv: iv },
|
||||||
aesKey,
|
aesKey,
|
||||||
@ -105,37 +110,24 @@ async function decryptResponseData(data: string): Promise<any> {
|
|||||||
return JSON.parse(decryptedStr)
|
return JSON.parse(decryptedStr)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('响应解密失败', error)
|
console.error('响应解密失败', error)
|
||||||
// 解密失败可能是密钥过期,清除缓存
|
|
||||||
cryptoConfigCache = null
|
cryptoConfigCache = null
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//axios.defaults.headers["Content-Type"] = 'application/json;charset=utf-8' //"application/x-www-form-urlencoded;charset=utf-8"; //"application/json;charset=utf-8";
|
|
||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
// 后端 API 统一使用 /api 前缀
|
|
||||||
const service: AxiosInstance = axios.create({
|
const service: AxiosInstance = axios.create({
|
||||||
//baseURL:'/api',
|
baseURL: '/api',
|
||||||
baseURL:BASE_API,
|
|
||||||
timeout: 30000
|
timeout: 30000
|
||||||
})
|
})
|
||||||
|
|
||||||
// 请求拦截器
|
// 请求拦截器 - 直接从 localStorage 读取 token
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
const userStore = useUserStore()
|
const token = getToken()
|
||||||
if (userStore.token) {
|
if (token) {
|
||||||
config.headers['Authorization'] = userStore.token
|
config.headers['Authorization'] = token
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.data instanceof FormData) {
|
|
||||||
// 文件上传:不设置 JSON 请求头、不序列化,交给浏览器自动处理 multipart
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
config.headers["Content-Type"] = 'application/json;charset=utf-8'
|
|
||||||
config.data = config.data instanceof Object ? JSON.stringify(config.data) : config.data
|
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@ -149,31 +141,25 @@ let isLoggingOut = false
|
|||||||
// 响应拦截器
|
// 响应拦截器
|
||||||
service.interceptors.response.use(
|
service.interceptors.response.use(
|
||||||
async (response: AxiosResponse<ApiResponse>) => {
|
async (response: AxiosResponse<ApiResponse>) => {
|
||||||
// 如果是 blob 类型响应(文件下载),直接返回
|
// blob 类型响应直接返回
|
||||||
if (response.config.responseType === 'blob') {
|
if (response.config.responseType === 'blob') {
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.config.responseType === 'arraybuffer') {
|
|
||||||
// 二进制流不走code校验,直接完整返回ArrayBuffer
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = response.data
|
const res = response.data
|
||||||
|
|
||||||
if (res.code !== 200) {
|
if (res.code !== 200) {
|
||||||
// logout 接口返回 401 时不显示错误消息(避免干扰)
|
|
||||||
const isLogoutRequest = response.config.url?.includes('/auth/logout')
|
const isLogoutRequest = response.config.url?.includes('/auth/logout')
|
||||||
|
|
||||||
// 401 未授权,弹出明确提示并跳转登录(防止重复调用)
|
// 401 未授权
|
||||||
if (res.code === 401 && !isLoggingOut && !isLogoutRequest) {
|
if (res.code === 401 && !isLoggingOut && !isLogoutRequest) {
|
||||||
window.$message?.warning('当前用户登录已过期,请重新登录')
|
|
||||||
|
|
||||||
isLoggingOut = true
|
isLoggingOut = true
|
||||||
|
window.$message?.error('当前用户登录已过期,请重新登录')
|
||||||
const userStore = useUserStore()
|
// 清除本地 token
|
||||||
await userStore.logout()
|
localStorage.removeItem('mes-user')
|
||||||
isLoggingOut = false
|
isLoggingOut = false
|
||||||
|
// 跳转到登录页
|
||||||
|
window.location.href = '/login'
|
||||||
return Promise.reject(new Error('登录已过期'))
|
return Promise.reject(new Error('登录已过期'))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +170,7 @@ service.interceptors.response.use(
|
|||||||
return Promise.reject(new Error(res.message || '请求失败'))
|
return Promise.reject(new Error(res.message || '请求失败'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查响应数据是否是AES加密的,自动解密
|
// AES 解密
|
||||||
if (isAesEncryptedData(res.data)) {
|
if (isAesEncryptedData(res.data)) {
|
||||||
try {
|
try {
|
||||||
return await decryptResponseData(res.data)
|
return await decryptResponseData(res.data)
|
||||||
|
|||||||
@ -4,25 +4,28 @@
|
|||||||
<div class="search-form">
|
<div class="search-form">
|
||||||
<n-form inline :model="searchForm" label-placement="left">
|
<n-form inline :model="searchForm" label-placement="left">
|
||||||
<n-form-item label="刀具编码">
|
<n-form-item label="刀具编码">
|
||||||
<n-input v-model:value="searchForm.toolCode" placeholder="请输入刀具编码" clearable />
|
<n-input v-model:value="searchForm.toolCode" placeholder="请输入刀具编码" clearable style="width: 140px" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="刀具名称">
|
<n-form-item label="刀具名称">
|
||||||
<n-input v-model:value="searchForm.toolName" placeholder="请输入刀具名称" clearable />
|
<n-input v-model:value="searchForm.toolName" placeholder="请输入刀具名称" clearable style="width: 140px" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="刀具状态">
|
<n-form-item label="刀具类型">
|
||||||
<n-select v-model:value="searchForm.status" placeholder="请选择刀具状态" clearable style="width: 150px" :options="statusOptions" />
|
<n-select v-model:value="searchForm.toolType" placeholder="请选择刀具类型" :options="toolTypeOptions" clearable style="width: 140px" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="品牌">
|
||||||
|
<n-input v-model:value="searchForm.brand" placeholder="请输入品牌" clearable style="width: 140px" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item>
|
<n-form-item>
|
||||||
<n-space>
|
<n-button type="primary" @click="handleSearch">
|
||||||
<n-button type="primary" @click="handleSearch">
|
<template #icon><n-icon><SearchOutline /></n-icon></template>
|
||||||
<template #icon><n-icon><SearchOutline /></n-icon></template>
|
搜索
|
||||||
搜索
|
</n-button>
|
||||||
</n-button>
|
</n-form-item>
|
||||||
<n-button @click="handleReset">
|
<n-form-item>
|
||||||
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
<n-button @click="handleReset">
|
||||||
重置
|
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
||||||
</n-button>
|
重置
|
||||||
</n-space>
|
</n-button>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</div>
|
</div>
|
||||||
@ -52,51 +55,31 @@
|
|||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="pagination"
|
|
||||||
:row-key="(row) => row.id"
|
:row-key="(row) => row.id"
|
||||||
:scroll-x="1400"
|
:scroll-x="1200"
|
||||||
@update:page="handlePageChange"
|
|
||||||
@update:page-size="handlePageSizeChange"
|
|
||||||
@update:checked-row-keys="handleCheck"
|
@update:checked-row-keys="handleCheck"
|
||||||
/>
|
/>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 700px">
|
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 600px">
|
||||||
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="110px">
|
<n-form ref="formRef" :model="formData" label-placement="left" label-width="100px">
|
||||||
<n-grid :cols="2" :x-gap="24">
|
<n-form-item label="刀具编码" path="toolCode">
|
||||||
<n-form-item-gi label="刀具编码" path="toolCode">
|
<n-input v-model:value="formData.toolCode" placeholder="请输入刀具编码" />
|
||||||
<n-input v-model:value="formData.toolCode" placeholder="请输入刀具编码" />
|
</n-form-item>
|
||||||
</n-form-item-gi>
|
<n-form-item label="刀具名称" path="toolName">
|
||||||
<n-form-item-gi label="刀具名称" path="toolName">
|
<n-input v-model:value="formData.toolName" placeholder="请输入刀具名称" />
|
||||||
<n-input v-model:value="formData.toolName" placeholder="请输入刀具名称" />
|
</n-form-item>
|
||||||
</n-form-item-gi>
|
<n-form-item label="刀具类型" path="toolType">
|
||||||
<n-form-item-gi label="刀具类型" path="toolType">
|
<n-select v-model:value="formData.toolType" placeholder="请选择刀具类型" :options="toolTypeOptions" />
|
||||||
<n-input v-model:value="formData.toolType" placeholder="请输入刀具类型" />
|
</n-form-item>
|
||||||
</n-form-item-gi>
|
<n-form-item label="规格型号" path="spec">
|
||||||
<n-form-item-gi label="规格型号" path="specModel">
|
<n-input v-model:value="formData.spec" placeholder="请输入规格型号" />
|
||||||
<n-input v-model:value="formData.specModel" placeholder="请输入规格型号" />
|
</n-form-item>
|
||||||
</n-form-item-gi>
|
<n-form-item label="品牌" path="brand">
|
||||||
<n-form-item-gi label="材质" path="material">
|
<n-input v-model:value="formData.brand" placeholder="请输入品牌" />
|
||||||
<n-input v-model:value="formData.material" placeholder="请输入材质" />
|
</n-form-item>
|
||||||
</n-form-item-gi>
|
<n-form-item label="仓库编码" path="warehouseCode">
|
||||||
<n-form-item-gi label="生产厂家" path="manufacturer">
|
<n-input v-model:value="formData.warehouseCode" placeholder="请输入仓库编码" />
|
||||||
<n-input v-model:value="formData.manufacturer" placeholder="请输入生产厂家" />
|
|
||||||
</n-form-item-gi>
|
|
||||||
<n-form-item-gi label="采购日期" path="purchaseDate">
|
|
||||||
<n-date-picker v-model:value="formData.purchaseDate" type="datetime" clearable style="width: 100%" />
|
|
||||||
</n-form-item-gi>
|
|
||||||
<n-form-item-gi label="生命周期" path="lifeCycle">
|
|
||||||
<n-input-number v-model:value="formData.lifeCycle" placeholder="请输入生命周期" :min="0" :show-button="false" style="width: 100%" />
|
|
||||||
</n-form-item-gi>
|
|
||||||
<n-form-item-gi label="刀具状态" path="status">
|
|
||||||
<n-select v-model:value="formData.status" placeholder="请选择刀具状态" style="width: 100%" :options="statusOptions" />
|
|
||||||
</n-form-item-gi>
|
|
||||||
<n-form-item-gi label="已使用次数" path="usedCount">
|
|
||||||
<n-input-number v-model:value="formData.usedCount" placeholder="已使用次数" :min="0" :show-button="false" style="width: 100%" />
|
|
||||||
</n-form-item-gi>
|
|
||||||
</n-grid>
|
|
||||||
<n-form-item label="备注" path="remark">
|
|
||||||
<n-input v-model:value="formData.remark" type="textarea" placeholder="请输入备注" :rows="3" />
|
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -107,7 +90,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|
||||||
<n-modal v-model:show="importModalVisible" preset="card" title="导入刀具表" style="width: 500px">
|
<n-modal v-model:show="importModalVisible" preset="card" title="导入刀具基础信息" style="width: 500px">
|
||||||
<n-space vertical>
|
<n-space vertical>
|
||||||
<n-alert type="info">
|
<n-alert type="info">
|
||||||
<template #header>导入说明</template>
|
<template #header>导入说明</template>
|
||||||
@ -141,10 +124,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, h, onMounted } from 'vue'
|
import { ref, reactive, h, onMounted } from 'vue'
|
||||||
import { NButton, NSpace, NIcon, NTag, NDataTable, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui'
|
import { NButton, NSpace, NIcon, NInput, NSelect, NForm, NFormItem, NDataTable, NCard, NModal, NTag, NUpload, NUploadDragger, NAlert, useMessage, useDialog, type UploadCustomRequestOptions } from 'naive-ui'
|
||||||
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline } from '@vicons/ionicons5'
|
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, DownloadOutline, CloudUploadOutline } from '@vicons/ionicons5'
|
||||||
import { toolApi, type Tool } from '@/api/tool'
|
import { toolApi } from '@/api/tool'
|
||||||
import { dictDataApi } from '@/api/org'
|
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
@ -152,80 +134,79 @@ const dialog = useDialog()
|
|||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
toolCode: '',
|
toolCode: '',
|
||||||
toolName: '',
|
toolName: '',
|
||||||
status: null as number | null
|
toolType: undefined as number | undefined,
|
||||||
|
brand: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const tableData = ref<Tool[]>([])
|
const tableData = ref<any[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const selectedIds = ref<number[]>([])
|
const selectedIds = ref<number[]>([])
|
||||||
const pagination = reactive({
|
|
||||||
page: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
itemCount: 0,
|
|
||||||
showSizePicker: true,
|
|
||||||
pageSizes: [10, 20, 50]
|
|
||||||
})
|
|
||||||
|
|
||||||
const modalVisible = ref(false)
|
const modalVisible = ref(false)
|
||||||
const modalTitle = ref('')
|
const modalTitle = ref('')
|
||||||
const importModalVisible = ref(false)
|
const importModalVisible = ref(false)
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
|
|
||||||
const defaultFormData: Tool = {
|
const defaultFormData = {
|
||||||
|
id: undefined as number | undefined,
|
||||||
toolCode: '',
|
toolCode: '',
|
||||||
toolName: '',
|
toolName: '',
|
||||||
toolType: '',
|
toolType: undefined as number | undefined,
|
||||||
specModel: '',
|
spec: '',
|
||||||
material: '',
|
brand: '',
|
||||||
manufacturer: '',
|
warehouseCode: ''
|
||||||
purchaseDate: undefined,
|
|
||||||
lifeCycle: 0,
|
|
||||||
usedCount: 0,
|
|
||||||
status: 0,
|
|
||||||
remark: ''
|
|
||||||
}
|
|
||||||
const formData = reactive<Tool>({ ...defaultFormData })
|
|
||||||
|
|
||||||
const statusOptions = ref<{ label: string; value: any }[]>([])
|
|
||||||
|
|
||||||
const formRules = {
|
|
||||||
toolCode: [{ required: true, message: '请输入刀具编码', trigger: 'blur' }],
|
|
||||||
toolName: [{ required: true, message: '请输入刀具名称', trigger: 'blur' }],
|
|
||||||
lifeCycle: [{ required: true, message: '请输入生命周期', trigger: 'blur' }],
|
|
||||||
status: [{ required: true, message: '请选择刀具状态', trigger: 'change' }]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: DataTableColumns<Tool> = [
|
const formData = reactive({ ...defaultFormData })
|
||||||
{ type: 'selection', width: 48, fixed: 'left' },
|
|
||||||
{ title: '刀具编码', key: 'toolCode', width: 120, align: 'center', fixed: 'left' },
|
const toolTypeMap: Record<number, string> = {
|
||||||
{ title: '刀具名称', key: 'toolName', width: 140, align: 'center' },
|
1: '钻头',
|
||||||
{ title: '刀具类型', key: 'toolType', width: 120, align: 'center' },
|
2: '铣刀',
|
||||||
{ title: '规格型号', key: 'specModel', width: 140, align: 'center' },
|
3: '砂轮/研磨',
|
||||||
{ title: '材质', key: 'material', width: 100, align: 'center' },
|
4: '丝锥',
|
||||||
{ title: '生产厂家', key: 'manufacturer', width: 120, align: 'center' },
|
5: '车刀',
|
||||||
{ title: '采购日期', key: 'purchaseDate', width: 170, align: 'center' },
|
6: '锯片',
|
||||||
{ title: '生命周期', key: 'lifeCycle', width: 100, align: 'center' },
|
7: '滚齿刀'
|
||||||
{ title: '已使用次数', key: 'usedCount', width: 100, align: 'center',
|
}
|
||||||
render(row) {
|
|
||||||
return row.usedCount ?? '-'
|
const getToolTypeName = (type: number) => {
|
||||||
|
return toolTypeMap[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolTypeOptions = [
|
||||||
|
{ label: '钻头', value: 1 },
|
||||||
|
{ label: '铣刀', value: 2 },
|
||||||
|
{ label: '砂轮/研磨', value: 3 },
|
||||||
|
{ label: '丝锥', value: 4 },
|
||||||
|
{ label: '车刀', value: 5 },
|
||||||
|
{ label: '锯片', value: 6 },
|
||||||
|
{ label: '滚齿刀', value: 7 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ type: 'selection' },
|
||||||
|
{ title: '序号', key: 'index', width: 60, render: (_row: any, index: number) => index + 1 },
|
||||||
|
{ title: '刀具编码', key: 'toolCode', width: 120 },
|
||||||
|
{ title: '刀具名称', key: 'toolName', width: 150 },
|
||||||
|
{
|
||||||
|
title: '刀具类型',
|
||||||
|
key: 'toolType',
|
||||||
|
width: 100,
|
||||||
|
render(row: any) {
|
||||||
|
return h(NTag, { type: 'info' }, () => getToolTypeName(row.toolType))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ title: '刀具状态', key: 'status', width: 100, align: 'center',
|
{ title: '规格型号', key: 'spec', width: 120 },
|
||||||
render(row) {
|
{ title: '品牌', key: 'brand', width: 120 },
|
||||||
const val = row.status
|
{ title: '仓库编码', key: 'warehouseCode', width: 120 },
|
||||||
const opt = statusOptions.value.find(o => o.value === val || String(o.value) === String(val))
|
{ title: '总寿命(小时)', key: 'lifeHour', width: 120 },
|
||||||
if (!opt) return val ?? '-'
|
{ title: '已使用(小时)', key: 'currentUseHour', width: 120 },
|
||||||
return h(NTag, { type: opt.value === 1 ? 'success' : 'default', size: 'small' }, { default: () => opt.label })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ title: '备注', key: 'remark', width: 150, ellipsis: { tooltip: true } },
|
|
||||||
{ title: '创建时间', key: 'createTime', width: 180, align: 'center' },
|
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
width: 140,
|
width: 140,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render(row) {
|
render(row: any) {
|
||||||
return h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'nowrap' } }, [
|
return h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'nowrap' } }, [
|
||||||
h(NButton, { size: 'small', quaternary: true, onClick: () => handleEdit(row) }, {
|
h(NButton, { size: 'small', quaternary: true, onClick: () => handleEdit(row) }, {
|
||||||
default: () => [h(NIcon, null, { default: () => h(CreateOutline) }), ' 编辑']
|
default: () => [h(NIcon, null, { default: () => h(CreateOutline) }), ' 编辑']
|
||||||
@ -242,42 +223,35 @@ async function loadData() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await toolApi.page({
|
const res = await toolApi.page({
|
||||||
page: pagination.page,
|
page: 1,
|
||||||
pageSize: pagination.pageSize,
|
pageSize: 1000,
|
||||||
toolCode: searchForm.toolCode || undefined,
|
toolCode: searchForm.toolCode || undefined,
|
||||||
toolName: searchForm.toolName || undefined,
|
toolName: searchForm.toolName || undefined,
|
||||||
status: searchForm.status || undefined
|
toolType: searchForm.toolType,
|
||||||
|
brand: searchForm.brand || undefined
|
||||||
})
|
})
|
||||||
tableData.value = res.list
|
tableData.value = res.list || []
|
||||||
pagination.itemCount = res.total
|
} catch (error: any) {
|
||||||
|
console.error('加载数据失败:', error)
|
||||||
|
tableData.value = []
|
||||||
|
message.error(error?.message || '加载数据失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
pagination.page = 1
|
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
searchForm.toolCode = ''
|
searchForm.toolCode = ''
|
||||||
searchForm.toolName = ''
|
searchForm.toolName = ''
|
||||||
searchForm.status = null
|
searchForm.toolType = undefined
|
||||||
|
searchForm.brand = ''
|
||||||
handleSearch()
|
handleSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePageChange(page: number) {
|
|
||||||
pagination.page = page
|
|
||||||
loadData()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePageSizeChange(pageSize: number) {
|
|
||||||
pagination.pageSize = pageSize
|
|
||||||
pagination.page = 1
|
|
||||||
loadData()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCheck(keys: Array<string | number>) {
|
function handleCheck(keys: Array<string | number>) {
|
||||||
selectedIds.value = keys as number[]
|
selectedIds.value = keys as number[]
|
||||||
}
|
}
|
||||||
@ -288,59 +262,40 @@ function handleAdd() {
|
|||||||
modalVisible.value = true
|
modalVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEdit(row: Tool) {
|
function handleEdit(row: any) {
|
||||||
modalTitle.value = '编辑刀具'
|
modalTitle.value = '编辑刀具'
|
||||||
Object.assign(formData, row)
|
Object.assign(formData, row)
|
||||||
if (typeof row.purchaseDate === 'string') {
|
|
||||||
formData.purchaseDate = new Date(row.purchaseDate.replace(' ', 'T')).getTime()
|
|
||||||
}
|
|
||||||
modalVisible.value = true
|
modalVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateTime(ts: number) {
|
|
||||||
const d = new Date(ts)
|
|
||||||
const pad = (n: number) => String(n).padStart(2, '0')
|
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
|
await formRef.value?.validate()
|
||||||
try {
|
try {
|
||||||
await formRef.value?.validate()
|
if (formData.id) {
|
||||||
} catch {
|
await toolApi.update(formData)
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const submitData = { ...formData } as Tool
|
|
||||||
if (typeof submitData.purchaseDate === 'number') {
|
|
||||||
submitData.purchaseDate = formatDateTime(submitData.purchaseDate)
|
|
||||||
}
|
|
||||||
if (submitData.id) {
|
|
||||||
await toolApi.update(submitData)
|
|
||||||
message.success('修改成功')
|
message.success('修改成功')
|
||||||
} else {
|
} else {
|
||||||
await toolApi.create(submitData)
|
await toolApi.create(formData)
|
||||||
message.success('新增成功')
|
message.success('新增成功')
|
||||||
}
|
}
|
||||||
modalVisible.value = false
|
modalVisible.value = false
|
||||||
loadData()
|
loadData()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 错误已在拦截器处理
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDelete(row: Tool) {
|
function handleDelete(row: any) {
|
||||||
dialog.warning({
|
dialog.warning({
|
||||||
title: '提示',
|
title: '提示',
|
||||||
content: '确定要删除该记录吗?',
|
content: `确定要删除刀具「${row.toolName}」吗?`,
|
||||||
positiveText: '确定',
|
positiveText: '确定',
|
||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
onPositiveClick: async () => {
|
onPositiveClick: async () => {
|
||||||
try {
|
try {
|
||||||
await toolApi.delete([row.id!])
|
await toolApi.delete([row.id])
|
||||||
message.success('删除成功')
|
message.success('删除成功')
|
||||||
loadData()
|
loadData()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 错误已在拦截器处理
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -359,7 +314,6 @@ function handleBatchDelete() {
|
|||||||
selectedIds.value = []
|
selectedIds.value = []
|
||||||
loadData()
|
loadData()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 错误已在拦截器处理
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -367,41 +321,37 @@ function handleBatchDelete() {
|
|||||||
|
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
try {
|
try {
|
||||||
const params: Record<string, any> = {}
|
const res = await toolApi.export({
|
||||||
if (selectedIds.value.length > 0) params.ids = selectedIds.value
|
toolCode: searchForm.toolCode || undefined,
|
||||||
if (searchForm.toolCode) params.toolCode = searchForm.toolCode
|
toolName: searchForm.toolName || undefined
|
||||||
if (searchForm.toolName) params.toolName = searchForm.toolName
|
})
|
||||||
if (searchForm.status != null) params.status = searchForm.status
|
const url = window.URL.createObjectURL(res)
|
||||||
const blob = await toolApi.export(params)
|
const a = document.createElement('a')
|
||||||
const url = window.URL.createObjectURL(blob)
|
a.href = url
|
||||||
const link = document.createElement('a')
|
a.download = '刀具基础信息数据.xlsx'
|
||||||
link.href = url
|
a.click()
|
||||||
link.download = '刀具数据.xlsx'
|
|
||||||
link.click()
|
|
||||||
window.URL.revokeObjectURL(url)
|
window.URL.revokeObjectURL(url)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 错误已在拦截器处理
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDownloadTemplate() {
|
async function handleDownloadTemplate() {
|
||||||
try {
|
try {
|
||||||
const blob = await toolApi.export({})
|
const blob = await toolApi.downloadTemplate()
|
||||||
const url = window.URL.createObjectURL(blob)
|
const url = window.URL.createObjectURL(blob)
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
link.href = url
|
link.href = url
|
||||||
link.download = '刀具导入模板.xlsx'
|
link.download = '刀具基础信息导入模板.xlsx'
|
||||||
link.click()
|
link.click()
|
||||||
window.URL.revokeObjectURL(url)
|
window.URL.revokeObjectURL(url)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 错误已在拦截器处理
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleImportUpload({ file }: UploadCustomRequestOptions) {
|
async function handleImportUpload({ file }: UploadCustomRequestOptions) {
|
||||||
if (!file.file) return
|
if (!file.file) return
|
||||||
try {
|
try {
|
||||||
const result = await toolApi.export({})
|
const result = await toolApi.importData(file.file)
|
||||||
if (result.fail > 0) {
|
if (result.fail > 0) {
|
||||||
dialog.warning({
|
dialog.warning({
|
||||||
title: '导入结果',
|
title: '导入结果',
|
||||||
@ -414,29 +364,33 @@ async function handleImportUpload({ file }: UploadCustomRequestOptions) {
|
|||||||
}
|
}
|
||||||
loadData()
|
loadData()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 错误已在拦截器处理
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadDictOptions() {
|
|
||||||
try {
|
|
||||||
const data = await dictDataApi.listByType('sys_status')
|
|
||||||
statusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) }))
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadData()
|
loadData()
|
||||||
loadDictOptions()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.search-form {
|
.search-form {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.n-form-item) {
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.n-form-item-label) {
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.n-form-item-control) {
|
||||||
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-toolbar {
|
.table-toolbar {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -1,253 +1,337 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="margin:15px 10px;">
|
<div class="page-container">
|
||||||
<n-grid x-gap="20">
|
<!-- 欢迎区域 -->
|
||||||
<n-gi :span="19">
|
<div class="welcome-section">
|
||||||
<n-grid x-gap="20">
|
<!-- 左侧欢迎信息 -->
|
||||||
<n-gi :span="3" style="padding:10px;border-radius: 10px;box-shadow: 0 0 20px -5px rgba(84, 151, 232, 0.5);background: #fff;">
|
<div class="welcome-info">
|
||||||
<n-button type="success" style="width: 100%;margin:10px 0 30px;">今天</n-button>
|
<div class="welcome-header">
|
||||||
<n-button type="tertiary" style="width: 100%; margin-bottom: 30px;">昨天</n-button>
|
<n-avatar round :size="56" :src="userStore.avatar || undefined">
|
||||||
<n-button type="tertiary" style="width: 100%; margin-bottom: 30px;">近7天</n-button>
|
{{ userStore.nickname?.charAt(0) || 'U' }}
|
||||||
<n-button type="tertiary" style="width: 100%; margin-bottom: 20px;">近30天</n-button>
|
</n-avatar>
|
||||||
</n-gi>
|
<div class="welcome-text">
|
||||||
<n-gi :span="21">
|
<h1 class="welcome-title">
|
||||||
|
{{ getGreeting() }},{{ userStore.nickname }} 👋
|
||||||
<n-grid x-gap="20">
|
</h1>
|
||||||
<n-gi :span="8" style="display: flex;align-items: center;justify-content: space-between;padding:10px;border-radius: 10px;box-shadow: 0 0 20px -5px rgba(84, 151, 232, 0.5);background: #fff;">
|
<p class="welcome-desc">
|
||||||
<div>
|
这是您的管理控制台,您可以在这里管理系统的各项功能
|
||||||
<div style="display: flex;align-items: center;font-size: 20px;color: #1890ff;">
|
</p>
|
||||||
<n-icon size="20">
|
</div>
|
||||||
<BarChartOutline />
|
|
||||||
</n-icon>
|
|
||||||
<div style="padding-left: 8px;">工单总数</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="color: #1890ff;">
|
|
||||||
<span style="font-size: 36px;font-weight: bold;">890</span>
|
|
||||||
<span style="padding-left: 5px;">单</span>
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 12px;">所选时间范围内全部工单</div>
|
|
||||||
</div>
|
|
||||||
<div style="padding-left: 20px;border-left:1px solid #e1e0e0;">
|
|
||||||
<div>已完成</div>
|
|
||||||
<div style="padding-bottom: 20px;color: #161718;">260单</div>
|
|
||||||
<div>进行中</div>
|
|
||||||
<div style="color: #161718;">5800单</div>
|
|
||||||
</div>
|
|
||||||
</n-gi>
|
|
||||||
<n-gi :span="8" style="display: flex;align-items: center;justify-content: space-between;padding:10px;border-radius: 10px;box-shadow: 0 0 20px -5px rgba(84, 151, 232, 0.5);background: #fff;">
|
|
||||||
<div>
|
|
||||||
<div style="display: flex;align-items: center;font-size: 20px;color: #0891b2;">
|
|
||||||
<n-icon size="20">
|
|
||||||
<BookmarksOutline />
|
|
||||||
</n-icon>
|
|
||||||
<div style="padding-left: 8px;">进行中工单</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="color: #0891b2;">
|
|
||||||
<span style="font-size: 36px;font-weight: bold;">560</span>
|
|
||||||
<span style="padding-left: 5px;">单</span>
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 12px;">当前正在执行的工单数里</div>
|
|
||||||
</div>
|
|
||||||
<div style="padding-left: 20px;border-left:1px solid #e1e0e0;">
|
|
||||||
<div>已完成</div>
|
|
||||||
<div style="padding-bottom: 20px;color: #161718;">260单</div>
|
|
||||||
<div>工单完成率</div>
|
|
||||||
<div style="color: #161718;">68%</div>
|
|
||||||
</div>
|
|
||||||
</n-gi>
|
|
||||||
<n-gi :span="8" style="display: flex;align-items: center;justify-content: space-between;padding:10px;border-radius: 10px;box-shadow: 0 0 20px -5px rgba(84, 151, 232, 0.5);background: #fff;">
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<div style="display: flex;align-items: center;font-size: 20px;color: #18181b;">
|
|
||||||
<n-icon size="20">
|
|
||||||
<CellularSharp />
|
|
||||||
</n-icon>
|
|
||||||
<div style="padding-left: 8px;">工单完成率</div>
|
|
||||||
</div>
|
|
||||||
<div style="color: #18181b;">
|
|
||||||
<span style="font-size: 36px;font-weight: bold;">65</span>
|
|
||||||
<span style="padding-left: 5px;">%</span>
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 12px;">按工单数量统计的完成比例</div>
|
|
||||||
</div>
|
|
||||||
<div style="padding-left: 20px;border-left:1px solid #e1e0e0;">
|
|
||||||
<div>已结工单</div>
|
|
||||||
<div style="padding-bottom: 20px;color: #161718;">260单</div>
|
|
||||||
<div>工单总数</div>
|
|
||||||
<div style="color: #161718;">5800单</div>
|
|
||||||
</div>
|
|
||||||
</n-gi>
|
|
||||||
<n-gi :span="8" style="display: flex;align-items: center;justify-content: space-between;margin-top: 20px;padding:10px;border-radius: 10px;box-shadow: 0 0 20px -5px rgba(84, 151, 232, 0.5);background: #fff;">
|
|
||||||
<div>
|
|
||||||
<div style="display: flex;align-items: center;font-size: 20px;color: #15803d;">
|
|
||||||
<n-icon size="22">
|
|
||||||
<CubeOutline />
|
|
||||||
</n-icon>
|
|
||||||
<div style="padding-left: 8px;">完工数量</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="color: #15803d;">
|
|
||||||
<span style="font-size: 36px;font-weight: bold;">560</span>
|
|
||||||
<span style="padding-left: 5px;">件</span>
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 12px;">所选时间范围内合格完工产量</div>
|
|
||||||
</div>
|
|
||||||
<div style="padding-left: 20px;border-left:1px solid #e1e0e0;">
|
|
||||||
<div>产能达成率</div>
|
|
||||||
<div style="padding-bottom: 20px;color: #161718;">26%</div>
|
|
||||||
<div>已结工单</div>
|
|
||||||
<div style="color: #161718;">46单</div>
|
|
||||||
</div>
|
|
||||||
</n-gi>
|
|
||||||
<n-gi :span="8" style="display: flex;align-items: center;justify-content: space-between;margin-top: 20px;padding:10px;border-radius: 10px;box-shadow: 0 0 20px -5px rgba(84, 151, 232, 0.5);background: #fff;">
|
|
||||||
<div>
|
|
||||||
<div style="display: flex;align-items: center;font-size: 20px;color: #ea580c;">
|
|
||||||
<n-icon size="22">
|
|
||||||
<ReaderOutline />
|
|
||||||
</n-icon>
|
|
||||||
<div style="padding-left: 8px;">库存预警</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="color: #ea580c;">
|
|
||||||
<span style="font-size: 36px;font-weight: bold;">89</span>
|
|
||||||
<span style="padding-left: 5px;">单</span>
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 12px;">需关注与处理的库存预警</div>
|
|
||||||
</div>
|
|
||||||
<div style="padding-left: 20px;border-left:1px solid #e1e0e0;">
|
|
||||||
<div>库存周转</div>
|
|
||||||
<div style="padding-bottom: 20px;color: #161718;">40%</div>
|
|
||||||
<div>库存总量</div>
|
|
||||||
<div style="color: #161718;">630</div>
|
|
||||||
</div>
|
|
||||||
</n-gi>
|
|
||||||
<n-gi :span="8" style="display: flex;align-items: center;justify-content: space-between;margin-top: 20px;padding:10px;border-radius: 10px;box-shadow: 0 0 20px -5px rgba(84, 151, 232, 0.5);background: #fff;">
|
|
||||||
<div>
|
|
||||||
<div style="display: flex;align-items: center;font-size: 20px;color: #8b8d07;">
|
|
||||||
<n-icon size="25">
|
|
||||||
<LayersOutline />
|
|
||||||
</n-icon>
|
|
||||||
<div style="padding-left: 8px;">质量概览</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div style="color: #8b8d07;">
|
|
||||||
<span style="font-size: 36px;font-weight: bold;">98</span>
|
|
||||||
<span style="padding-left: 5px;">%</span>
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 12px;">所选时间范围内质量表现</div>
|
|
||||||
</div>
|
|
||||||
<div style="padding-left: 20px;border-left:1px solid #e1e0e0;">
|
|
||||||
<div>未关闭异常</div>
|
|
||||||
<div style="padding-bottom: 20px;color: #161718;">0</div>
|
|
||||||
<div>累计异常</div>
|
|
||||||
<div style="color: #161718;">75</div>
|
|
||||||
</div>
|
|
||||||
</n-gi>
|
|
||||||
</n-grid>
|
|
||||||
</n-gi>
|
|
||||||
</n-grid>
|
|
||||||
|
|
||||||
</n-gi>
|
|
||||||
<n-gi :span="5" style="padding:10px;border-radius: 10px;box-shadow: 0 0 20px -5px rgba(84, 151, 232, 0.5);background: #fff;">
|
|
||||||
<div style="display: flex;align-items: center;font-size: 18px;color: #663706;">
|
|
||||||
<n-icon size="23">
|
|
||||||
<Contract />
|
|
||||||
</n-icon>
|
|
||||||
<div style="padding-left: 5px;">快捷入口</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="shortcuts-grid">
|
<div class="welcome-time">
|
||||||
<div
|
<div class="time-display">{{ currentTime }}</div>
|
||||||
v-for="n in list"
|
<div class="date-display">{{ currentDate }}</div>
|
||||||
:key="n.path"
|
</div>
|
||||||
class="shortcut-item"
|
</div>
|
||||||
@click="topage(n.path)"
|
<!-- 右侧轮播Banner -->
|
||||||
>
|
<div class="welcome-banner">
|
||||||
<div class="shortcut-icon" :style="{ background: n.bgColor }">
|
<n-carousel autoplay :interval="5000" dot-type="line" show-arrow="hover" class="banner-carousel">
|
||||||
<n-icon size="24" :color="n.color">
|
<div v-for="(banner, index) in banners" :key="index" class="banner-item"
|
||||||
<component :is="n.icon"/>
|
:style="{ background: banner.bgColor }">
|
||||||
|
<div class="banner-content">
|
||||||
|
<div class="banner-text">
|
||||||
|
<h3 class="banner-title">{{ banner.title }}</h3>
|
||||||
|
<p class="banner-subtitle">{{ banner.subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="banner-icon">
|
||||||
|
<n-icon :size="64" :color="banner.iconColor">
|
||||||
|
<component :is="banner.icon"/>
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="shortcut-name">{{ n.name }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-gi>
|
</n-carousel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</n-grid>
|
<!-- 统计卡片 -->
|
||||||
<n-grid style="margin-top: 20px;">
|
<div class="stat-cards">
|
||||||
<n-gi :span="24" style="padding:10px;border-radius: 10px;box-shadow: 0 0 20px -5px rgba(84, 151, 232, 0.5);background: #fff;">
|
<n-card v-for="stat in stats" :key="stat.title" class="stat-card">
|
||||||
<div style="display: flex;align-items: center;justify-content: space-between;">
|
<div class="stat-content">
|
||||||
<div style="display: flex;align-items: center;font-size: 18px;color: #056a83;">
|
<div class="stat-icon" :style="{ background: stat.bgColor }">
|
||||||
<n-icon size="23">
|
<n-icon size="24" :color="stat.color">
|
||||||
<DiceOutline />
|
<component :is="stat.icon"/>
|
||||||
</n-icon>
|
</n-icon>
|
||||||
<div style="padding-left: 5px;">代办事项</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
<div style="display: flex;align-items: center;font-size: 14px;color: #4c7c8b;cursor: pointer;">
|
<n-skeleton v-if="loading" :width="60" :height="28"/>
|
||||||
|
<div v-else class="stat-value">
|
||||||
<div>查看全部</div>
|
{{ stat.value }}{{ stat.unit ? ' ' + stat.unit : '' }}
|
||||||
<n-icon size="18">
|
</div>
|
||||||
<ChevronForward />
|
<div class="stat-title">{{ stat.title }}</div>
|
||||||
</n-icon>
|
<div v-if="stat.subText" class="stat-sub-text">{{ stat.subText }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间区域:快捷入口 + 更新日志 -->
|
||||||
|
<n-grid :x-gap="20" :cols="2" class="middle-section">
|
||||||
|
<!-- 快捷入口 -->
|
||||||
|
<n-gi>
|
||||||
|
<n-card title="快捷入口" class="shortcuts-card">
|
||||||
|
<div class="shortcuts-grid">
|
||||||
|
<div
|
||||||
|
v-for="shortcut in shortcuts"
|
||||||
|
:key="shortcut.path"
|
||||||
|
class="shortcut-item"
|
||||||
|
@click="router.push(shortcut.path)"
|
||||||
|
>
|
||||||
|
<div class="shortcut-icon" :style="{ background: shortcut.bgColor }">
|
||||||
|
<n-icon size="24" :color="shortcut.color">
|
||||||
|
<component :is="shortcut.icon"/>
|
||||||
|
</n-icon>
|
||||||
|
</div>
|
||||||
|
<div class="shortcut-name">{{ shortcut.name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</n-gi>
|
||||||
|
|
||||||
|
<!-- 更新日志 -->
|
||||||
|
<n-gi>
|
||||||
|
<n-card title="更新日志" class="changelog-card">
|
||||||
|
<n-timeline>
|
||||||
|
<n-timeline-item
|
||||||
|
v-for="log in changelog"
|
||||||
|
:key="log.version"
|
||||||
|
:type="log.type"
|
||||||
|
:title="log.version"
|
||||||
|
:time="log.date"
|
||||||
|
>
|
||||||
|
<ul class="changelog-list">
|
||||||
|
<li v-for="(item, idx) in log.changes" :key="idx">{{ item }}</li>
|
||||||
|
</ul>
|
||||||
|
</n-timeline-item>
|
||||||
|
</n-timeline>
|
||||||
|
</n-card>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
|
||||||
|
<!-- 底部区域:系统信息 + 作者介绍 -->
|
||||||
|
<n-grid :x-gap="20" :cols="2" class="bottom-section">
|
||||||
|
<!-- 系统信息 -->
|
||||||
|
<n-gi>
|
||||||
|
<n-card title="系统信息" class="system-card">
|
||||||
|
<n-descriptions :column="1" label-placement="left">
|
||||||
|
<n-descriptions-item label="系统名称">mes Admin</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="系统版本">v1.0.7</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="前端框架">Vue 3.4 + Naive UI</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="后端框架">Spring Boot 3.2</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="数据库">MySQL 8.0</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="缓存">Redis 7.0</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
</n-card>
|
||||||
|
</n-gi>
|
||||||
|
|
||||||
|
<!-- 作者介绍 -->
|
||||||
|
<n-gi>
|
||||||
|
<n-card title="关于作者" class="author-card">
|
||||||
|
<div class="author-content">
|
||||||
|
<div class="author-avatar">
|
||||||
|
<n-avatar
|
||||||
|
round
|
||||||
|
:size="80"
|
||||||
|
>
|
||||||
|
</n-avatar>
|
||||||
|
</div>
|
||||||
|
<div class="author-info">
|
||||||
|
<h3 class="author-name">程序员mes</h3>
|
||||||
|
<p class="author-desc">开源作者全栈开发,抖音技术博主,专注于后台管理系统的开发与优化。</p>
|
||||||
|
<div class="author-links">
|
||||||
|
<n-space>
|
||||||
|
<a href="https://gitee.com/mesfactory/evo-mes" target="_blank" class="author-link">
|
||||||
|
<n-icon size="16">
|
||||||
|
<LogoGitlab/>
|
||||||
|
</n-icon>
|
||||||
|
<span>Gitee</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://mes-coder.cn/" target="_blank" class="author-link">
|
||||||
|
<n-icon size="16">
|
||||||
|
<Globe/>
|
||||||
|
</n-icon>
|
||||||
|
<span>火星编程导航</span>
|
||||||
|
</a>
|
||||||
|
<n-popover trigger="hover" placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<span class="author-link">
|
||||||
|
<n-icon size="16"><ChatbubbleOutline/></n-icon>
|
||||||
|
<span>微信</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="wechat-info">
|
||||||
|
<n-icon size="16" color="#07C160">
|
||||||
|
<LogoWechat/>
|
||||||
|
</n-icon>
|
||||||
|
<span>mes8377</span>
|
||||||
|
</div>
|
||||||
|
</n-popover>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<n-divider/>
|
||||||
|
<div class="project-info">
|
||||||
|
<p class="project-desc">
|
||||||
|
mes Admin 是一个基于 Spring Boot 3 + Vue 3 的现代化后台管理系统,
|
||||||
|
采用最新的技术栈,提供完整的权限管理、系统监控等功能。
|
||||||
|
</p>
|
||||||
|
<div class="project-stats">
|
||||||
|
<div class="project-stat-item">
|
||||||
|
<n-icon size="18" color="#F59E0B">
|
||||||
|
<Star/>
|
||||||
|
</n-icon>
|
||||||
|
<span>开源免费</span>
|
||||||
|
</div>
|
||||||
|
<div class="project-stat-item">
|
||||||
|
<n-icon size="18" color="#10B981">
|
||||||
|
<Refresh/>
|
||||||
|
</n-icon>
|
||||||
|
<span>持续更新</span>
|
||||||
|
</div>
|
||||||
|
<div class="project-stat-item">
|
||||||
|
<n-icon size="18" color="#3B82F6">
|
||||||
|
<DocumentText/>
|
||||||
|
</n-icon>
|
||||||
|
<span>文档完善</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import {ref, markRaw, reactive} from 'vue'
|
<script setup lang="ts">
|
||||||
|
import {ref, onMounted, onUnmounted, markRaw} from 'vue'
|
||||||
import {useRouter} from 'vue-router'
|
import {useRouter} from 'vue-router'
|
||||||
import {
|
import {
|
||||||
PersonOutline,
|
PersonOutline,
|
||||||
PeopleOutline,
|
PeopleOutline,
|
||||||
Contract,
|
MenuOutline,
|
||||||
BarChartOutline,
|
ShieldCheckmarkOutline,
|
||||||
BookmarksOutline,
|
LogoGithub,
|
||||||
CellularSharp,
|
LogoGitlab,
|
||||||
CubeOutline,
|
LogoWechat,
|
||||||
ReaderOutline,
|
Globe,
|
||||||
LayersOutline,
|
Mail,
|
||||||
DiceOutline,
|
Star,
|
||||||
ChevronForward,
|
Refresh,
|
||||||
//MenuOutline,
|
DocumentText,
|
||||||
GitNetworkOutline,
|
SettingsOutline,
|
||||||
GameControllerOutline,
|
TimerOutline,
|
||||||
//ShieldCheckmarkOutline,
|
ServerOutline,
|
||||||
ListOutline,
|
RocketOutline,
|
||||||
IdCardOutline,
|
SparklesOutline,
|
||||||
// LogoWechat,
|
CodeSlashOutline,
|
||||||
// Globe,
|
CloudOutline,
|
||||||
// Mail,
|
ChatbubbleOutline
|
||||||
// Star,
|
|
||||||
// Refresh,
|
|
||||||
// DocumentText,
|
|
||||||
// SettingsOutline,
|
|
||||||
// TimerOutline,
|
|
||||||
// ServerOutline,
|
|
||||||
// RocketOutline,
|
|
||||||
// SparklesOutline,
|
|
||||||
// CodeSlashOutline,
|
|
||||||
// CloudOutline,
|
|
||||||
// ChatbubbleOutline
|
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
|
import {useUserStore} from '@/stores/user'
|
||||||
|
import {dashboardStatsApi} from '@/api/dashboardStats'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
function topage(path:any) {
|
const currentTime = ref('')
|
||||||
router.push(path)
|
const currentDate = ref('')
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
// 获取问候语
|
||||||
|
function getGreeting() {
|
||||||
|
const hour = new Date().getHours()
|
||||||
|
if (hour < 6) return '夜深了'
|
||||||
|
if (hour < 9) return '早上好'
|
||||||
|
if (hour < 12) return '上午好'
|
||||||
|
if (hour < 14) return '中午好'
|
||||||
|
if (hour < 18) return '下午好'
|
||||||
|
if (hour < 22) return '晚上好'
|
||||||
|
return '夜深了'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 快捷入口
|
// 轮播Banner数据
|
||||||
const list:any =ref(
|
const banners = [
|
||||||
[
|
{
|
||||||
|
title: 'mes Admin',
|
||||||
|
subtitle: '现代化后台管理系统',
|
||||||
|
bgColor: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
||||||
|
icon: markRaw(RocketOutline),
|
||||||
|
iconColor: 'rgba(255,255,255,0.3)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '技术栈',
|
||||||
|
subtitle: 'Spring Boot 3 + Vue 3',
|
||||||
|
bgColor: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
||||||
|
icon: markRaw(CodeSlashOutline),
|
||||||
|
iconColor: 'rgba(255,255,255,0.3)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '开源免费',
|
||||||
|
subtitle: '持续更新 · 文档完善',
|
||||||
|
bgColor: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
||||||
|
icon: markRaw(SparklesOutline),
|
||||||
|
iconColor: 'rgba(255,255,255,0.3)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '云端部署',
|
||||||
|
subtitle: '支持 Docker 一键部署',
|
||||||
|
bgColor: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
|
||||||
|
icon: markRaw(CloudOutline),
|
||||||
|
iconColor: 'rgba(255,255,255,0.3)'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 统计数据 - 工单相关5个卡片
|
||||||
|
const stats = ref([
|
||||||
|
{
|
||||||
|
title: '工单总数',
|
||||||
|
value: 0,
|
||||||
|
icon: markRaw(DocumentText),
|
||||||
|
color: '#2563EB',
|
||||||
|
bgColor: '#DBEAFE',
|
||||||
|
unit: '单',
|
||||||
|
subText: '所有工单统计'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '进行中工单',
|
||||||
|
value: 0,
|
||||||
|
icon: markRaw(RocketOutline),
|
||||||
|
color: '#D97706',
|
||||||
|
bgColor: '#FEF3C7',
|
||||||
|
unit: '单',
|
||||||
|
subText: '正在执行的工单'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '工单完成率',
|
||||||
|
value: 0,
|
||||||
|
icon: markRaw(ShieldCheckmarkOutline),
|
||||||
|
color: '#059669',
|
||||||
|
bgColor: '#D1FAE5',
|
||||||
|
unit: '%',
|
||||||
|
subText: '已完成比例'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '完工数量',
|
||||||
|
value: 0,
|
||||||
|
icon: markRaw(Star),
|
||||||
|
color: '#7C3AED',
|
||||||
|
bgColor: '#EDE9FE',
|
||||||
|
unit: '件',
|
||||||
|
subText: '已完工的产品数'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '产能达成率',
|
||||||
|
value: 0,
|
||||||
|
icon: markRaw(SparklesOutline),
|
||||||
|
color: '#DC2626',
|
||||||
|
bgColor: '#FEE2E2',
|
||||||
|
unit: '%',
|
||||||
|
subText: '实际/计划产量比'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 快捷入口
|
||||||
|
const shortcuts = [
|
||||||
{
|
{
|
||||||
name: '用户管理',
|
name: '用户管理',
|
||||||
path: '/system/user',
|
path: '/system/user',
|
||||||
@ -263,31 +347,205 @@ const list:any =ref(
|
|||||||
bgColor: '#D1FAE5'
|
bgColor: '#D1FAE5'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '部门管理',
|
name: '菜单管理',
|
||||||
path: '/org/dept',
|
path: '/system/menu',
|
||||||
icon: markRaw(GitNetworkOutline),
|
icon: markRaw(MenuOutline),
|
||||||
color: '#2563EB',
|
color: '#2563EB',
|
||||||
bgColor: '#DBEAFE'
|
bgColor: '#DBEAFE'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '岗位管理',
|
name: '系统配置',
|
||||||
path: '/org/post',
|
path: '/system/config',
|
||||||
icon: markRaw(IdCardOutline),
|
icon: markRaw(SettingsOutline),
|
||||||
color: '#7C3AED',
|
color: '#7C3AED',
|
||||||
bgColor: '#EDE9FE'
|
bgColor: '#EDE9FE'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '操作日志',
|
name: '定时任务',
|
||||||
path: '/log/operlog',
|
path: '/monitor/job',
|
||||||
icon: markRaw(ListOutline),
|
icon: markRaw(TimerOutline),
|
||||||
color: '#DC2626',
|
color: '#DC2626',
|
||||||
bgColor: '#FEE2E2'
|
bgColor: '#FEE2E2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '服务监控',
|
||||||
|
path: '/monitor/server',
|
||||||
|
icon: markRaw(ServerOutline),
|
||||||
|
color: '#0891B2',
|
||||||
|
bgColor: '#CFFAFE'
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
)
|
|
||||||
|
|
||||||
|
// 更新日志
|
||||||
|
const changelog = [
|
||||||
|
{
|
||||||
|
version: 'v1.0.7',
|
||||||
|
date: '2026-03-01',
|
||||||
|
type: 'success' as const,
|
||||||
|
changes: [
|
||||||
|
'代码生成:新增表单布局配置,支持「一行两列」和「从上到下」两种布局',
|
||||||
|
'代码生成:编辑配置弹窗新增「布局配置」标签页',
|
||||||
|
'代码生成:修复 LocalDate/LocalDateTime 字段 JSON 解析错误',
|
||||||
|
'代码生成:日期时间字段提交格式与 Jackson 配置(yyyy-MM-dd HH:mm:ss)兼容',
|
||||||
|
'代码生成:编辑时日期字符串正确转换为时间戳供日期选择器使用'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 'v1.0.6',
|
||||||
|
date: '2026-03-01',
|
||||||
|
type: 'success' as const,
|
||||||
|
changes: [
|
||||||
|
'系统通知:移除短信渠道,Webhook 拆分为飞书/钉钉/企业微信分别选择',
|
||||||
|
'钉钉推送:支持加签密钥(SEC),确保安全校验',
|
||||||
|
'mes-push 模块重构:支持文本和图片,统一 Webhook 发送逻辑',
|
||||||
|
'新增通知记录:可查看各渠道推送触达情况及成功/失败状态',
|
||||||
|
'推送失败支持重试:通知记录中失败渠道可一键重试'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 'v1.0.5',
|
||||||
|
date: '2026-02-28',
|
||||||
|
type: 'success' as const,
|
||||||
|
changes: [
|
||||||
|
'整合 Druid 数据库连接池监控平台',
|
||||||
|
'定时任务新增 Cron 表达式常用预设选择',
|
||||||
|
'定时任务新增调度日志查看功能',
|
||||||
|
'定时任务新增调度统计图表(执行数、成功/失败比例)',
|
||||||
|
'缓存监控页面新增 ECharts 统计图(内存、QPS、命中率、连接数)',
|
||||||
|
'修复通知类型表单校验问题',
|
||||||
|
'优化统计卡片样式(透明背景)'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 'v1.0.4',
|
||||||
|
date: '2026-02-24',
|
||||||
|
type: 'success' as const,
|
||||||
|
changes: [
|
||||||
|
'新增用户批量导入导出功能(EasyExcel)',
|
||||||
|
'导入模板支持角色和岗位字段',
|
||||||
|
'新增用户多选导出功能',
|
||||||
|
'新增用户批量删除功能',
|
||||||
|
'优化文件下载认证处理'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 'v1.0.3',
|
||||||
|
date: '2026-02-24',
|
||||||
|
type: 'success' as const,
|
||||||
|
changes: [
|
||||||
|
'新增前端反调试控制(安全配置开关)',
|
||||||
|
'优化系统配置页面按钮布局',
|
||||||
|
'优化弹窗按钮主题色适配',
|
||||||
|
'新增多种主题颜色选择',
|
||||||
|
'修复操作日志耗时统计问题'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 'v1.0.2',
|
||||||
|
date: '2026-02-13',
|
||||||
|
type: 'success' as const,
|
||||||
|
changes: [
|
||||||
|
'新增 RustFS 对象存储支持',
|
||||||
|
'新增腾讯云 COS 存储支持',
|
||||||
|
'优化存储配置页面布局,访问域名按存储类型分组',
|
||||||
|
'修复 Office 文档预览样式问题',
|
||||||
|
'修复 PDF 预览需要登录的问题',
|
||||||
|
'支持大文件上传(最大 500MB)',
|
||||||
|
'优化文件列表全选效果',
|
||||||
|
'文件管理新增拖拽上传提示'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 'v1.0.1',
|
||||||
|
date: '2026-01-31',
|
||||||
|
type: 'success' as const,
|
||||||
|
changes: [
|
||||||
|
'新增暗黑主题模式,支持一键切换',
|
||||||
|
'优化首页布局,新增轮播 Banner',
|
||||||
|
'新增邮件配置及测试发送功能',
|
||||||
|
'新增接口加密功能(全局/部分加密)',
|
||||||
|
'新增 RSA 密钥自动生成功能',
|
||||||
|
'优化即时聊天页面暗黑模式适配'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 'v1.0.0',
|
||||||
|
date: '2026-01-29',
|
||||||
|
type: 'info' as const,
|
||||||
|
changes: [
|
||||||
|
'新增文件存储策略工厂(本地/MinIO/OSS/COS)',
|
||||||
|
'新增推送服务策略工厂(极光/友盟/个推)',
|
||||||
|
'新增短信/支付服务策略工厂',
|
||||||
|
'优化登录页面(三种样式+滑块验证码)',
|
||||||
|
'完善系统配置分组管理'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 'v0.9.0',
|
||||||
|
date: '2026-01-25',
|
||||||
|
type: 'info' as const,
|
||||||
|
changes: [
|
||||||
|
'新增即时通讯功能(WebSocket私聊/群聊)',
|
||||||
|
'完成字典管理和系统配置功能',
|
||||||
|
'实现部门和岗位管理',
|
||||||
|
'完成定时任务管理功能'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 'v0.8.0',
|
||||||
|
date: '2026-01-20',
|
||||||
|
type: 'default' as const,
|
||||||
|
changes: [
|
||||||
|
'搭建项目基础框架',
|
||||||
|
'集成 Sa-Token 实现认证授权',
|
||||||
|
'完成基础权限管理(用户、角色、菜单)',
|
||||||
|
'实现登录日志和操作日志记录',
|
||||||
|
'添加系统监控功能'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 加载统计数据
|
||||||
|
async function loadStats() {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const data = await dashboardStatsApi.getStats()
|
||||||
|
stats.value[0].value = data.totalOrders
|
||||||
|
stats.value[1].value = data.inProgressOrders
|
||||||
|
stats.value[2].value = data.completionRate
|
||||||
|
stats.value[3].value = data.completedQuantity
|
||||||
|
stats.value[4].value = data.achievementRate
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载统计数据失败', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新时间
|
||||||
|
function updateTime() {
|
||||||
|
const now = new Date()
|
||||||
|
currentTime.value = now.toLocaleTimeString('zh-CN', {hour12: false})
|
||||||
|
currentDate.value = now.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
weekday: 'long'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let timer: number
|
||||||
|
onMounted(() => {
|
||||||
|
updateTime()
|
||||||
|
timer = window.setInterval(updateTime, 1000)
|
||||||
|
loadStats()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(timer)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
// 欢迎区域
|
// 欢迎区域
|
||||||
.welcome-section {
|
.welcome-section {
|
||||||
@ -428,7 +686,7 @@ const list:any =ref(
|
|||||||
|
|
||||||
.stat-cards {
|
.stat-cards {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(5, 1fr);
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
@ -467,6 +725,12 @@ const list:any =ref(
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-sub-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.middle-section {
|
.middle-section {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user