添加Vue.js Admin管理后台和Dashboard BI大屏前端项目
parent
1f21dc2d20
commit
28983c0eb3
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>浩景CNC机床管理系统 - 管理后台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "haoliang-admin",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 8080",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
"pinia": "^2.1.7",
|
||||
"axios": "^1.6.8",
|
||||
"element-plus": "^2.6.1",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"echarts": "^5.5.0",
|
||||
"dayjs": "^1.11.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"typescript": "^5.4.2",
|
||||
"vite": "^5.2.0",
|
||||
"vue-tsc": "^2.0.6",
|
||||
"vitest": "^1.4.0",
|
||||
"@vue/test-utils": "^2.4.5",
|
||||
"jsdom": "^24.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.22.0"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useAuthStore } from './stores/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
onMounted(() => {
|
||||
authStore.checkAuth()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,79 @@
|
||||
import apiClient from './auth'
|
||||
import type { ApiResponse } from './auth'
|
||||
|
||||
export type AlarmType = 'DeviceOffline' | 'DeviceError' | 'ProductionError' | 'SystemError' | 'NetworkError' | 'Maintenance'
|
||||
export type AlarmLevel = 'Low' | 'Medium' | 'High' | 'Critical'
|
||||
export type AlarmStatus = 'Active' | 'Acknowledged' | 'Resolved' | 'Suppressed'
|
||||
|
||||
export interface Alarm {
|
||||
id: number
|
||||
alarmType: AlarmType
|
||||
alarmLevel: AlarmLevel
|
||||
alarmContent: string
|
||||
deviceId?: number
|
||||
deviceName?: string
|
||||
isResolved: boolean
|
||||
occurrenceTime: string
|
||||
resolutionTime?: string
|
||||
resolutionNote?: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface AlarmQueryParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
alarmType?: AlarmType
|
||||
alarmLevel?: AlarmLevel
|
||||
isResolved?: boolean
|
||||
deviceId?: number
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
export interface AlarmListResponse {
|
||||
items: Alarm[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface AlarmStatistics {
|
||||
totalAlarms: number
|
||||
activeAlarms: number
|
||||
criticalAlarms: number
|
||||
resolvedAlarms: number
|
||||
alarmsByType: Record<AlarmType, number>
|
||||
alarmsByLevel: Record<AlarmLevel, number>
|
||||
}
|
||||
|
||||
export async function getAlarms(params?: AlarmQueryParams): Promise<ApiResponse<AlarmListResponse>> {
|
||||
return apiClient.get('/alarms', { params })
|
||||
}
|
||||
|
||||
export async function getAlarmById(id: number): Promise<ApiResponse<Alarm>> {
|
||||
return apiClient.get(`/alarms/${id}`)
|
||||
}
|
||||
|
||||
export async function getActiveAlarms(): Promise<ApiResponse<Alarm[]>> {
|
||||
return apiClient.get('/alarms/active')
|
||||
}
|
||||
|
||||
export async function getCriticalAlarms(): Promise<ApiResponse<Alarm[]>> {
|
||||
return apiClient.get('/alarms/critical')
|
||||
}
|
||||
|
||||
export async function getDeviceAlarms(deviceId: number, days?: number): Promise<ApiResponse<Alarm[]>> {
|
||||
return apiClient.get(`/alarms/device/${deviceId}`, { params: { days } })
|
||||
}
|
||||
|
||||
export async function getAlarmStatistics(date?: string): Promise<ApiResponse<AlarmStatistics>> {
|
||||
return apiClient.get('/alarms/statistics', { params: { date } })
|
||||
}
|
||||
|
||||
export async function resolveAlarm(id: number, resolutionNote?: string): Promise<ApiResponse<boolean>> {
|
||||
return apiClient.put(`/alarms/${id}/resolve`, { resolutionNote })
|
||||
}
|
||||
|
||||
export async function acknowledgeAlarm(id: number, acknowledgeNote?: string): Promise<ApiResponse<boolean>> {
|
||||
return apiClient.put(`/alarms/${id}/acknowledge`, { acknowledgeNote })
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
import axios from 'axios'
|
||||
import type { AxiosInstance, AxiosResponse } from 'axios'
|
||||
|
||||
const apiClient: AxiosInstance = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const res = response.data
|
||||
if (res.success === false) {
|
||||
return Promise.reject(new Error(res.message || '请求失败'))
|
||||
}
|
||||
return res
|
||||
},
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number
|
||||
username: string
|
||||
realName: string
|
||||
email: string
|
||||
phone: string
|
||||
role: string
|
||||
roleName: string
|
||||
department: string
|
||||
isActive: boolean
|
||||
lastLoginTime: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
success: boolean
|
||||
token?: string
|
||||
user?: User
|
||||
permissions?: string[]
|
||||
message: string
|
||||
expiresAt?: string
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean
|
||||
data: T
|
||||
message: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export async function login(data: LoginRequest): Promise<AuthResponse> {
|
||||
const response = await apiClient.post<AuthResponse>('/auth/login', data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
await apiClient.post('/auth/logout')
|
||||
}
|
||||
|
||||
export async function getUserInfo(): Promise<User | null> {
|
||||
try {
|
||||
const response = await apiClient.get<User>('/auth/user')
|
||||
return response.data
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshToken(): Promise<AuthResponse> {
|
||||
const response = await apiClient.post<AuthResponse>('/auth/refresh')
|
||||
return response.data
|
||||
}
|
||||
|
||||
export default apiClient
|
||||
@ -0,0 +1,66 @@
|
||||
import apiClient from './auth'
|
||||
import type { ApiResponse } from './auth'
|
||||
|
||||
export interface CNCDevice {
|
||||
id: number
|
||||
deviceCode: string
|
||||
deviceName: string
|
||||
ipAddress: string
|
||||
httpUrl: string
|
||||
collectionInterval: number
|
||||
templateId: number
|
||||
templateName?: string
|
||||
isAvailable: boolean
|
||||
isOnline: boolean
|
||||
lastCollectionTime?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface DeviceQueryParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
isOnline?: boolean
|
||||
isAvailable?: boolean
|
||||
templateId?: number
|
||||
}
|
||||
|
||||
export interface DeviceListResponse {
|
||||
items: CNCDevice[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export async function getDevices(params?: DeviceQueryParams): Promise<ApiResponse<DeviceListResponse>> {
|
||||
return apiClient.get('/devices', { params })
|
||||
}
|
||||
|
||||
export async function getDeviceById(id: number): Promise<ApiResponse<CNCDevice>> {
|
||||
return apiClient.get(`/devices/${id}`)
|
||||
}
|
||||
|
||||
export async function createDevice(device: Partial<CNCDevice>): Promise<ApiResponse<CNCDevice>> {
|
||||
return apiClient.post('/devices', device)
|
||||
}
|
||||
|
||||
export async function updateDevice(id: number, device: Partial<CNCDevice>): Promise<ApiResponse<CNCDevice>> {
|
||||
return apiClient.put(`/devices/${id}`, device)
|
||||
}
|
||||
|
||||
export async function deleteDevice(id: number): Promise<ApiResponse<boolean>> {
|
||||
return apiClient.delete(`/devices/${id}`)
|
||||
}
|
||||
|
||||
export async function getDeviceStatus(id: number): Promise<ApiResponse<any>> {
|
||||
return apiClient.get(`/devices/${id}/status`)
|
||||
}
|
||||
|
||||
export async function collectDevice(id: number): Promise<ApiResponse<void>> {
|
||||
return apiClient.post(`/devices/${id}/collect`)
|
||||
}
|
||||
|
||||
export async function collectAllDevices(): Promise<ApiResponse<void>> {
|
||||
return apiClient.post('/devices/collect-all')
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
import apiClient from './auth'
|
||||
import type { ApiResponse } from './auth'
|
||||
|
||||
export interface ProductionRecord {
|
||||
recordId: number
|
||||
deviceId: number
|
||||
deviceName: string
|
||||
programName: string
|
||||
quantity: number
|
||||
productionDate: string
|
||||
productionTime: string
|
||||
isCompleted: boolean
|
||||
operator?: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface ProductionQueryParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
deviceId?: number
|
||||
programName?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
export interface ProductionListResponse {
|
||||
items: ProductionRecord[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface DailyProduction {
|
||||
date: string
|
||||
totalQuantity: number
|
||||
deviceCount: number
|
||||
records: ProductionRecord[]
|
||||
}
|
||||
|
||||
export interface DeviceProductionSummary {
|
||||
deviceId: number
|
||||
deviceName: string
|
||||
totalQuantity: number
|
||||
programCount: number
|
||||
programs: ProgramSummary[]
|
||||
}
|
||||
|
||||
export interface ProgramSummary {
|
||||
programName: string
|
||||
quantity: number
|
||||
percentage: number
|
||||
}
|
||||
|
||||
export async function getProductionRecords(params?: ProductionQueryParams): Promise<ApiResponse<ProductionListResponse>> {
|
||||
return apiClient.get('/production/records', { params })
|
||||
}
|
||||
|
||||
export async function getDailyProduction(date: string): Promise<ApiResponse<DailyProduction>> {
|
||||
return apiClient.get(`/production/daily/${date}`)
|
||||
}
|
||||
|
||||
export async function getDeviceProductionSummary(deviceId: number, startDate?: string, endDate?: string): Promise<ApiResponse<DeviceProductionSummary>> {
|
||||
return apiClient.get(`/production/device/${deviceId}/summary`, { params: { startDate, endDate } })
|
||||
}
|
||||
|
||||
export async function getProductionTrend(deviceIds?: number[], days?: number): Promise<ApiResponse<any>> {
|
||||
return apiClient.get('/production/trend', { params: { deviceIds, days } })
|
||||
}
|
||||
|
||||
export async function exportProductionData(params: ProductionQueryParams): Promise<Blob> {
|
||||
const response = await apiClient.get('/production/export', {
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
return response
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
import apiClient from './auth'
|
||||
import type { ApiResponse } from './auth'
|
||||
|
||||
export interface TemplateFieldMapping {
|
||||
id: number
|
||||
sourceFieldPath: string
|
||||
standardFieldId: string
|
||||
standardFieldDesc: string
|
||||
dataType: string
|
||||
conversionRuleType?: string
|
||||
conversionRuleParameters?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface CNCBrandTemplate {
|
||||
id: number
|
||||
brandName: string
|
||||
description: string
|
||||
isEnabled: boolean
|
||||
fieldMappings: TemplateFieldMapping[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface TemplateQueryParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
isEnabled?: boolean
|
||||
brandName?: string
|
||||
}
|
||||
|
||||
export interface TemplateListResponse {
|
||||
items: CNCBrandTemplate[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export async function getTemplates(params?: TemplateQueryParams): Promise<ApiResponse<TemplateListResponse>> {
|
||||
return apiClient.get('/templates', { params })
|
||||
}
|
||||
|
||||
export async function getTemplateById(id: number): Promise<ApiResponse<CNCBrandTemplate>> {
|
||||
return apiClient.get(`/templates/${id}`)
|
||||
}
|
||||
|
||||
export async function createTemplate(template: Partial<CNCBrandTemplate>): Promise<ApiResponse<CNCBrandTemplate>> {
|
||||
return apiClient.post('/templates', template)
|
||||
}
|
||||
|
||||
export async function updateTemplate(id: number, template: Partial<CNCBrandTemplate>): Promise<ApiResponse<CNCBrandTemplate>> {
|
||||
return apiClient.put(`/templates/${id}`, template)
|
||||
}
|
||||
|
||||
export async function deleteTemplate(id: number): Promise<ApiResponse<boolean>> {
|
||||
return apiClient.delete(`/templates/${id}`)
|
||||
}
|
||||
|
||||
export async function enableTemplate(id: number): Promise<ApiResponse<boolean>> {
|
||||
return apiClient.put(`/templates/${id}/enable`)
|
||||
}
|
||||
|
||||
export async function disableTemplate(id: number): Promise<ApiResponse<boolean>> {
|
||||
return apiClient.put(`/templates/${id}/disable`)
|
||||
}
|
||||
|
||||
export async function cloneTemplate(id: number, newName: string): Promise<ApiResponse<CNCBrandTemplate>> {
|
||||
return apiClient.post(`/templates/${id}/clone`, { newName })
|
||||
}
|
||||
|
||||
export async function testTemplate(id: number): Promise<ApiResponse<void>> {
|
||||
return apiClient.post(`/templates/${id}/test`)
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
import apiClient from './auth'
|
||||
import type { ApiResponse } from './auth'
|
||||
|
||||
export interface User {
|
||||
id: number
|
||||
username: string
|
||||
realName: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
email: string
|
||||
phone: string
|
||||
roleId: number
|
||||
roleName?: string
|
||||
department: string
|
||||
isActive: boolean
|
||||
lastLoginTime?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface UserQueryParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
isActive?: boolean
|
||||
roleId?: number
|
||||
}
|
||||
|
||||
export interface UserListResponse {
|
||||
items: User[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export async function getUsers(params?: UserQueryParams): Promise<ApiResponse<UserListResponse>> {
|
||||
return apiClient.get('/users', { params })
|
||||
}
|
||||
|
||||
export async function getUserById(id: number): Promise<ApiResponse<User>> {
|
||||
return apiClient.get(`/users/${id}`)
|
||||
}
|
||||
|
||||
export async function createUser(user: Partial<User>): Promise<ApiResponse<User>> {
|
||||
return apiClient.post('/users', user)
|
||||
}
|
||||
|
||||
export async function updateUser(id: number, user: Partial<User>): Promise<ApiResponse<User>> {
|
||||
return apiClient.put(`/users/${id}`, user)
|
||||
}
|
||||
|
||||
export async function deleteUser(id: number): Promise<ApiResponse<boolean>> {
|
||||
return apiClient.delete(`/users/${id}`)
|
||||
}
|
||||
|
||||
export async function changePassword(userId: number, oldPassword: string, newPassword: string): Promise<ApiResponse<boolean>> {
|
||||
return apiClient.post(`/users/${userId}/change-password`, { oldPassword, newPassword })
|
||||
}
|
||||
|
||||
export async function activateUser(id: number): Promise<ApiResponse<boolean>> {
|
||||
return apiClient.put(`/users/${id}/activate`)
|
||||
}
|
||||
|
||||
export async function deactivateUser(id: number): Promise<ApiResponse<boolean>> {
|
||||
return apiClient.put(`/users/${id}/deactivate`)
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #999;
|
||||
}
|
||||
|
||||
/* 布局工具类 */
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mb-10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mb-20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
import './assets/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册所有图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
|
||||
app.mount('#app')
|
||||
@ -0,0 +1,91 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('../views/Login.vue'),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('../views/Layout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirect: '/dashboard'
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('../views/Dashboard.vue'),
|
||||
meta: { title: '首页', icon: 'Odometer' }
|
||||
},
|
||||
{
|
||||
path: 'devices',
|
||||
name: 'Devices',
|
||||
component: () => import('../views/Devices.vue'),
|
||||
meta: { title: '设备管理', icon: 'Cpu' }
|
||||
},
|
||||
{
|
||||
path: 'templates',
|
||||
name: 'Templates',
|
||||
component: () => import('../views/Templates.vue'),
|
||||
meta: { title: '模板管理', icon: 'Document' }
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'Users',
|
||||
component: () => import('../views/Users.vue'),
|
||||
meta: { title: '用户管理', icon: 'User' }
|
||||
},
|
||||
{
|
||||
path: 'employees',
|
||||
name: 'Employees',
|
||||
component: () => import('../views/Employees.vue'),
|
||||
meta: { title: '员工管理', icon: 'UserFilled' }
|
||||
},
|
||||
{
|
||||
path: 'alarms',
|
||||
name: 'Alarms',
|
||||
component: () => import('../views/Alarms.vue'),
|
||||
meta: { title: '告警管理', icon: 'Bell' }
|
||||
},
|
||||
{
|
||||
path: 'statistics',
|
||||
name: 'Statistics',
|
||||
component: () => import('../views/Statistics.vue'),
|
||||
meta: { title: '统计配置', icon: 'DataAnalysis' }
|
||||
},
|
||||
{
|
||||
path: 'system',
|
||||
name: 'System',
|
||||
component: () => import('../views/System.vue'),
|
||||
meta: { title: '系统配置', icon: 'Setting' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const authStore = useAuthStore()
|
||||
const requiresAuth = to.matched.some(record => record.meta.requiresAuth !== false)
|
||||
|
||||
if (requiresAuth && !authStore.isAuthenticated) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && authStore.isAuthenticated) {
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@ -0,0 +1,70 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { login, logout, getUserInfo } from '../api/auth'
|
||||
import type { LoginRequest, User } from '../api/auth'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const token = ref<string | null>(localStorage.getItem('token'))
|
||||
const user = ref<User | null>(null)
|
||||
const permissions = ref<string[]>([])
|
||||
|
||||
const isAuthenticated = computed(() => !!token.value)
|
||||
|
||||
async function loginAction(loginRequest: LoginRequest) {
|
||||
try {
|
||||
const response = await login(loginRequest)
|
||||
if (response.success) {
|
||||
token.value = response.token
|
||||
user.value = response.user
|
||||
permissions.value = response.permissions || []
|
||||
localStorage.setItem('token', response.token || '')
|
||||
return { success: true, message: response.message }
|
||||
}
|
||||
return { success: false, message: response.message }
|
||||
} catch (error: any) {
|
||||
return { success: false, message: error.message || '登录失败' }
|
||||
}
|
||||
}
|
||||
|
||||
async function logoutAction() {
|
||||
try {
|
||||
await logout()
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error)
|
||||
} finally {
|
||||
token.value = null
|
||||
user.value = null
|
||||
permissions.value = []
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAuth() {
|
||||
if (token.value) {
|
||||
try {
|
||||
const response = await getUserInfo()
|
||||
if (response) {
|
||||
user.value = response
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Check auth error:', error)
|
||||
logoutAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasPermission(permission: string): boolean {
|
||||
return permissions.value.includes(permission)
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
permissions,
|
||||
isAuthenticated,
|
||||
loginAction,
|
||||
logoutAction,
|
||||
checkAuth,
|
||||
hasPermission
|
||||
}
|
||||
})
|
||||
@ -0,0 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getAlarms, resolveAlarm, acknowledgeAlarm } from '../api/alarm'
|
||||
import type { Alarm, AlarmQueryParams } from '../api/alarm'
|
||||
|
||||
const loading = ref(false)
|
||||
const alarmList = ref<Alarm[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
const searchForm = reactive<AlarmQueryParams>({
|
||||
alarmType: undefined,
|
||||
alarmLevel: undefined,
|
||||
isResolved: undefined
|
||||
})
|
||||
|
||||
const alarmTypeOptions = [
|
||||
{ label: '设备离线', value: 'DeviceOffline' },
|
||||
{ label: '设备故障', value: 'DeviceError' },
|
||||
{ label: '生产异常', value: 'ProductionError' },
|
||||
{ label: '系统错误', value: 'SystemError' },
|
||||
{ label: '网络错误', value: 'NetworkError' },
|
||||
{ label: '维护告警', value: 'Maintenance' }
|
||||
]
|
||||
|
||||
const alarmLevelOptions = [
|
||||
{ label: '低', value: 'Low' },
|
||||
{ label: '中', value: 'Medium' },
|
||||
{ label: '高', value: 'High' },
|
||||
{ label: '严重', value: 'Critical' }
|
||||
]
|
||||
|
||||
function getLevelType(level: string): string {
|
||||
switch (level) {
|
||||
case 'Critical': return 'danger'
|
||||
case 'High': return 'warning'
|
||||
case 'Medium': return 'info'
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
function getTypeName(type: string): string {
|
||||
const found = alarmTypeOptions.find(o => o.value === type)
|
||||
return found ? found.label : type
|
||||
}
|
||||
|
||||
async function loadAlarms() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: AlarmQueryParams = {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchForm
|
||||
}
|
||||
const res = await getAlarms(params)
|
||||
if (res.success) {
|
||||
alarmList.value = res.data.items
|
||||
total.value = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load alarms:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
loadAlarms()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.alarmType = undefined
|
||||
searchForm.alarmLevel = undefined
|
||||
searchForm.isResolved = undefined
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
page.value = newPage
|
||||
loadAlarms()
|
||||
}
|
||||
|
||||
function handleSizeChange(newSize: number) {
|
||||
pageSize.value = newSize
|
||||
loadAlarms()
|
||||
}
|
||||
|
||||
async function handleResolve(row: Alarm) {
|
||||
try {
|
||||
await resolveAlarm(row.id)
|
||||
ElMessage.success('告警已解决')
|
||||
loadAlarms()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAcknowledge(row: Alarm) {
|
||||
try {
|
||||
await acknowledgeAlarm(row.id)
|
||||
ElMessage.success('告警已确认')
|
||||
loadAlarms()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAlarms()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="alarms-page">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>告警管理</span>
|
||||
</template>
|
||||
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="告警类型">
|
||||
<el-select v-model="searchForm.alarmType" placeholder="选择类型" clearable>
|
||||
<el-option v-for="item in alarmTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="告警级别">
|
||||
<el-select v-model="searchForm.alarmLevel" placeholder="选择级别" clearable>
|
||||
<el-option v-for="item in alarmLevelOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.isResolved" placeholder="选择状态" clearable>
|
||||
<el-option label="未解决" :value="false" />
|
||||
<el-option label="已解决" :value="true" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="alarmList" v-loading="loading" stripe>
|
||||
<el-table-column prop="alarmType" label="告警类型" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getTypeName(row.alarmType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="alarmLevel" label="级别" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getLevelType(row.alarmLevel)">
|
||||
{{ row.alarmLevel }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="alarmContent" label="告警内容" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column prop="deviceName" label="设备" width="120" />
|
||||
<el-table-column prop="isResolved" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isResolved ? 'success' : 'danger'">
|
||||
{{ row.isResolved ? '已解决' : '未解决' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="occurrenceTime" label="发生时间" width="160" />
|
||||
<el-table-column prop="resolutionTime" label="解决时间" width="160" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="handleAcknowledge(row)" v-if="!row.isResolved">确认</el-button>
|
||||
<el-button link type="success" size="small" @click="handleResolve(row)" v-if="!row.isResolved">解决</el-button>
|
||||
<span v-else style="color: #999;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
style="margin-top: 20px; justify-content: flex-end;"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.alarms-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,251 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ElCard, ElRow, ElCol, ElStatistic } from 'element-plus'
|
||||
import * as echarts from 'echarts'
|
||||
import { getDevices } from '../api/device'
|
||||
import { getActiveAlarms, getAlarmStatistics } from '../api/alarm'
|
||||
import type { CNCDevice } from '../api/device'
|
||||
import type { Alarm, AlarmStatistics } from '../api/alarm'
|
||||
|
||||
const devices = ref<CNCDevice[]>([])
|
||||
const alarms = ref<Alarm[]>([])
|
||||
const statistics = ref<AlarmStatistics | null>(null)
|
||||
|
||||
const onlineCount = ref(0)
|
||||
const offlineCount = ref(0)
|
||||
const runningCount = ref(0)
|
||||
|
||||
const trendChart = ref<echarts.ECharts | null>(null)
|
||||
const statusChart = ref<echarts.ECharts | null>(null)
|
||||
const alarmChart = ref<echarts.ECharts | null>(null)
|
||||
|
||||
let trendInterval: ReturnType<typeof setInterval>
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const [deviceRes, alarmRes, statsRes] = await Promise.all([
|
||||
getDevices({ pageSize: 100 }),
|
||||
getActiveAlarms(),
|
||||
getAlarmStatistics()
|
||||
])
|
||||
|
||||
if (deviceRes.success) {
|
||||
devices.value = deviceRes.data.items
|
||||
onlineCount.value = devices.value.filter(d => d.isOnline).length
|
||||
offlineCount.value = devices.value.filter(d => !d.isOnline).length
|
||||
}
|
||||
|
||||
if (alarmRes.success) {
|
||||
alarms.value = alarmRes.data
|
||||
}
|
||||
|
||||
if (statsRes.success) {
|
||||
statistics.value = statsRes.data
|
||||
}
|
||||
|
||||
updateCharts()
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function updateCharts() {
|
||||
if (trendChart.value) {
|
||||
const option = {
|
||||
title: { text: '设备状态趋势', left: 'center' },
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00']
|
||||
},
|
||||
yAxis: { type: 'value', min: 0, max: 10 },
|
||||
series: [
|
||||
{
|
||||
name: '在线',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [8, 8, 9, 9, 8, 9, 9],
|
||||
areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(64, 158, 255, 0.5)' },
|
||||
{ offset: 1, color: 'rgba(64, 158, 255, 0.1)' }
|
||||
]) }
|
||||
},
|
||||
{
|
||||
name: '运行',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [6, 7, 8, 8, 7, 8, 7],
|
||||
areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(103, 194, 58, 0.5)' },
|
||||
{ offset: 1, color: 'rgba(103, 194, 58, 0.1)' }
|
||||
]) }
|
||||
}
|
||||
]
|
||||
}
|
||||
trendChart.value.setOption(option)
|
||||
}
|
||||
|
||||
if (statusChart.value) {
|
||||
const option = {
|
||||
title: { text: '设备状态分布', left: 'center' },
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { bottom: 10 },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: { show: false },
|
||||
emphasis: {
|
||||
label: { show: true, fontSize: 16 }
|
||||
},
|
||||
data: [
|
||||
{ value: onlineCount.value, name: '在线', itemStyle: { color: '#67c23a' } },
|
||||
{ value: offlineCount.value, name: '离线', itemStyle: { color: '#909399' } },
|
||||
{ value: 5, name: '维护', itemStyle: { color: '#e6a23c' } },
|
||||
{ value: 2, name: '故障', itemStyle: { color: '#f56c6c' } }
|
||||
]
|
||||
}]
|
||||
}
|
||||
statusChart.value.setOption(option)
|
||||
}
|
||||
|
||||
if (alarmChart.value && statistics.value) {
|
||||
const option = {
|
||||
title: { text: '告警统计', left: 'center' },
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: Object.keys(statistics.value.alarmsByType || {})
|
||||
},
|
||||
yAxis: { type: 'value' },
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: Object.values(statistics.value.alarmsByType || {}),
|
||||
itemStyle: { color: '#f56c6c' }
|
||||
}]
|
||||
}
|
||||
alarmChart.value.setOption(option)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
|
||||
trendChart.value = echarts.init(document.getElementById('trend-chart') as HTMLElement)
|
||||
statusChart.value = echarts.init(document.getElementById('status-chart') as HTMLElement)
|
||||
alarmChart.value = echarts.init(document.getElementById('alarm-chart') as HTMLElement)
|
||||
|
||||
window.addEventListener('resize', updateCharts)
|
||||
|
||||
trendInterval = setInterval(loadData, 30000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(trendInterval)
|
||||
window.removeEventListener('resize', updateCharts)
|
||||
trendChart.value?.dispose()
|
||||
statusChart.value?.dispose()
|
||||
alarmChart.value?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<el-row :gutter="20" class="stat-row">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<el-statistic title="设备总数" :value="devices.length">
|
||||
<template #prefix><el-icon class="stat-icon" color="#409eff"><Cpu /></el-icon></template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<el-statistic title="在线设备" :value="onlineCount">
|
||||
<template #prefix><el-icon class="stat-icon" color="#67c23a"><CircleCheck /></el-icon></template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<el-statistic title="离线设备" :value="offlineCount">
|
||||
<template #prefix><el-icon class="stat-icon" color="#909399"><CircleClose /></el-icon></template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<el-statistic title="活跃告警" :value="alarms.length">
|
||||
<template #prefix><el-icon class="stat-icon" color="#f56c6c"><Bell /></el-icon></template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="chart-row">
|
||||
<el-col :span="16">
|
||||
<el-card>
|
||||
<div id="trend-chart" style="height: 350px;"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card>
|
||||
<div id="status-chart" style="height: 350px;"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="chart-row">
|
||||
<el-col :span="24">
|
||||
<el-card>
|
||||
<div id="alarm-chart" style="height: 300px;"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="table-row">
|
||||
<el-col :span="24">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>最新告警</span>
|
||||
</template>
|
||||
<el-table :data="alarms.slice(0, 5)" style="width: 100%">
|
||||
<el-table-column prop="alarmType" label="告警类型" width="120" />
|
||||
<el-table-column prop="alarmLevel" label="级别" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.alarmLevel === 'Critical' ? 'danger' : row.alarmLevel === 'High' ? 'warning' : 'info'">
|
||||
{{ row.alarmLevel }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="alarmContent" label="告警内容" />
|
||||
<el-table-column prop="deviceName" label="设备" width="120" />
|
||||
<el-table-column prop="occurrenceTime" label="发生时间" width="180" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,274 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { getDevices, createDevice, updateDevice, deleteDevice, collectDevice } from '../api/device'
|
||||
import type { CNCDevice, DeviceQueryParams } from '../api/device'
|
||||
|
||||
const loading = ref(false)
|
||||
const deviceList = ref<CNCDevice[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('添加设备')
|
||||
const isEdit = ref(false)
|
||||
const currentDevice = ref<Partial<CNCDevice>>({})
|
||||
|
||||
const searchForm = reactive<DeviceQueryParams>({
|
||||
keyword: '',
|
||||
isOnline: undefined,
|
||||
isAvailable: undefined
|
||||
})
|
||||
|
||||
const deviceForm = reactive<Partial<CNCDevice>>({
|
||||
deviceCode: '',
|
||||
deviceName: '',
|
||||
ipAddress: '',
|
||||
httpUrl: '',
|
||||
collectionInterval: 60,
|
||||
templateId: 0,
|
||||
isAvailable: true
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const rules = {
|
||||
deviceCode: [{ required: true, message: '请输入设备编号', trigger: 'blur' }],
|
||||
deviceName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
|
||||
ipAddress: [{ required: true, message: '请输入IP地址', trigger: 'blur' }],
|
||||
httpUrl: [{ required: true, message: '请输入采集地址', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
async function loadDevices() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: DeviceQueryParams = {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchForm
|
||||
}
|
||||
const res = await getDevices(params)
|
||||
if (res.success) {
|
||||
deviceList.value = res.data.items
|
||||
total.value = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load devices:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
loadDevices()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.isOnline = undefined
|
||||
searchForm.isAvailable = undefined
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
page.value = newPage
|
||||
loadDevices()
|
||||
}
|
||||
|
||||
function handleSizeChange(newSize: number) {
|
||||
pageSize.value = newSize
|
||||
loadDevices()
|
||||
}
|
||||
|
||||
function openAddDialog() {
|
||||
dialogTitle.value = '添加设备'
|
||||
isEdit.value = false
|
||||
currentDevice.value = {
|
||||
deviceCode: '',
|
||||
deviceName: '',
|
||||
ipAddress: '',
|
||||
httpUrl: '',
|
||||
collectionInterval: 60,
|
||||
templateId: 0,
|
||||
isAvailable: true
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEditDialog(row: CNCDevice) {
|
||||
dialogTitle.value = '编辑设备'
|
||||
isEdit.value = true
|
||||
currentDevice.value = { ...row }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await updateDevice(currentDevice.value.id!, currentDevice.value)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createDevice(currentDevice.value)
|
||||
ElMessage.success('添加成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
loadDevices()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDelete(row: CNCDevice) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除设备 ${row.deviceName} 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await deleteDevice(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadDevices()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCollect(row: CNCDevice) {
|
||||
try {
|
||||
await collectDevice(row.id)
|
||||
ElMessage.success('采集任务已启动')
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '采集失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDevices()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="devices-page">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>设备管理</span>
|
||||
<el-button type="primary" @click="openAddDialog">添加设备</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="searchForm.keyword" placeholder="设备编号/名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.isOnline" placeholder="在线状态" clearable>
|
||||
<el-option label="在线" :value="true" />
|
||||
<el-option label="离线" :value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="deviceList" v-loading="loading" stripe>
|
||||
<el-table-column prop="deviceCode" label="设备编号" width="120" />
|
||||
<el-table-column prop="deviceName" label="设备名称" width="150" />
|
||||
<el-table-column prop="ipAddress" label="IP地址" width="130" />
|
||||
<el-table-column prop="httpUrl" label="采集地址" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="templateName" label="模板" width="100" />
|
||||
<el-table-column prop="isOnline" label="在线状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isOnline ? 'success' : 'info'">
|
||||
{{ row.isOnline ? '在线' : '离线' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="isAvailable" label="可用性" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isAvailable ? 'success' : 'warning'">
|
||||
{{ row.isAvailable ? '可用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="lastCollectionTime" label="最后采集" width="160" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button link type="success" size="small" @click="handleCollect(row)">采集</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
style="margin-top: 20px; justify-content: flex-end;"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
|
||||
<el-form ref="formRef" :model="deviceForm" :rules="rules" label-width="100px">
|
||||
<el-form-item label="设备编号" prop="deviceCode">
|
||||
<el-input v-model="deviceForm.deviceCode" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="设备名称" prop="deviceName">
|
||||
<el-input v-model="deviceForm.deviceName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="IP地址" prop="ipAddress">
|
||||
<el-input v-model="deviceForm.ipAddress" />
|
||||
</el-form-item>
|
||||
<el-form-item label="采集地址" prop="httpUrl">
|
||||
<el-input v-model="deviceForm.httpUrl" placeholder="http://192.168.1.100:8080/api/data" />
|
||||
</el-form-item>
|
||||
<el-form-item label="采集间隔" prop="collectionInterval">
|
||||
<el-input-number v-model="deviceForm.collectionInterval" :min="10" :max="3600" /> 秒
|
||||
</el-form-item>
|
||||
<el-form-item label="模板" prop="templateId">
|
||||
<el-input-number v-model="deviceForm.templateId" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="可用性">
|
||||
<el-switch v-model="deviceForm.isAvailable" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.devices-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,223 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
interface Employee {
|
||||
id: number
|
||||
employeeCode: string
|
||||
name: string
|
||||
department: string
|
||||
position: string
|
||||
assignedDevices: number[]
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const employeeList = ref<Employee[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('添加员工')
|
||||
const isEdit = ref(false)
|
||||
const currentEmployee = ref<Partial<Employee>>({})
|
||||
|
||||
const searchKeyword = ref('')
|
||||
|
||||
const employeeForm = reactive<Partial<Employee>>({
|
||||
employeeCode: '',
|
||||
name: '',
|
||||
department: '',
|
||||
position: '',
|
||||
assignedDevices: []
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const rules = {
|
||||
employeeCode: [{ required: true, message: '请输入员工工号', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入员工姓名', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const mockEmployees: Employee[] = [
|
||||
{ id: 1, employeeCode: 'EMP001', name: '张三', department: '生产部', position: '班长', assignedDevices: [1, 2], createdAt: '2024-01-01' },
|
||||
{ id: 2, employeeCode: 'EMP002', name: '李四', department: '生产部', position: '操作员', assignedDevices: [3], createdAt: '2024-01-02' },
|
||||
{ id: 3, employeeCode: 'EMP003', name: '王五', department: '维修部', position: '维修工', assignedDevices: [4, 5], createdAt: '2024-01-03' }
|
||||
]
|
||||
|
||||
function loadEmployees() {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
let filtered = mockEmployees
|
||||
if (searchKeyword.value) {
|
||||
filtered = mockEmployees.filter(e =>
|
||||
e.name.includes(searchKeyword.value) ||
|
||||
e.employeeCode.includes(searchKeyword.value) ||
|
||||
e.department.includes(searchKeyword.value)
|
||||
)
|
||||
}
|
||||
employeeList.value = filtered
|
||||
total.value = filtered.length
|
||||
loading.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
loadEmployees()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchKeyword.value = ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
page.value = newPage
|
||||
loadEmployees()
|
||||
}
|
||||
|
||||
function openAddDialog() {
|
||||
dialogTitle.value = '添加员工'
|
||||
isEdit.value = false
|
||||
currentEmployee.value = {
|
||||
employeeCode: '',
|
||||
name: '',
|
||||
department: '',
|
||||
position: '',
|
||||
assignedDevices: []
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEditDialog(row: Employee) {
|
||||
dialogTitle.value = '编辑员工'
|
||||
isEdit.value = true
|
||||
currentEmployee.value = { ...row, assignedDevices: [...row.assignedDevices] }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '添加成功')
|
||||
dialogVisible.value = false
|
||||
loadEmployees()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDelete(row: Employee) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除员工 ${row.name} 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
ElMessage.success('删除成功')
|
||||
loadEmployees()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadEmployees()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="employees-page">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>员工管理</span>
|
||||
<el-button type="primary" @click="openAddDialog">添加员工</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :inline="true" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="searchKeyword" placeholder="姓名/工号/部门" clearable @keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="employeeList" v-loading="loading" stripe>
|
||||
<el-table-column prop="employeeCode" label="工号" width="120" />
|
||||
<el-table-column prop="name" label="姓名" width="100" />
|
||||
<el-table-column prop="department" label="部门" width="120" />
|
||||
<el-table-column prop="position" label="岗位" width="100" />
|
||||
<el-table-column prop="assignedDevices" label="绑定设备" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-for="deviceId in row.assignedDevices" :key="deviceId" size="small" style="margin-right: 5px;">
|
||||
设备{{ deviceId }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="160" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="handlePageChange"
|
||||
style="margin-top: 20px; justify-content: flex-end;"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
|
||||
<el-form ref="formRef" :model="employeeForm" :rules="rules" label-width="80px">
|
||||
<el-form-item label="工号" prop="employeeCode">
|
||||
<el-input v-model="employeeForm.employeeCode" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="employeeForm.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="employeeForm.department" />
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位" prop="position">
|
||||
<el-input v-model="employeeForm.position" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.employees-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,180 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const isCollapse = ref(false)
|
||||
|
||||
const menuItems = [
|
||||
{ path: '/dashboard', title: '首页', icon: 'Odometer' },
|
||||
{ path: '/devices', title: '设备管理', icon: 'Cpu' },
|
||||
{ path: '/templates', title: '模板管理', icon: 'Document' },
|
||||
{ path: '/users', title: '用户管理', icon: 'User' },
|
||||
{ path: '/employees', title: '员工管理', icon: 'UserFilled' },
|
||||
{ path: '/alarms', title: '告警管理', icon: 'Bell' },
|
||||
{ path: '/statistics', title: '统计配置', icon: 'DataAnalysis' },
|
||||
{ path: '/system', title: '系统配置', icon: 'Setting' }
|
||||
]
|
||||
|
||||
function handleMenuSelect(index: string) {
|
||||
router.push(index)
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await authStore.logoutAction()
|
||||
ElMessage.success('已退出登录')
|
||||
router.push('/login')
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<el-aside :width="isCollapse ? '64px' : '200px'" class="aside">
|
||||
<div class="logo">
|
||||
<span v-if="!isCollapse">浩景CNC</span>
|
||||
<span v-else>CNC</span>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="route.path"
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
class="sidebar-menu"
|
||||
@select="handleMenuSelect"
|
||||
>
|
||||
<el-menu-item
|
||||
v-for="item in menuItems"
|
||||
:key="item.path"
|
||||
:index="item.path"
|
||||
>
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
<template #title>{{ item.title }}</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header class="header">
|
||||
<div class="header-left">
|
||||
<el-icon class="collapse-btn" @click="isCollapse = !isCollapse">
|
||||
<Fold v-if="!isCollapse" />
|
||||
<Expand v-else />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span class="user-info">
|
||||
<el-icon><User /></el-icon>
|
||||
<span class="username">{{ authStore.user?.realName || authStore.user?.username }}</span>
|
||||
<el-icon class="arrow"><ArrowDown /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="profile">个人中心</el-dropdown-item>
|
||||
<el-dropdown-item command="password">修改密码</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main class="main-content">
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.layout-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.aside {
|
||||
background: #304156;
|
||||
transition: width 0.3s;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
background: #2b3a4a;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border-right: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar-menu:not(.el-menu--collapse) {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.collapse-btn:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.username {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
background: #f5f7fa;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import type { LoginRequest } from '../api/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const loginForm = reactive<LoginRequest>({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await authStore.loginAction(loginForm)
|
||||
if (result.success) {
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/')
|
||||
} else {
|
||||
ElMessage.error(result.message || '登录失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '登录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<h1>浩景CNC机床管理系统</h1>
|
||||
<p>浩景智能科技</p>
|
||||
</div>
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="rules"
|
||||
class="login-form"
|
||||
@submit.prevent="handleLogin"
|
||||
>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="用户名"
|
||||
prefix-icon="User"
|
||||
size="large"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
prefix-icon="Lock"
|
||||
size="large"
|
||||
show-password
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
class="login-button"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登 录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.login-box {
|
||||
width: 400px;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
interface StatisticRule {
|
||||
id: number
|
||||
ruleName: string
|
||||
description: string
|
||||
metricFormula: string
|
||||
groupByDimensions: string[]
|
||||
isEnabled: boolean
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const ruleList = ref<StatisticRule[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('添加规则')
|
||||
const isEdit = ref(false)
|
||||
const currentRule = ref<Partial<StatisticRule>>({})
|
||||
|
||||
const ruleForm = reactive<Partial<StatisticRule>>({
|
||||
ruleName: '',
|
||||
description: '',
|
||||
metricFormula: '',
|
||||
groupByDimensions: [],
|
||||
isEnabled: true
|
||||
})
|
||||
|
||||
const mockRules: StatisticRule[] = [
|
||||
{
|
||||
id: 1,
|
||||
ruleName: '日产量统计',
|
||||
description: '按设备统计每日产量',
|
||||
metricFormula: 'SUM(quantity)',
|
||||
groupByDimensions: ['deviceId', 'date'],
|
||||
isEnabled: true,
|
||||
createdAt: '2024-01-01'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
ruleName: '设备利用率',
|
||||
description: '计算设备利用率百分比',
|
||||
metricFormula: 'runningTime / totalTime * 100',
|
||||
groupByDimensions: ['deviceId'],
|
||||
isEnabled: true,
|
||||
createdAt: '2024-01-02'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
ruleName: '程序产量统计',
|
||||
description: '按NC程序统计产量',
|
||||
metricFormula: 'SUM(quantity)',
|
||||
groupByDimensions: ['programName', 'date'],
|
||||
isEnabled: false,
|
||||
createdAt: '2024-01-03'
|
||||
}
|
||||
]
|
||||
|
||||
function loadRules() {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
ruleList.value = mockRules
|
||||
loading.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function openAddDialog() {
|
||||
dialogTitle.value = '添加规则'
|
||||
isEdit.value = false
|
||||
currentRule.value = {
|
||||
ruleName: '',
|
||||
description: '',
|
||||
metricFormula: '',
|
||||
groupByDimensions: [],
|
||||
isEnabled: true
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEditDialog(row: StatisticRule) {
|
||||
dialogTitle.value = '编辑规则'
|
||||
isEdit.value = true
|
||||
currentRule.value = { ...row, groupByDimensions: [...row.groupByDimensions] }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '添加成功')
|
||||
dialogVisible.value = false
|
||||
loadRules()
|
||||
}
|
||||
|
||||
async function handleToggleEnabled(row: StatisticRule) {
|
||||
row.isEnabled = !row.isEnabled
|
||||
ElMessage.success(row.isEnabled ? '已启用' : '已禁用')
|
||||
}
|
||||
|
||||
async function handleDelete(row: StatisticRule) {
|
||||
ElMessage.success('删除成功')
|
||||
loadRules()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRules()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="statistics-page">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>统计配置</span>
|
||||
<el-button type="primary" @click="openAddDialog">添加规则</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="ruleList" v-loading="loading" stripe>
|
||||
<el-table-column prop="ruleName" label="规则名称" width="150" />
|
||||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="metricFormula" label="计算公式" width="200" />
|
||||
<el-table-column prop="groupByDimensions" label="分组维度" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-for="dim in row.groupByDimensions" :key="dim" size="small" style="margin-right: 5px;">
|
||||
{{ dim }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="isEnabled" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isEnabled ? 'success' : 'info'">
|
||||
{{ row.isEnabled ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="160" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button link :type="row.isEnabled ? 'warning' : 'success'" size="small" @click="handleToggleEnabled(row)">
|
||||
{{ row.isEnabled ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
|
||||
<el-form :model="ruleForm" label-width="100px">
|
||||
<el-form-item label="规则名称" required>
|
||||
<el-input v-model="ruleForm.ruleName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="ruleForm.description" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
<el-form-item label="计算公式" required>
|
||||
<el-input v-model="ruleForm.metricFormula" placeholder="如: SUM(quantity)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分组维度" required>
|
||||
<el-select v-model="ruleForm.groupByDimensions" multiple placeholder="选择维度">
|
||||
<el-option label="设备" value="deviceId" />
|
||||
<el-option label="日期" value="date" />
|
||||
<el-option label="程序" value="programName" />
|
||||
<el-option label="员工" value="employeeId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="ruleForm.isEnabled" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.statistics-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,163 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
interface SystemConfig {
|
||||
id: number
|
||||
configKey: string
|
||||
configValue: string
|
||||
description: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const configList = ref<SystemConfig[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('编辑配置')
|
||||
const currentConfig = ref<Partial<SystemConfig>>({})
|
||||
|
||||
const configForm = reactive<Partial<SystemConfig>>({
|
||||
configKey: '',
|
||||
configValue: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const mockConfigs: SystemConfig[] = [
|
||||
{ id: 1, configKey: 'CollectionInterval', configValue: '60', description: '默认采集间隔(秒)', updatedAt: '2024-01-01' },
|
||||
{ id: 2, configKey: 'MaxRetryCount', configValue: '3', description: '采集失败最大重试次数', updatedAt: '2024-01-01' },
|
||||
{ id: 3, configKey: 'RetryInterval', configValue: '30', description: '重试间隔(秒)', updatedAt: '2024-01-01' },
|
||||
{ id: 4, configKey: 'DataRetentionDays', configValue: '90', description: '数据保留天数', updatedAt: '2024-01-01' },
|
||||
{ id: 5, configKey: 'AlarmEmailEnabled', configValue: 'true', description: '启用邮件告警', updatedAt: '2024-01-01' },
|
||||
{ id: 6, configKey: 'AlarmSMSEnabled', configValue: 'false', description: '启用短信告警', updatedAt: '2024-01-01' }
|
||||
]
|
||||
|
||||
function loadConfigs() {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
configList.value = mockConfigs
|
||||
loading.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function openEditDialog(row: SystemConfig) {
|
||||
dialogTitle.value = '编辑配置'
|
||||
currentConfig.value = { ...row }
|
||||
configForm.configKey = row.configKey
|
||||
configForm.configValue = row.configValue
|
||||
configForm.description = row.description
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
ElMessage.success('配置已保存')
|
||||
dialogVisible.value = false
|
||||
loadConfigs()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfigs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="system-page">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<span>系统信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="系统版本">v1.0.0</el-descriptions-item>
|
||||
<el-descriptions-item label=".NET版本">.NET 8.0</el-descriptions-item>
|
||||
<el-descriptions-item label="数据库">MariaDB 10.6</el-descriptions-item>
|
||||
<el-descriptions-item label="运行环境">Linux</el-descriptions-item>
|
||||
<el-descriptions-item label="启动时间">2024-01-01 08:00:00</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<span>运行状态</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="CPU使用率">35%</el-descriptions-item>
|
||||
<el-descriptions-item label="内存使用率">48%</el-descriptions-item>
|
||||
<el-descriptions-item label="磁盘使用率">62%</el-descriptions-item>
|
||||
<el-descriptions-item label="数据库大小">2.5 GB</el-descriptions-item>
|
||||
<el-descriptions-item label="在线设备">8/10</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<span>服务状态</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="API服务">
|
||||
<el-tag type="success">运行中</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="采集服务">
|
||||
<el-tag type="success">运行中</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="SignalR服务">
|
||||
<el-tag type="success">运行中</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="数据库连接">
|
||||
<el-tag type="success">正常</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Redis缓存">
|
||||
<el-tag type="success">正常</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card style="margin-top: 20px;">
|
||||
<template #header>
|
||||
<span>系统配置</span>
|
||||
</template>
|
||||
|
||||
<el-table :data="configList" v-loading="loading" stripe>
|
||||
<el-table-column prop="configKey" label="配置项" width="200" />
|
||||
<el-table-column prop="configValue" label="配置值" width="150" />
|
||||
<el-table-column prop="description" label="描述" min-width="200" />
|
||||
<el-table-column prop="updatedAt" label="更新时间" width="160" />
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openEditDialog(row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
|
||||
<el-form :model="configForm" label-width="100px">
|
||||
<el-form-item label="配置项">
|
||||
<el-input v-model="configForm.configKey" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="配置值">
|
||||
<el-input v-model="configForm.configValue" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="configForm.description" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.system-page {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,268 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { getTemplates, createTemplate, updateTemplate, deleteTemplate, enableTemplate, disableTemplate, cloneTemplate } from '../api/template'
|
||||
import type { CNCBrandTemplate, TemplateQueryParams } from '../api/template'
|
||||
|
||||
const loading = ref(false)
|
||||
const templateList = ref<CNCBrandTemplate[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('添加模板')
|
||||
const isEdit = ref(false)
|
||||
const currentTemplate = ref<Partial<CNCBrandTemplate>>({})
|
||||
|
||||
const searchForm = reactive<TemplateQueryParams>({
|
||||
keyword: '',
|
||||
isEnabled: undefined
|
||||
})
|
||||
|
||||
const templateForm = reactive<Partial<CNCBrandTemplate>>({
|
||||
brandName: '',
|
||||
description: '',
|
||||
isEnabled: true,
|
||||
fieldMappings: []
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const rules = {
|
||||
brandName: [{ required: true, message: '请输入模板名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
async function loadTemplates() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: TemplateQueryParams = {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchForm
|
||||
}
|
||||
const res = await getTemplates(params)
|
||||
if (res.success) {
|
||||
templateList.value = res.data.items
|
||||
total.value = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load templates:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.isEnabled = undefined
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
page.value = newPage
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
function handleSizeChange(newSize: number) {
|
||||
pageSize.value = newSize
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
function openAddDialog() {
|
||||
dialogTitle.value = '添加模板'
|
||||
isEdit.value = false
|
||||
currentTemplate.value = {
|
||||
brandName: '',
|
||||
description: '',
|
||||
isEnabled: true,
|
||||
fieldMappings: []
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEditDialog(row: CNCBrandTemplate) {
|
||||
dialogTitle.value = '编辑模板'
|
||||
isEdit.value = true
|
||||
currentTemplate.value = { ...row, fieldMappings: [...row.fieldMappings] }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await updateTemplate(currentTemplate.value.id!, currentTemplate.value)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createTemplate(currentTemplate.value)
|
||||
ElMessage.success('添加成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
loadTemplates()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDelete(row: CNCBrandTemplate) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除模板 ${row.brandName} 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await deleteTemplate(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadTemplates()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleEnabled(row: CNCBrandTemplate) {
|
||||
try {
|
||||
if (row.isEnabled) {
|
||||
await disableTemplate(row.id)
|
||||
} else {
|
||||
await enableTemplate(row.id)
|
||||
}
|
||||
ElMessage.success(row.isEnabled ? '已禁用' : '已启用')
|
||||
loadTemplates()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
loadTemplates()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClone(row: CNCBrandTemplate) {
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt('请输入新模板名称', '克隆模板', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
await cloneTemplate(row.id, value!)
|
||||
ElMessage.success('克隆成功')
|
||||
loadTemplates()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '克隆失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTemplates()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="templates-page">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>模板管理</span>
|
||||
<el-button type="primary" @click="openAddDialog">添加模板</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="searchForm.keyword" placeholder="模板名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.isEnabled" placeholder="启用状态" clearable>
|
||||
<el-option label="启用" :value="true" />
|
||||
<el-option label="禁用" :value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="templateList" v-loading="loading" stripe>
|
||||
<el-table-column prop="brandName" label="模板名称" width="150" />
|
||||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="fieldMappings.length" label="字段映射" width="100" align="center" />
|
||||
<el-table-column prop="isEnabled" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isEnabled ? 'success' : 'info'">
|
||||
{{ row.isEnabled ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="160" />
|
||||
<el-table-column prop="updatedAt" label="更新时间" width="160" />
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button link type="success" size="small" @click="handleClone(row)">克隆</el-button>
|
||||
<el-button link type="warning" size="small" @click="handleToggleEnabled(row)">
|
||||
{{ row.isEnabled ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
style="margin-top: 20px; justify-content: flex-end;"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
|
||||
<el-form ref="formRef" :model="templateForm" :rules="rules" label-width="100px">
|
||||
<el-form-item label="模板名称" prop="brandName">
|
||||
<el-input v-model="templateForm.brandName" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="templateForm.description" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="templateForm.isEnabled" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.templates-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,276 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { getUsers, createUser, updateUser, deleteUser, activateUser, deactivateUser } from '../api/user'
|
||||
import type { User, UserQueryParams } from '../api/user'
|
||||
|
||||
const loading = ref(false)
|
||||
const userList = ref<User[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('添加用户')
|
||||
const isEdit = ref(false)
|
||||
const currentUser = ref<Partial<User>>({})
|
||||
|
||||
const searchForm = reactive<UserQueryParams>({
|
||||
keyword: '',
|
||||
isActive: undefined
|
||||
})
|
||||
|
||||
const userForm = reactive<Partial<User>>({
|
||||
username: '',
|
||||
realName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
roleId: 1,
|
||||
department: '',
|
||||
isActive: true
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
realName: [{ required: true, message: '请输入真实姓名', trigger: 'blur' }],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: UserQueryParams = {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchForm
|
||||
}
|
||||
const res = await getUsers(params)
|
||||
if (res.success) {
|
||||
userList.value = res.data.items
|
||||
total.value = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load users:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.isActive = undefined
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
page.value = newPage
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
function handleSizeChange(newSize: number) {
|
||||
pageSize.value = newSize
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
function openAddDialog() {
|
||||
dialogTitle.value = '添加用户'
|
||||
isEdit.value = false
|
||||
currentUser.value = {
|
||||
username: '',
|
||||
realName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
roleId: 1,
|
||||
department: '',
|
||||
isActive: true
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEditDialog(row: User) {
|
||||
dialogTitle.value = '编辑用户'
|
||||
isEdit.value = true
|
||||
currentUser.value = { ...row }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await updateUser(currentUser.value.id!, currentUser.value)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createUser(currentUser.value)
|
||||
ElMessage.success('添加成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
loadUsers()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDelete(row: User) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除用户 ${row.realName} 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await deleteUser(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadUsers()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleActive(row: User) {
|
||||
try {
|
||||
if (row.isActive) {
|
||||
await deactivateUser(row.id)
|
||||
} else {
|
||||
await activateUser(row.id)
|
||||
}
|
||||
ElMessage.success(row.isActive ? '已停用' : '已激活')
|
||||
loadUsers()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
loadUsers()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadUsers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="users-page">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户管理</span>
|
||||
<el-button type="primary" @click="openAddDialog">添加用户</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="searchForm.keyword" placeholder="用户名/姓名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.isActive" placeholder="用户状态" clearable>
|
||||
<el-option label="激活" :value="true" />
|
||||
<el-option label="停用" :value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="userList" v-loading="loading" stripe>
|
||||
<el-table-column prop="username" label="用户名" width="120" />
|
||||
<el-table-column prop="realName" label="姓名" width="100" />
|
||||
<el-table-column prop="email" label="邮箱" width="180" />
|
||||
<el-table-column prop="phone" label="电话" width="130" />
|
||||
<el-table-column prop="roleName" label="角色" width="100" />
|
||||
<el-table-column prop="department" label="部门" width="120" />
|
||||
<el-table-column prop="isActive" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isActive ? 'success' : 'info'">
|
||||
{{ row.isActive ? '激活' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="lastLoginTime" label="最后登录" width="160" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button link :type="row.isActive ? 'warning' : 'success'" size="small" @click="handleToggleActive(row)">
|
||||
{{ row.isActive ? '停用' : '激活' }}
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
style="margin-top: 20px; justify-content: flex-end;"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
|
||||
<el-form ref="formRef" :model="userForm" :rules="rules" label-width="80px">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="userForm.username" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="realName">
|
||||
<el-input v-model="userForm.realName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="userForm.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="电话" prop="phone">
|
||||
<el-input v-model="userForm.phone" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="userForm.department" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-input-number v-model="userForm.roleId" :min="1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-switch v-model="userForm.isActive" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.users-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 8080,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5000',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/signalr': {
|
||||
target: 'http://localhost:5000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>浩景CNC机床管理系统 - BI大屏</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "haoliang-dashboard",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 8081",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
"pinia": "^2.1.7",
|
||||
"axios": "^1.6.8",
|
||||
"echarts": "^5.5.0",
|
||||
"@microsoft/signalr": "^8.0.0",
|
||||
"dayjs": "^1.11.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"typescript": "^5.4.2",
|
||||
"vite": "^5.2.0",
|
||||
"vue-tsc": "^2.0.6",
|
||||
"vitest": "^1.4.0",
|
||||
"@vue/test-utils": "^2.4.5",
|
||||
"jsdom": "^24.0.0"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,461 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import * as signalR from '@microsoft/signalr'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const connection = ref<signalR.HubConnection | null>(null)
|
||||
const refreshInterval = ref<ReturnType<typeof setInterval> | null>(null)
|
||||
const currentTime = ref(new Date().toLocaleString())
|
||||
|
||||
const deviceStats = ref({
|
||||
total: 10,
|
||||
online: 8,
|
||||
offline: 2,
|
||||
running: 6,
|
||||
maintenance: 1,
|
||||
error: 1
|
||||
})
|
||||
|
||||
const productionData = ref([
|
||||
{ name: 'CNC-001', today: 120, week: 840, month: 3600 },
|
||||
{ name: 'CNC-002', today: 95, week: 665, month: 2850 },
|
||||
{ name: 'CNC-003', today: 150, week: 1050, month: 4500 },
|
||||
{ name: 'CNC-004', today: 80, week: 560, month: 2400 },
|
||||
{ name: 'CNC-005', today: 110, week: 770, month: 3300 }
|
||||
])
|
||||
|
||||
const trendData = ref({
|
||||
dates: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
||||
production: [850, 920, 880, 960, 1000, 450, 380],
|
||||
online: [8, 9, 8, 9, 10, 8, 9]
|
||||
})
|
||||
|
||||
const utilizationData = ref([
|
||||
{ name: '运行中', value: 65 },
|
||||
{ name: '空闲中', value: 15 },
|
||||
{ name: '维护中', value: 10 },
|
||||
{ name: '故障中', value: 10 }
|
||||
])
|
||||
|
||||
let statusChart: echarts.ECharts | null = null
|
||||
let trendChart: echarts.ECharts | null = null
|
||||
let utilizationChart: echarts.ECharts | null = null
|
||||
let productionChart: echarts.ECharts | null = null
|
||||
|
||||
function initCharts() {
|
||||
statusChart = echarts.init(document.getElementById('status-chart') as HTMLElement)
|
||||
trendChart = echarts.init(document.getElementById('trend-chart') as HTMLElement)
|
||||
utilizationChart = echarts.init(document.getElementById('utilization-chart') as HTMLElement)
|
||||
productionChart = echarts.init(document.getElementById('production-chart') as HTMLElement)
|
||||
|
||||
updateCharts()
|
||||
}
|
||||
|
||||
function updateCharts() {
|
||||
if (statusChart) {
|
||||
statusChart.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { bottom: 10, textStyle: { color: '#fff' } },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['45%', '70%'],
|
||||
center: ['50%', '45%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: { show: false },
|
||||
emphasis: {
|
||||
label: { show: true, fontSize: 14, fontWeight: 'bold' }
|
||||
},
|
||||
data: [
|
||||
{ value: deviceStats.value.online, name: '在线', itemStyle: { color: '#52c41a' } },
|
||||
{ value: deviceStats.value.offline, name: '离线', itemStyle: { color: '#8c8c8c' } },
|
||||
{ value: deviceStats.value.maintenance, name: '维护', itemStyle: { color: '#faad14' } },
|
||||
{ value: deviceStats.value.error, name: '故障', itemStyle: { color: '#ff4d4f' } }
|
||||
]
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (trendChart) {
|
||||
trendChart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['产量', '在线设备'], textStyle: { color: '#fff' } },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: trendData.value.dates,
|
||||
axisLine: { lineStyle: { color: '#fff' } },
|
||||
axisLabel: { color: '#fff' }
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '产量',
|
||||
axisLine: { lineStyle: { color: '#fff' } },
|
||||
axisLabel: { color: '#fff' },
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '在线',
|
||||
max: 10,
|
||||
axisLine: { lineStyle: { color: '#fff' } },
|
||||
axisLabel: { color: '#fff' },
|
||||
splitLine: { show: false }
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '产量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: trendData.value.production,
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(82, 196, 26, 0.5)' },
|
||||
{ offset: 1, color: 'rgba(82, 196, 26, 0.1)' }
|
||||
])
|
||||
},
|
||||
lineStyle: { color: '#52c41a' },
|
||||
itemStyle: { color: '#52c41a' }
|
||||
},
|
||||
{
|
||||
name: '在线设备',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
smooth: true,
|
||||
data: trendData.value.online,
|
||||
lineStyle: { color: '#1890ff' },
|
||||
itemStyle: { color: '#1890ff' }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
if (utilizationChart) {
|
||||
utilizationChart.setOption({
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c}%' },
|
||||
series: [{
|
||||
type: 'gauge',
|
||||
startAngle: 180,
|
||||
endAngle: 0,
|
||||
center: ['50%', '70%'],
|
||||
radius: '90%',
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitNumber: 4,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
width: 20,
|
||||
color: [
|
||||
[0.3, '#ff4d4f'],
|
||||
[0.7, '#faad14'],
|
||||
[1, '#52c41a']
|
||||
]
|
||||
}
|
||||
},
|
||||
pointer: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { show: false },
|
||||
title: {
|
||||
offsetCenter: [0, '20%'],
|
||||
fontSize: 16,
|
||||
color: '#fff'
|
||||
},
|
||||
detail: {
|
||||
fontSize: 36,
|
||||
fontWeight: 'bold',
|
||||
offsetCenter: [0, '-10%'],
|
||||
valueAnimation: true,
|
||||
formatter: '{value}%',
|
||||
color: '#fff'
|
||||
},
|
||||
data: [{ value: 65, name: '设备利用率' }]
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (productionChart) {
|
||||
productionChart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['今日产量', '本周产量'], textStyle: { color: '#fff' } },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: productionData.value.map(p => p.name),
|
||||
axisLine: { lineStyle: { color: '#fff' } },
|
||||
axisLabel: { color: '#fff', rotate: 30 }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { lineStyle: { color: '#fff' } },
|
||||
axisLabel: { color: '#fff' },
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '今日产量',
|
||||
type: 'bar',
|
||||
data: productionData.value.map(p => p.today),
|
||||
itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#1890ff' },
|
||||
{ offset: 1, color: '#69c0ff' }
|
||||
]) },
|
||||
barWidth: '35%'
|
||||
},
|
||||
{
|
||||
name: '本周产量',
|
||||
type: 'bar',
|
||||
data: productionData.value.map(p => p.week / 7),
|
||||
itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#52c41a' },
|
||||
{ offset: 1, color: '#95de64' }
|
||||
]) },
|
||||
barWidth: '35%'
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function connectSignalR() {
|
||||
try {
|
||||
connection.value = new signalR.HubConnectionBuilder()
|
||||
.withUrl('/signalr/hub')
|
||||
.withAutomaticReconnect()
|
||||
.build()
|
||||
|
||||
connection.value.on('ReceiveDeviceStatus', (data: any) => {
|
||||
deviceStats.value = data
|
||||
updateCharts()
|
||||
})
|
||||
|
||||
connection.value.on('ReceiveProductionData', (data: any) => {
|
||||
productionData.value = data
|
||||
updateCharts()
|
||||
})
|
||||
|
||||
await connection.value.start()
|
||||
console.log('SignalR connected')
|
||||
} catch (error) {
|
||||
console.error('SignalR connection failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
fetch('/api/v1/realtime/status')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
deviceStats.value = data.data
|
||||
updateCharts()
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function handleFullscreen() {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen()
|
||||
} else {
|
||||
document.exitFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCharts()
|
||||
connectSignalR()
|
||||
|
||||
refreshInterval.value = setInterval(() => {
|
||||
refreshData()
|
||||
currentTime.value = new Date().toLocaleString()
|
||||
}, 5000)
|
||||
|
||||
window.addEventListener('resize', updateCharts)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshInterval.value) {
|
||||
clearInterval(refreshInterval.value)
|
||||
}
|
||||
if (connection.value) {
|
||||
connection.value.stop()
|
||||
}
|
||||
statusChart?.dispose()
|
||||
trendChart?.dispose()
|
||||
utilizationChart?.dispose()
|
||||
productionChart?.dispose()
|
||||
window.removeEventListener('resize', updateCharts)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<div class="header">
|
||||
<div class="title">浩景CNC机床数据采集分析系统</div>
|
||||
<div class="header-right">
|
||||
<span class="time">{{ currentTime }}</span>
|
||||
<el-button type="primary" size="small" @click="handleFullscreen">全屏</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">设备总数</div>
|
||||
<div class="stat-value">{{ deviceStats.total }}</div>
|
||||
</div>
|
||||
<div class="stat-card online">
|
||||
<div class="stat-label">在线设备</div>
|
||||
<div class="stat-value">{{ deviceStats.online }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">运行中</div>
|
||||
<div class="stat-value">{{ deviceStats.running }}</div>
|
||||
</div>
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-label">维护中</div>
|
||||
<div class="stat-value">{{ deviceStats.maintenance }}</div>
|
||||
</div>
|
||||
<div class="stat-card danger">
|
||||
<div class="stat-label">故障中</div>
|
||||
<div class="stat-value">{{ deviceStats.error }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">离线设备</div>
|
||||
<div class="stat-value">{{ deviceStats.offline }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">设备状态分布</div>
|
||||
<div id="status-chart" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-card wide">
|
||||
<div class="chart-title">产量趋势</div>
|
||||
<div id="trend-chart" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">设备利用率</div>
|
||||
<div id="utilization-chart" class="chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="chart-card full">
|
||||
<div class="chart-title">各设备产量统计</div>
|
||||
<div id="production-chart" class="chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 40px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 16px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px 40px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.stat-card.online {
|
||||
background: rgba(82, 196, 26, 0.2);
|
||||
border: 1px solid rgba(82, 196, 26, 0.5);
|
||||
}
|
||||
|
||||
.stat-card.warning {
|
||||
background: rgba(250, 173, 20, 0.2);
|
||||
border: 1px solid rgba(250, 173, 20, 0.5);
|
||||
}
|
||||
|
||||
.stat-card.danger {
|
||||
background: rgba(255, 77, 79, 0.2);
|
||||
border: 1px solid rgba(255, 77, 79, 0.5);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.chart-card.wide {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.chart-card.full {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 280px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,30 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import './assets/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
|
||||
app.mount('#app')
|
||||
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 8081,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5000',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/signalr': {
|
||||
target: 'http://localhost:5000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue