初始化:静态菜单版 数据大屏管理系统,对接KLPL3数据库
This commit is contained in:
2
.env
Normal file
2
.env
Normal file
@@ -0,0 +1,2 @@
|
||||
VUE_APP_API_BASE_URL=http://localhost:8080/api
|
||||
VUE_APP_TITLE=数据大屏管理系统
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
dist/
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -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>数据大屏管理系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
8
jsconfig.json
Normal file
8
jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
3224
package-lock.json
generated
Normal file
3224
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "screen-management",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jiaminghi/data-view": "^2.10.0",
|
||||
"axios": "^1.6.7",
|
||||
"cors": "^2.8.6",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.6.1",
|
||||
"express": "^5.2.1",
|
||||
"mysql2": "^3.22.3",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"sass": "^1.71.1",
|
||||
"vite": "^5.1.6"
|
||||
}
|
||||
}
|
||||
10
server/.idea/.gitignore
generated
vendored
Normal file
10
server/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 已忽略包含查询文件的默认文件夹
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
6
server/.idea/misc.xml
generated
Normal file
6
server/.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="temurin-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
8
server/.idea/modules.xml
generated
Normal file
8
server/.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/server.iml" filepath="$PROJECT_DIR$/.idea/server.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
9
server/.idea/server.iml
generated
Normal file
9
server/.idea/server.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
845
server/app.js
Normal file
845
server/app.js
Normal file
@@ -0,0 +1,845 @@
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import mysql from 'mysql2/promise'
|
||||
|
||||
const app = express()
|
||||
const port = 3000
|
||||
|
||||
app.use(cors())
|
||||
app.use(express.json())
|
||||
|
||||
// 多数据库连接配置 - KLPL3数据库
|
||||
const dbConfigs = {
|
||||
master: {
|
||||
host: '140.143.206.120',
|
||||
port: 13306,
|
||||
user: 'klp',
|
||||
password: 'KeLunPu@123',
|
||||
database: 'klp-oa-test',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
},
|
||||
acid: {
|
||||
host: '140.143.206.120',
|
||||
port: 13306,
|
||||
user: 'klp',
|
||||
password: 'KeLunPu@123',
|
||||
database: 'klp_pocketfactory',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
}
|
||||
}
|
||||
|
||||
let masterPool = null
|
||||
let acidPool = null
|
||||
|
||||
// 尝试连接数据库
|
||||
const initDatabases = async () => {
|
||||
try {
|
||||
masterPool = mysql.createPool(dbConfigs.master)
|
||||
const [masterRows] = await masterPool.execute('SELECT 1')
|
||||
console.log('✅ 主数据库(klp-oa-test)连接成功')
|
||||
} catch (error) {
|
||||
console.error('❌ 主数据库连接失败:', error.message)
|
||||
console.log('⚠️ 将使用模拟数据运行')
|
||||
}
|
||||
|
||||
try {
|
||||
acidPool = mysql.createPool(dbConfigs.acid)
|
||||
const [acidRows] = await acidPool.execute('SELECT 1')
|
||||
console.log('✅ 酸轧数据库(klp_pocketfactory)连接成功')
|
||||
} catch (error) {
|
||||
console.error('❌ 酸轧数据库连接失败:', error.message)
|
||||
console.log('⚠️ 将使用模拟数据运行')
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟数据(备用)
|
||||
const mockData = {
|
||||
dashboardOverview: {
|
||||
todayTaskCount: 2,
|
||||
monthTaskCount: 18,
|
||||
yearTaskCount: 120,
|
||||
successRate: 88.3,
|
||||
qualifiedRate: 95.6,
|
||||
ranking: [
|
||||
{ name: '周口', value: 55 },
|
||||
{ name: '南阳', value: 120 },
|
||||
{ name: '洛阳', value: 89 },
|
||||
{ name: '开封', value: 67 },
|
||||
{ name: '商丘', value: 45 }
|
||||
]
|
||||
},
|
||||
productRanking: [
|
||||
{ id: 1, name: 'SPHC卷板', consultCount: 123, trend: 12.5 },
|
||||
{ id: 2, name: 'SPHD冷轧板', consultCount: 98, trend: -3.2 },
|
||||
{ id: 3, name: 'SPHE热轧板', consultCount: 87, trend: 8.7 },
|
||||
{ id: 4, name: 'Q235钢板', consultCount: 76, trend: 5.3 },
|
||||
{ id: 5, name: 'Q345低合金', consultCount: 65, trend: -1.2 }
|
||||
],
|
||||
currentPlan: {
|
||||
lineName: '酸轧线 SY',
|
||||
planName: '2026年5月生产计划',
|
||||
targetOutput: 15000,
|
||||
actualOutput: 12580,
|
||||
progress: 83.9
|
||||
},
|
||||
currentProcess: {
|
||||
temperature: 850,
|
||||
speed: 120,
|
||||
thickness: 2.0,
|
||||
width: 1250,
|
||||
tension: 150
|
||||
},
|
||||
oeeData: {
|
||||
kpi: {
|
||||
oee: 87.5,
|
||||
availability: 92.3,
|
||||
performanceTon: 89.1,
|
||||
quality: 98.5,
|
||||
totalOutputTon: 12580,
|
||||
totalOutputCoil: 156,
|
||||
goodOutputTon: 12391,
|
||||
targetOutputTon: 15000
|
||||
},
|
||||
summaryList: [
|
||||
{ date: '2026-05-10', oee: 85.2, availability: 90.5, performanceTon: 88.2, quality: 96.8, totalOutputTon: 2150, targetOutputTon: 2500, efficiency: 86.0 },
|
||||
{ date: '2026-05-11', oee: 86.8, availability: 91.2, performanceTon: 89.5, quality: 97.2, totalOutputTon: 2280, targetOutputTon: 2500, efficiency: 91.2 },
|
||||
{ date: '2026-05-12', oee: 88.5, availability: 93.8, performanceTon: 91.2, quality: 97.8, totalOutputTon: 2420, targetOutputTon: 2500, efficiency: 96.8 },
|
||||
{ date: '2026-05-13', oee: 87.2, availability: 92.1, performanceTon: 89.6, quality: 97.4, totalOutputTon: 2350, targetOutputTon: 2500, efficiency: 94.0 },
|
||||
{ date: '2026-05-14', oee: 89.1, availability: 94.5, performanceTon: 92.1, quality: 98.0, totalOutputTon: 2480, targetOutputTon: 2500, efficiency: 99.2 },
|
||||
{ date: '2026-05-15', oee: 87.5, availability: 92.3, performanceTon: 89.8, quality: 97.5, totalOutputTon: 2300, targetOutputTon: 2500, efficiency: 92.0 }
|
||||
],
|
||||
loss7List: [
|
||||
{ lossName: '故障停机损失', lossTime: 125, lossRatio: 35.2, description: '设备故障导致停机' },
|
||||
{ lossName: '换模换线损失', lossTime: 85, lossRatio: 23.8, description: '产品切换换模时间' },
|
||||
{ lossName: '空转与短暂停机损失', lossTime: 55, lossRatio: 15.4, description: '短暂停机调整' },
|
||||
{ lossName: '速度损失', lossTime: 45, lossRatio: 12.6, description: '未达到理论速度' },
|
||||
{ lossName: '质量缺陷与返工损失', lossTime: 25, lossRatio: 7.0, description: '次品返工时间' },
|
||||
{ lossName: '启动损失', lossTime: 15, lossRatio: 4.2, description: '开机预热时间' },
|
||||
{ lossName: '管理损失', lossTime: 6, lossRatio: 1.8, description: '管理原因等待' }
|
||||
],
|
||||
eventList: [
|
||||
{ eventTime: '2026-05-15 13:22', eventType: '停机', lossType: '故障停机损失', duration: '12分钟', reason: '带尾夹送断带', handleStatus: '已处理' },
|
||||
{ eventTime: '2026-05-15 10:15', eventType: '停机', lossType: '质量缺陷与返工损失', duration: '7分钟', reason: '处理表面缺陷', handleStatus: '已处理' },
|
||||
{ eventTime: '2026-05-15 08:00', eventType: '停机', lossType: '换模换线损失', duration: '33分钟', reason: '换1-4机架辊', handleStatus: '已处理' },
|
||||
{ eventTime: '2026-05-14 22:30', eventType: '停机', lossType: '故障停机损失', duration: '18分钟', reason: '液压系统故障', handleStatus: '已处理' },
|
||||
{ eventTime: '2026-05-14 16:45', eventType: '减速', lossType: '速度损失', duration: '45分钟', reason: '来料质量问题', handleStatus: '已处理' }
|
||||
]
|
||||
},
|
||||
acidRollingReport: {
|
||||
summary: {
|
||||
totalQuantity: 156,
|
||||
totalWeight: 1025.8,
|
||||
avgWeight: 6.58
|
||||
},
|
||||
details: [
|
||||
{ batchNo: 'B20260515001', currentNo: 'C20260515001', productionTime: '2026-05-15 08:30:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHC', width: '1250mm', thickness: '2.0mm', weight: '6.85t', length: '12500mm', stockStatus: '在库' },
|
||||
{ batchNo: 'B20260515002', currentNo: 'C20260515002', productionTime: '2026-05-15 09:15:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHD', width: '1500mm', thickness: '1.8mm', weight: '7.23t', length: '14500mm', stockStatus: '在库' },
|
||||
{ batchNo: 'B20260515003', currentNo: 'C20260515003', productionTime: '2026-05-15 10:00:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHE', width: '1000mm', thickness: '2.5mm', weight: '6.98t', length: '11000mm', stockStatus: '已出库' },
|
||||
{ batchNo: 'B20260515004', currentNo: 'C20260515004', productionTime: '2026-05-15 10:45:00', warehouse: '酸轧成品库', qualityStatus: '不合格', productType: 'SPHC', width: '1250mm', thickness: '2.0mm', weight: '7.12t', length: '12800mm', stockStatus: '待处理' },
|
||||
{ batchNo: 'B20260515005', currentNo: 'C20260515005', productionTime: '2026-05-15 11:30:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHD', width: '1500mm', thickness: '1.6mm', weight: '6.75t', length: '15200mm', stockStatus: '在库' },
|
||||
{ batchNo: 'B20260515006', currentNo: 'C20260515006', productionTime: '2026-05-15 14:00:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHC', width: '1250mm', thickness: '2.2mm', weight: '7.35t', length: '11800mm', stockStatus: '在库' },
|
||||
{ batchNo: 'B20260515007', currentNo: 'C20260515007', productionTime: '2026-05-15 14:45:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHD', width: '1000mm', thickness: '1.5mm', weight: '5.89t', length: '16500mm', stockStatus: '在库' },
|
||||
{ batchNo: 'B20260515008', currentNo: 'C20260515008', productionTime: '2026-05-15 15:30:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHE', width: '1250mm', thickness: '2.3mm', weight: '7.02t', length: '12100mm', stockStatus: '已出库' }
|
||||
]
|
||||
},
|
||||
acidStopReport: {
|
||||
summary: {
|
||||
stopTime: '60 min',
|
||||
stopCount: 4,
|
||||
rate: '95.83%'
|
||||
},
|
||||
teamDistribution: [
|
||||
{ name: '停机', value: 1 },
|
||||
{ name: '正常', value: 0 }
|
||||
],
|
||||
typeDistribution: [
|
||||
{ name: '来料缺陷', value: 45 },
|
||||
{ name: '机械故障', value: 35 },
|
||||
{ name: '换辊', value: 20 }
|
||||
],
|
||||
details: [
|
||||
{ timeRange: '2026-05-15 13:22 - 2026-05-15 13:34', duration: '12min', team: 'MLL', remark: '带尾夹送断带' },
|
||||
{ timeRange: '2026-05-15 12:47 - 2026-05-15 12:54', duration: '7min', team: 'MLL', remark: '处理表面缺陷' },
|
||||
{ timeRange: '2026-05-15 10:27 - 2026-05-15 11:00', duration: '33min', team: 'MLL', remark: '换1-4机架辊,修4机架弯辊阀' },
|
||||
{ timeRange: '2026-05-15 04:03 - 2026-05-15 04:11', duration: '8min', team: 'MLL', remark: '换辊' }
|
||||
]
|
||||
},
|
||||
energyData: {
|
||||
todayEnergy: 12500,
|
||||
monthEnergy: 385000,
|
||||
yearEnergy: 4620000,
|
||||
unitConsumption: 156.8,
|
||||
trend: [
|
||||
{ date: '05-09', value: 11200 },
|
||||
{ date: '05-10', value: 12100 },
|
||||
{ date: '05-11', value: 11800 },
|
||||
{ date: '05-12', value: 12400 },
|
||||
{ date: '05-13', value: 12000 },
|
||||
{ date: '05-14', value: 12300 },
|
||||
{ date: '05-15', value: 12500 }
|
||||
]
|
||||
},
|
||||
orderData: {
|
||||
todayOrders: 45,
|
||||
pendingOrders: 12,
|
||||
completedOrders: 156,
|
||||
orderAmount: 2580000,
|
||||
orderList: [
|
||||
{ orderNo: 'ORD20260515001', customer: '周口钢铁', amount: 125000, status: '生产中' },
|
||||
{ orderNo: 'ORD20260515002', customer: '南阳重工', amount: 89000, status: '已完成' },
|
||||
{ orderNo: 'ORD20260515003', customer: '洛阳机械', amount: 156000, status: '待生产' },
|
||||
{ orderNo: 'ORD20260515004', customer: '开封汽配', amount: 67000, status: '生产中' },
|
||||
{ orderNo: 'ORD20260515005', customer: '商丘金属', amount: 45000, status: '已完成' }
|
||||
]
|
||||
},
|
||||
costData: {
|
||||
totalCost: 1568000,
|
||||
materialCost: 985000,
|
||||
laborCost: 234000,
|
||||
energyCost: 189000,
|
||||
otherCost: 160000,
|
||||
costTrend: [
|
||||
{ month: '1月', value: 1420000 },
|
||||
{ month: '2月', value: 1380000 },
|
||||
{ month: '3月', value: 1520000 },
|
||||
{ month: '4月', value: 1490000 },
|
||||
{ month: '5月', value: 1568000 }
|
||||
]
|
||||
},
|
||||
screens: [
|
||||
{ id: 1, name: '酸轧数据大屏', path: '/screens/acid-rolling', status: 'running', createTime: '2026-05-10 10:00:00' },
|
||||
{ id: 2, name: '订单大屏', path: '/dashboard/order', status: 'stopped', createTime: '2026-05-11 14:30:00' },
|
||||
{ id: 3, name: '成本大屏', path: '/dashboard/cost', status: 'running', createTime: '2026-05-12 09:00:00' },
|
||||
{ id: 4, name: '能源大屏', path: '/dashboard/energy', status: 'running', createTime: '2026-05-13 11:00:00' }
|
||||
],
|
||||
dataSources: [
|
||||
{ id: 1, name: 'KLPL3主接口', type: 'api', url: 'http://klpl3-server/api', status: 'connected', createTime: '2026-05-01 08:00:00' },
|
||||
{ id: 2, name: 'WMS数据库', type: 'database', url: 'mysql://localhost:3306/wms', status: 'connected', createTime: '2026-05-02 10:00:00' },
|
||||
{ id: 3, name: 'EMS系统', type: 'api', url: 'http://ems-server/api', status: 'disconnected', createTime: '2026-05-03 14:00:00' }
|
||||
],
|
||||
users: [
|
||||
{ id: 1, username: 'admin', name: '管理员', role: '超级管理员', status: 'active', createTime: '2026-01-01 00:00:00' },
|
||||
{ id: 2, username: 'user1', name: '张三', role: '普通用户', status: 'active', createTime: '2026-02-15 10:00:00' },
|
||||
{ id: 3, username: 'user2', name: '李四', role: '普通用户', status: 'inactive', createTime: '2026-03-20 14:00:00' }
|
||||
]
|
||||
}
|
||||
|
||||
// 封装统一响应
|
||||
const sendResponse = (res, data, message = 'success') => {
|
||||
res.json({
|
||||
code: 200,
|
||||
message,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== OEE报表接口 ====================
|
||||
|
||||
// OEE日汇总
|
||||
app.get('/da/oee/summary', async (req, res) => {
|
||||
const { lineType = 'acid', startDate, endDate } = req.query
|
||||
try {
|
||||
if (masterPool) {
|
||||
let query = 'SELECT * FROM da_oee_summary WHERE line_type = ?'
|
||||
const params = [lineType]
|
||||
|
||||
if (startDate) {
|
||||
query += ' AND date >= ?'
|
||||
params.push(startDate)
|
||||
}
|
||||
if (endDate) {
|
||||
query += ' AND date <= ?'
|
||||
params.push(endDate)
|
||||
}
|
||||
|
||||
const [rows] = await masterPool.execute(query, params)
|
||||
if (rows.length > 0) {
|
||||
sendResponse(res, rows)
|
||||
return
|
||||
}
|
||||
}
|
||||
sendResponse(res, mockData.oeeData.summaryList)
|
||||
} catch (error) {
|
||||
console.error('OEE日汇总查询失败:', error.message)
|
||||
sendResponse(res, mockData.oeeData.summaryList)
|
||||
}
|
||||
})
|
||||
|
||||
// 7大损失汇总
|
||||
app.get('/da/oee/loss7', async (req, res) => {
|
||||
const { lineType = 'acid' } = req.query
|
||||
try {
|
||||
if (masterPool) {
|
||||
const [rows] = await masterPool.execute('SELECT * FROM da_oee_loss7 WHERE line_type = ?', [lineType])
|
||||
if (rows.length > 0) {
|
||||
sendResponse(res, rows)
|
||||
return
|
||||
}
|
||||
}
|
||||
sendResponse(res, mockData.oeeData.loss7List)
|
||||
} catch (error) {
|
||||
console.error('7大损失查询失败:', error.message)
|
||||
sendResponse(res, mockData.oeeData.loss7List)
|
||||
}
|
||||
})
|
||||
|
||||
// 停机事件明细
|
||||
app.get('/da/oee/events', async (req, res) => {
|
||||
const { lineType = 'acid', page = 1, size = 20 } = req.query
|
||||
try {
|
||||
if (masterPool) {
|
||||
const offset = (page - 1) * size
|
||||
const [rows] = await masterPool.execute(
|
||||
'SELECT * FROM da_oee_events WHERE line_type = ? ORDER BY event_time DESC LIMIT ? OFFSET ?',
|
||||
[lineType, size, offset]
|
||||
)
|
||||
sendResponse(res, {
|
||||
rows,
|
||||
total: rows.length + 100
|
||||
})
|
||||
return
|
||||
}
|
||||
sendResponse(res, {
|
||||
rows: mockData.oeeData.eventList,
|
||||
total: 50
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('停机事件查询失败:', error.message)
|
||||
sendResponse(res, {
|
||||
rows: mockData.oeeData.eventList,
|
||||
total: 50
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 理论节拍
|
||||
app.get('/da/oee/idealCycle', async (req, res) => {
|
||||
const { lineType = 'acid' } = req.query
|
||||
try {
|
||||
if (masterPool) {
|
||||
const [rows] = await masterPool.execute('SELECT * FROM da_oee_ideal_cycle WHERE line_type = ?', [lineType])
|
||||
if (rows.length > 0) {
|
||||
sendResponse(res, rows[0])
|
||||
return
|
||||
}
|
||||
}
|
||||
sendResponse(res, { lineType, idealCycle: 120, unit: 'm/min' })
|
||||
} catch (error) {
|
||||
console.error('理论节拍查询失败:', error.message)
|
||||
sendResponse(res, { lineType, idealCycle: 120, unit: 'm/min' })
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 大屏数据接口 ====================
|
||||
|
||||
// OEE大屏数据
|
||||
app.get('/api/wms/acid-rolling/dashboard/oee', async (req, res) => {
|
||||
const { lineType = 'acid' } = req.query
|
||||
try {
|
||||
if (masterPool && acidPool) {
|
||||
const [kpiRows] = await masterPool.execute('SELECT * FROM da_oee_kpi WHERE line_type = ? ORDER BY date DESC LIMIT 1', [lineType])
|
||||
const [summaryRows] = await masterPool.execute('SELECT * FROM da_oee_summary WHERE line_type = ? ORDER BY date DESC LIMIT 6', [lineType])
|
||||
const [lossRows] = await masterPool.execute('SELECT * FROM da_oee_loss7 WHERE line_type = ?', [lineType])
|
||||
const [eventRows] = await masterPool.execute('SELECT * FROM da_oee_events WHERE line_type = ? ORDER BY event_time DESC LIMIT 5', [lineType])
|
||||
|
||||
sendResponse(res, {
|
||||
kpi: kpiRows.length > 0 ? {
|
||||
oee: kpiRows[0].oee,
|
||||
availability: kpiRows[0].availability,
|
||||
performanceTon: kpiRows[0].performance_ton,
|
||||
quality: kpiRows[0].quality,
|
||||
totalOutputTon: kpiRows[0].total_output_ton,
|
||||
totalOutputCoil: kpiRows[0].total_output_coil,
|
||||
goodOutputTon: kpiRows[0].good_output_ton,
|
||||
targetOutputTon: 15000
|
||||
} : mockData.oeeData.kpi,
|
||||
summaryList: summaryRows.length > 0 ? summaryRows.map(r => ({
|
||||
date: r.date,
|
||||
oee: r.oee,
|
||||
availability: r.availability,
|
||||
performanceTon: r.performance_ton,
|
||||
quality: r.quality,
|
||||
totalOutputTon: r.total_output_ton,
|
||||
targetOutputTon: 2500,
|
||||
efficiency: r.efficiency
|
||||
})) : mockData.oeeData.summaryList,
|
||||
loss7List: lossRows.length > 0 ? lossRows.map(r => ({
|
||||
lossName: r.loss_name,
|
||||
lossTime: r.loss_time,
|
||||
lossRatio: r.loss_ratio,
|
||||
description: r.description
|
||||
})) : mockData.oeeData.loss7List,
|
||||
eventList: eventRows.length > 0 ? eventRows.map(r => ({
|
||||
eventTime: r.event_time,
|
||||
eventType: r.event_type,
|
||||
lossType: r.loss_type,
|
||||
duration: r.duration,
|
||||
reason: r.reason,
|
||||
handleStatus: r.handle_status
|
||||
})) : mockData.oeeData.eventList
|
||||
})
|
||||
return
|
||||
}
|
||||
sendResponse(res, mockData.oeeData)
|
||||
} catch (error) {
|
||||
console.error('OEE大屏数据查询失败:', error.message)
|
||||
sendResponse(res, mockData.oeeData)
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 酸轧打卷接口 ====================
|
||||
|
||||
// 分页查询酸轧打卷记录
|
||||
app.get('/pocket/acidTyping/page', async (req, res) => {
|
||||
const { page = 1, size = 20, batchNo, currentNo } = req.query
|
||||
try {
|
||||
if (acidPool) {
|
||||
let query = 'SELECT * FROM acid_typing WHERE 1=1'
|
||||
const params = []
|
||||
|
||||
if (batchNo) {
|
||||
query += ' AND batch_no LIKE ?'
|
||||
params.push(`%${batchNo}%`)
|
||||
}
|
||||
if (currentNo) {
|
||||
query += ' AND current_no LIKE ?'
|
||||
params.push(`%${currentNo}%`)
|
||||
}
|
||||
|
||||
query += ' ORDER BY create_time DESC LIMIT ? OFFSET ?'
|
||||
params.push(size, (page - 1) * size)
|
||||
|
||||
const [rows] = await acidPool.execute(query, params)
|
||||
sendResponse(res, {
|
||||
rows,
|
||||
total: rows.length + 500
|
||||
})
|
||||
return
|
||||
}
|
||||
sendResponse(res, {
|
||||
rows: mockData.acidRollingReport.details.slice(0, parseInt(size)),
|
||||
total: 156
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('酸轧打卷查询失败:', error.message)
|
||||
sendResponse(res, {
|
||||
rows: mockData.acidRollingReport.details.slice(0, parseInt(size)),
|
||||
total: 156
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 创建酸轧打卷记录
|
||||
app.post('/pocket/acidTyping', async (req, res) => {
|
||||
const data = req.body
|
||||
try {
|
||||
if (acidPool) {
|
||||
await acidPool.execute(
|
||||
'INSERT INTO acid_typing (batch_no, current_no, production_time, warehouse, quality_status, product_type, width, thickness, weight, length, stock_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[data.batchNo, data.currentNo, data.productionTime, data.warehouse, data.qualityStatus, data.productType, data.width, data.thickness, data.weight, data.length, data.stockStatus]
|
||||
)
|
||||
sendResponse(res, { id: Date.now() }, '创建成功')
|
||||
return
|
||||
}
|
||||
sendResponse(res, { id: Date.now() }, '创建成功(模拟)')
|
||||
} catch (error) {
|
||||
console.error('创建酸轧打卷失败:', error.message)
|
||||
sendResponse(res, { id: Date.now() }, '创建成功(模拟)')
|
||||
}
|
||||
})
|
||||
|
||||
// 获取酸轧打卷详情
|
||||
app.get('/pocket/acidTyping/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
try {
|
||||
if (acidPool) {
|
||||
const [rows] = await acidPool.execute('SELECT * FROM acid_typing WHERE id = ?', [id])
|
||||
if (rows.length > 0) {
|
||||
sendResponse(res, rows[0])
|
||||
return
|
||||
}
|
||||
}
|
||||
sendResponse(res, mockData.acidRollingReport.details[0])
|
||||
} catch (error) {
|
||||
console.error('获取酸轧打卷详情失败:', error.message)
|
||||
sendResponse(res, mockData.acidRollingReport.details[0])
|
||||
}
|
||||
})
|
||||
|
||||
// 更新酸轧打卷记录
|
||||
app.put('/pocket/acidTyping/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
const data = req.body
|
||||
try {
|
||||
if (acidPool) {
|
||||
await acidPool.execute(
|
||||
'UPDATE acid_typing SET batch_no=?, current_no=?, production_time=?, warehouse=?, quality_status=?, product_type=?, width=?, thickness=?, weight=?, length=?, stock_status=? WHERE id=?',
|
||||
[data.batchNo, data.currentNo, data.productionTime, data.warehouse, data.qualityStatus, data.productType, data.width, data.thickness, data.weight, data.length, data.stockStatus, id]
|
||||
)
|
||||
sendResponse(res, {}, '更新成功')
|
||||
return
|
||||
}
|
||||
sendResponse(res, {}, '更新成功(模拟)')
|
||||
} catch (error) {
|
||||
console.error('更新酸轧打卷失败:', error.message)
|
||||
sendResponse(res, {}, '更新成功(模拟)')
|
||||
}
|
||||
})
|
||||
|
||||
// 删除酸轧打卷记录
|
||||
app.delete('/pocket/acidTyping/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
try {
|
||||
if (acidPool) {
|
||||
await acidPool.execute('DELETE FROM acid_typing WHERE id = ?', [id])
|
||||
sendResponse(res, {}, '删除成功')
|
||||
return
|
||||
}
|
||||
sendResponse(res, {}, '删除成功(模拟)')
|
||||
} catch (error) {
|
||||
console.error('删除酸轧打卷失败:', error.message)
|
||||
sendResponse(res, {}, '删除成功(模拟)')
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== L2数据接口 ====================
|
||||
|
||||
// 酸轧L2报表数据
|
||||
app.get('/l2/report/acid', async (req, res) => {
|
||||
const { startDate, endDate } = req.query
|
||||
try {
|
||||
if (acidPool) {
|
||||
let query = 'SELECT * FROM l2_acid_report WHERE 1=1'
|
||||
const params = []
|
||||
|
||||
if (startDate) {
|
||||
query += ' AND report_date >= ?'
|
||||
params.push(startDate)
|
||||
}
|
||||
if (endDate) {
|
||||
query += ' AND report_date <= ?'
|
||||
params.push(endDate)
|
||||
}
|
||||
|
||||
const [rows] = await acidPool.execute(query, params)
|
||||
sendResponse(res, rows)
|
||||
return
|
||||
}
|
||||
sendResponse(res, mockData.acidRollingReport)
|
||||
} catch (error) {
|
||||
console.error('L2酸轧报表查询失败:', error.message)
|
||||
sendResponse(res, mockData.acidRollingReport)
|
||||
}
|
||||
})
|
||||
|
||||
// 酸轧生产计划
|
||||
app.get('/l2/plan/acid', async (req, res) => {
|
||||
try {
|
||||
if (acidPool) {
|
||||
const [rows] = await acidPool.execute('SELECT * FROM l2_production_plan WHERE line_type = "acid" AND is_active = 1')
|
||||
if (rows.length > 0) {
|
||||
sendResponse(res, rows[0])
|
||||
return
|
||||
}
|
||||
}
|
||||
sendResponse(res, mockData.currentPlan)
|
||||
} catch (error) {
|
||||
console.error('L2生产计划查询失败:', error.message)
|
||||
sendResponse(res, mockData.currentPlan)
|
||||
}
|
||||
})
|
||||
|
||||
// 酸轧停机记录
|
||||
app.get('/l2/stop/acid', async (req, res) => {
|
||||
const { date } = req.query
|
||||
try {
|
||||
if (acidPool) {
|
||||
let query = 'SELECT * FROM l2_stop_record WHERE line_type = "acid"'
|
||||
const params = []
|
||||
|
||||
if (date) {
|
||||
query += ' AND DATE(stop_time) = ?'
|
||||
params.push(date)
|
||||
}
|
||||
|
||||
const [rows] = await acidPool.execute(query, params)
|
||||
sendResponse(res, rows)
|
||||
return
|
||||
}
|
||||
sendResponse(res, mockData.acidStopReport)
|
||||
} catch (error) {
|
||||
console.error('L2停机记录查询失败:', error.message)
|
||||
sendResponse(res, mockData.acidStopReport)
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 大屏管理接口 ====================
|
||||
|
||||
app.get('/api/screens', async (req, res) => {
|
||||
try {
|
||||
if (masterPool) {
|
||||
const [rows] = await masterPool.execute('SELECT * FROM dashboard_screens')
|
||||
sendResponse(res, rows.map(r => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
path: r.path,
|
||||
status: r.status,
|
||||
createTime: r.create_time
|
||||
})))
|
||||
return
|
||||
}
|
||||
sendResponse(res, mockData.screens)
|
||||
} catch (error) {
|
||||
console.error('大屏列表查询失败:', error.message)
|
||||
sendResponse(res, mockData.screens)
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 系统菜单接口 ====================
|
||||
|
||||
const menuData = [
|
||||
{
|
||||
path: '/index',
|
||||
name: 'Dashboard',
|
||||
component: '/home/index.vue',
|
||||
meta: { title: '工作台', icon: 'dashboard' },
|
||||
children: []
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'DashboardGroup',
|
||||
component: '/home/index.vue',
|
||||
meta: { title: '数据大屏', icon: 'monitor' },
|
||||
children: [
|
||||
{ path: '/dashboard/demo', name: 'Demo', component: '/dashboard/demo/index.vue', meta: { title: '示例大屏', icon: 'example' }, children: [] },
|
||||
{ path: '/dashboard/order', name: 'Order', component: '/dashboard/order/index.vue', meta: { title: '订单大屏', icon: 'order' }, children: [] },
|
||||
{ path: '/dashboard/cost', name: 'Cost', component: '/dashboard/cost/index.vue', meta: { title: '成本大屏', icon: 'cost' }, children: [] },
|
||||
{ path: '/dashboard/energy', name: 'Energy', component: '/dashboard/energy/index.vue', meta: { title: '能源大屏', icon: 'energy' }, children: [] }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/screens',
|
||||
name: 'ScreensGroup',
|
||||
component: '/home/index.vue',
|
||||
meta: { title: '大屏管理', icon: 'pie-chart' },
|
||||
children: [
|
||||
{ path: '/screens', name: 'ScreenList', component: '/screens/index.vue', meta: { title: '大屏列表', icon: 'list' }, children: [] },
|
||||
{ path: '/screens/acid-rolling', name: 'AcidRolling', component: '/screens/acid-rolling/index.vue', meta: { title: '酸轧数据大屏', icon: 'chart' }, children: [] }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/reports',
|
||||
name: 'ReportsGroup',
|
||||
component: '/home/index.vue',
|
||||
meta: { title: '报表管理', icon: 'document' },
|
||||
children: [
|
||||
{ path: '/reports', name: 'ReportList', component: '/reports/index.vue', meta: { title: '报表列表', icon: 'list' }, children: [] },
|
||||
{ path: '/reports/acid-rolling', name: 'AcidRollingReport', component: '/reports/acid-rolling/index.vue', meta: { title: '酸轧产出报表', icon: 'output' }, children: [] },
|
||||
{ path: '/reports/acid-stop', name: 'AcidStopReport', component: '/reports/acid-stop/index.vue', meta: { title: '酸轧停机报表', icon: 'stop' }, children: [] }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
name: 'SystemGroup',
|
||||
component: '/home/index.vue',
|
||||
meta: { title: '系统管理', icon: 'setting' },
|
||||
children: [
|
||||
{ path: '/system/user', name: 'User', component: '/system/user/index.vue', meta: { title: '用户管理', icon: 'user' }, children: [] },
|
||||
{ path: '/system/role', name: 'Role', component: '/system/role/index.vue', meta: { title: '角色管理', icon: 'role' }, children: [] },
|
||||
{ path: '/system/menu', name: 'Menu', component: '/system/menu/index.vue', meta: { title: '菜单管理', icon: 'menu' }, children: [] },
|
||||
{ path: '/system/config', name: 'Config', component: '/system/config/index.vue', meta: { title: '系统配置', icon: 'config' }, children: [] }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/data-source',
|
||||
name: 'DataSource',
|
||||
component: '/data-source/index.vue',
|
||||
meta: { title: '数据源配置', icon: 'data-board' },
|
||||
children: []
|
||||
}
|
||||
]
|
||||
|
||||
app.get('/api/system/menu/list', async (req, res) => {
|
||||
try {
|
||||
if (masterPool) {
|
||||
const [rows] = await masterPool.execute('SELECT * FROM sys_menu ORDER BY order_num ASC')
|
||||
if (rows.length > 0) {
|
||||
const menuTree = buildMenuTree(rows)
|
||||
sendResponse(res, menuTree)
|
||||
return
|
||||
}
|
||||
}
|
||||
sendResponse(res, menuData)
|
||||
} catch (error) {
|
||||
console.error('菜单列表查询失败:', error.message)
|
||||
sendResponse(res, menuData)
|
||||
}
|
||||
})
|
||||
|
||||
function buildMenuTree(menuList) {
|
||||
const map = new Map()
|
||||
const roots = []
|
||||
|
||||
menuList.forEach(item => {
|
||||
map.set(item.id, {
|
||||
id: item.id,
|
||||
path: item.path || '/',
|
||||
name: item.name || '',
|
||||
component: item.component || '',
|
||||
meta: {
|
||||
title: item.title,
|
||||
icon: item.icon,
|
||||
breadcrumb: item.breadcrumb !== undefined ? item.breadcrumb : true
|
||||
},
|
||||
children: [],
|
||||
parentId: item.parent_id || 0
|
||||
})
|
||||
})
|
||||
|
||||
map.forEach(item => {
|
||||
if (item.parentId === 0) {
|
||||
roots.push(item)
|
||||
} else {
|
||||
const parent = map.get(item.parentId)
|
||||
if (parent) {
|
||||
parent.children.push(item)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return roots
|
||||
}
|
||||
|
||||
app.get('/api/system/menu/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
try {
|
||||
if (masterPool) {
|
||||
const [rows] = await masterPool.execute('SELECT * FROM sys_menu WHERE id = ?', [id])
|
||||
if (rows.length > 0) {
|
||||
sendResponse(res, rows[0])
|
||||
return
|
||||
}
|
||||
}
|
||||
sendResponse(res, menuData.find(m => m.path === `/system/menu/${id}`) || menuData[0])
|
||||
} catch (error) {
|
||||
console.error('菜单详情查询失败:', error.message)
|
||||
sendResponse(res, menuData[0])
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/system/menu', async (req, res) => {
|
||||
const data = req.body
|
||||
try {
|
||||
if (masterPool) {
|
||||
await masterPool.execute(
|
||||
'INSERT INTO sys_menu (parent_id, title, name, path, component, icon, order_num, breadcrumb, visible) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[data.parentId || 0, data.title, data.name, data.path, data.component, data.icon, data.orderNum || 0, data.breadcrumb !== undefined ? data.breadcrumb : true, data.visible !== undefined ? data.visible : true]
|
||||
)
|
||||
sendResponse(res, { id: Date.now() }, '创建成功')
|
||||
return
|
||||
}
|
||||
sendResponse(res, { id: Date.now() }, '创建成功(模拟)')
|
||||
} catch (error) {
|
||||
console.error('创建菜单失败:', error.message)
|
||||
sendResponse(res, { id: Date.now() }, '创建成功(模拟)')
|
||||
}
|
||||
})
|
||||
|
||||
app.put('/api/system/menu/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
const data = req.body
|
||||
try {
|
||||
if (masterPool) {
|
||||
await masterPool.execute(
|
||||
'UPDATE sys_menu SET parent_id=?, title=?, name=?, path=?, component=?, icon=?, order_num=?, breadcrumb=?, visible=? WHERE id=?',
|
||||
[data.parentId || 0, data.title, data.name, data.path, data.component, data.icon, data.orderNum || 0, data.breadcrumb !== undefined ? data.breadcrumb : true, data.visible !== undefined ? data.visible : true, id]
|
||||
)
|
||||
sendResponse(res, {}, '更新成功')
|
||||
return
|
||||
}
|
||||
sendResponse(res, {}, '更新成功(模拟)')
|
||||
} catch (error) {
|
||||
console.error('更新菜单失败:', error.message)
|
||||
sendResponse(res, {}, '更新成功(模拟)')
|
||||
}
|
||||
})
|
||||
|
||||
app.delete('/api/system/menu/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
try {
|
||||
if (masterPool) {
|
||||
await masterPool.execute('DELETE FROM sys_menu WHERE id = ?', [id])
|
||||
sendResponse(res, {}, '删除成功')
|
||||
return
|
||||
}
|
||||
sendResponse(res, {}, '删除成功(模拟)')
|
||||
} catch (error) {
|
||||
console.error('删除菜单失败:', error.message)
|
||||
sendResponse(res, {}, '删除成功(模拟)')
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 系统用户接口 ====================
|
||||
|
||||
app.get('/api/system/user/list', async (req, res) => {
|
||||
const { page = 1, size = 50, username, name } = req.query
|
||||
try {
|
||||
if (masterPool) {
|
||||
let query = 'SELECT * FROM sys_user WHERE 1=1'
|
||||
const params = []
|
||||
|
||||
if (username) {
|
||||
query += ' AND username LIKE ?'
|
||||
params.push(`%${username}%`)
|
||||
}
|
||||
if (name) {
|
||||
query += ' AND name LIKE ?'
|
||||
params.push(`%${name}%`)
|
||||
}
|
||||
|
||||
query += ' ORDER BY create_time DESC LIMIT ? OFFSET ?'
|
||||
params.push(size, (page - 1) * size)
|
||||
|
||||
const [rows] = await masterPool.execute(query, params)
|
||||
sendResponse(res, {
|
||||
rows: rows.map(r => ({
|
||||
id: r.id,
|
||||
username: r.username,
|
||||
name: r.name,
|
||||
role: r.role,
|
||||
status: r.status,
|
||||
createTime: r.create_time
|
||||
})),
|
||||
total: 100
|
||||
})
|
||||
return
|
||||
}
|
||||
sendResponse(res, {
|
||||
rows: mockData.users.slice(0, parseInt(size)),
|
||||
total: 100
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('用户列表查询失败:', error.message)
|
||||
sendResponse(res, {
|
||||
rows: mockData.users.slice(0, parseInt(size)),
|
||||
total: 100
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 启动服务 ====================
|
||||
|
||||
initDatabases().then(() => {
|
||||
app.listen(port, () => {
|
||||
console.log(`🚀 服务启动成功: http://localhost:${port}`)
|
||||
console.log(`📊 主数据库: ${masterPool ? '✅ 已连接' : '⚠️ 使用模拟数据'}`)
|
||||
console.log(`📊 酸轧数据库: ${acidPool ? '✅ 已连接' : '⚠️ 使用模拟数据'}`)
|
||||
console.log(`🔗 OEE大屏接口: http://localhost:${port}/api/wms/acid-rolling/dashboard/oee`)
|
||||
console.log(`🔗 OEE汇总接口: http://localhost:${port}/da/oee/summary`)
|
||||
console.log(`🔗 酸轧打卷接口: http://localhost:${port}/pocket/acidTyping/page`)
|
||||
})
|
||||
})
|
||||
9
src/App.vue
Normal file
9
src/App.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
24
src/api/acidRolling.js
Normal file
24
src/api/acidRolling.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import request from './request'
|
||||
|
||||
export const getAcidRollingSummary = () => {
|
||||
return request({
|
||||
url: '/acid-rolling/summary',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export const getAcidRollingDetail = params => {
|
||||
return request({
|
||||
url: '/acid-rolling/detail',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const getAcidRollingTrend = params => {
|
||||
return request({
|
||||
url: '/acid-rolling/trend',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
45
src/api/dataSource.js
Normal file
45
src/api/dataSource.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import request from './request'
|
||||
|
||||
export const getDataSourceList = () => {
|
||||
return request({
|
||||
url: '/data-source/list',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export const getDataSourceById = id => {
|
||||
return request({
|
||||
url: `/data-source/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export const createDataSource = data => {
|
||||
return request({
|
||||
url: '/data-source',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const updateDataSource = (id, data) => {
|
||||
return request({
|
||||
url: `/data-source/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteDataSource = id => {
|
||||
return request({
|
||||
url: `/data-source/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export const testConnection = id => {
|
||||
return request({
|
||||
url: `/data-source/${id}/test`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
48
src/api/report.js
Normal file
48
src/api/report.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import request from './request'
|
||||
|
||||
export const getReportList = params => {
|
||||
return request({
|
||||
url: '/report/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const getReportById = id => {
|
||||
return request({
|
||||
url: `/report/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export const createReport = data => {
|
||||
return request({
|
||||
url: '/report',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const updateReport = (id, data) => {
|
||||
return request({
|
||||
url: `/report/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteReport = id => {
|
||||
return request({
|
||||
url: `/report/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export const exportReport = (id, params) => {
|
||||
return request({
|
||||
url: `/report/${id}/export`,
|
||||
method: 'get',
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
41
src/api/request.js
Normal file
41
src/api/request.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const baseURL = process.env.VUE_APP_API_BASE_URL || '/api'
|
||||
|
||||
const service = axios.create({
|
||||
baseURL,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8'
|
||||
}
|
||||
})
|
||||
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
if (res.code !== 200) {
|
||||
console.error('请求失败:', res.message)
|
||||
return Promise.reject(new Error(res.message || 'Error'))
|
||||
}
|
||||
return res.data
|
||||
},
|
||||
error => {
|
||||
console.error('请求错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
||||
109
src/api/screen.js
Normal file
109
src/api/screen.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getDashboardOverview() {
|
||||
return request({
|
||||
url: '/api/dashboard/overview',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getScreenList() {
|
||||
return request({
|
||||
url: '/api/screens',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getOeeData(params) {
|
||||
return request({
|
||||
url: '/api/wms/acid-rolling/dashboard/oee',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getOeeSummary(params) {
|
||||
return request({
|
||||
url: '/da/oee/summary',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getOeeLoss7(params) {
|
||||
return request({
|
||||
url: '/da/oee/loss7',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getOeeEvents(params) {
|
||||
return request({
|
||||
url: '/da/oee/events',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getAcidRollingReport(params) {
|
||||
return request({
|
||||
url: '/l2/report/acid',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getAcidTypingPage(params) {
|
||||
return request({
|
||||
url: '/pocket/acidTyping/page',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getAcidTyping(id) {
|
||||
return request({
|
||||
url: `/pocket/acidTyping/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function createAcidTyping(data) {
|
||||
return request({
|
||||
url: '/pocket/acidTyping',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateAcidTyping(id, data) {
|
||||
return request({
|
||||
url: `/pocket/acidTyping/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteAcidTyping(id) {
|
||||
return request({
|
||||
url: `/pocket/acidTyping/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function getProductionPlan(params) {
|
||||
return request({
|
||||
url: '/l2/plan/acid',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getStopRecords(params) {
|
||||
return request({
|
||||
url: '/l2/stop/acid',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
100
src/api/system.js
Normal file
100
src/api/system.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getMenuList() {
|
||||
return request({
|
||||
url: '/api/system/menu/list',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getUserList(params) {
|
||||
return request({
|
||||
url: '/api/system/user/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function createUser(data) {
|
||||
return request({
|
||||
url: '/api/system/user',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateUser(id, data) {
|
||||
return request({
|
||||
url: `/api/system/user/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteUser(id) {
|
||||
return request({
|
||||
url: `/api/system/user/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function getRoleList(params) {
|
||||
return request({
|
||||
url: '/api/system/role/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function createRole(data) {
|
||||
return request({
|
||||
url: '/api/system/role',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateRole(id, data) {
|
||||
return request({
|
||||
url: `/api/system/role/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteRole(id) {
|
||||
return request({
|
||||
url: `/api/system/role/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function getMenuInfo(id) {
|
||||
return request({
|
||||
url: `/api/system/menu/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function createMenu(data) {
|
||||
return request({
|
||||
url: '/api/system/menu',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateMenu(id, data) {
|
||||
return request({
|
||||
url: `/api/system/menu/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteMenu(id) {
|
||||
return request({
|
||||
url: `/api/system/menu/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
55
src/layout/components/Breadcrumb/index.vue
Normal file
55
src/layout/components/Breadcrumb/index.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="breadcrumb-wrapper">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<template v-for="item in levelList" :key="item.path">
|
||||
<el-breadcrumb-item v-if="item.meta?.title && (item.meta.breadcrumb === undefined || item.meta.breadcrumb === true)" :to="item.path !== route.path ? { path: item.path } : ''">
|
||||
{{ item.meta.title }}
|
||||
</el-breadcrumb-item>
|
||||
</template>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const levelList = computed(() => {
|
||||
const matched = route.matched.filter(item => item.meta?.title)
|
||||
return matched
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.breadcrumb-wrapper {
|
||||
background: #ffffff;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid #e4e4e4;
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb) {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__item) {
|
||||
color: #666666;
|
||||
|
||||
&:hover {
|
||||
color: #4a90d9;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__item:last-child) {
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__separator) {
|
||||
color: #cccccc;
|
||||
margin: 0 8px;
|
||||
}
|
||||
</style>
|
||||
59
src/layout/components/Navbar/Hamburger.vue
Normal file
59
src/layout/components/Navbar/Hamburger.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<button class="hamburger" @click="toggleClick">
|
||||
<span class="hamburger-inner"></span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const emit = defineEmits(['toggle-click'])
|
||||
|
||||
const toggleClick = () => {
|
||||
emit('toggle-click')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hamburger {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.hamburger-inner {
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background-color: #606266;
|
||||
border-radius: 1px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background-color: #606266;
|
||||
border-radius: 1px;
|
||||
position: absolute;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -6px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: -6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
116
src/layout/components/Navbar/index.vue
Normal file
116
src/layout/components/Navbar/index.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<nav class="navbar">
|
||||
<div class="navbar-left">
|
||||
<hamburger @toggle-click="toggleSideBar" />
|
||||
<span class="navbar-title">{{ title }}</span>
|
||||
</div>
|
||||
<div class="navbar-right">
|
||||
<el-button link icon="Search" class="nav-btn">搜索</el-button>
|
||||
<el-button link icon="Refresh" class="nav-btn">刷新</el-button>
|
||||
<el-button link icon="FullScreen" class="nav-btn">全屏</el-button>
|
||||
<el-button link icon="Bell" class="nav-btn">
|
||||
<span class="badge">3</span>
|
||||
</el-button>
|
||||
<div class="user-info">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>管理员</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { User } from '@element-plus/icons-vue'
|
||||
import Hamburger from './Hamburger.vue'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const title = computed(() => store.state.settings.title)
|
||||
|
||||
const toggleSideBar = () => {
|
||||
store.dispatch('app/toggleSideBar')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
height: 50px;
|
||||
background: linear-gradient(90deg, #20b6f9, #2178f1);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 200px;
|
||||
z-index: 100;
|
||||
transition: left 0.28s ease;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
|
||||
.app-wrapper.sidebar-close & {
|
||||
left: 54px;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
color: #ffffff;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
font-size: 12px;
|
||||
background: #f56c6c;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
height: 36px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
margin-left: 8px;
|
||||
|
||||
:deep(.el-icon) {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
51
src/layout/components/Sidebar/Logo.vue
Normal file
51
src/layout/components/Sidebar/Logo.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="sidebar-logo-container">
|
||||
<router-link class="sidebar-logo-link" to="/">
|
||||
<div class="sidebar-logo">📊</div>
|
||||
<h1 v-show="!collapse" class="sidebar-title">{{ title }}</h1>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const title = '数据大屏管理系统'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar-logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: #2b3848;
|
||||
border-bottom: 1px solid #233040;
|
||||
}
|
||||
|
||||
.sidebar-logo-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
font-size: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
31
src/layout/components/Sidebar/SidebarItem.vue
Normal file
31
src/layout/components/Sidebar/SidebarItem.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div v-if="item.children?.length">
|
||||
<el-sub-menu :index="item.path">
|
||||
<template #title>
|
||||
<component :is="item.icon" class="svg-icon" />
|
||||
<span>{{ item.meta?.title || item.title }}</span>
|
||||
</template>
|
||||
<sidebar-item v-for="child in item.children" :key="child.path" :item="child" />
|
||||
</el-sub-menu>
|
||||
</div>
|
||||
<el-menu-item v-else :index="item.path">
|
||||
<component :is="item.icon" class="svg-icon" />
|
||||
<span>{{ item.meta?.title || item.title }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.svg-icon {
|
||||
font-size: 18px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
276
src/layout/components/Sidebar/index.vue
Normal file
276
src/layout/components/Sidebar/index.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div :class="['sidebar-container', { 'is-collapse': !sidebar.opened }]">
|
||||
<div class="sidebar-header">
|
||||
<div class="logo-wrapper">
|
||||
<span class="logo-icon">📊</span>
|
||||
<span v-show="sidebar.opened" class="logo-text">数据管理平台</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-scrollbar class="sidebar-scroll">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="!sidebar.opened"
|
||||
mode="vertical"
|
||||
class="sidebar-menu"
|
||||
router
|
||||
unique-opened
|
||||
>
|
||||
<template v-for="item in menuItems" :key="item.path">
|
||||
<el-menu-item v-if="!item.children || item.children.length === 0" :index="item.path">
|
||||
<el-icon :size="14"><component :is="getIcon(item.meta?.icon)" /></el-icon>
|
||||
<span>{{ item.meta?.title }}</span>
|
||||
</el-menu-item>
|
||||
<el-sub-menu v-else :index="item.path">
|
||||
<template #title>
|
||||
<el-icon :size="14"><component :is="getIcon(item.meta?.icon)" /></el-icon>
|
||||
<span>{{ item.meta?.title }}</span>
|
||||
</template>
|
||||
<template v-for="child in item.children" :key="child.path">
|
||||
<el-menu-item v-if="!child.children || child.children.length === 0" :index="child.path">
|
||||
<el-icon :size="12"><component :is="getIcon(child.meta?.icon)" /></el-icon>
|
||||
<span>{{ child.meta?.title }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { Refresh, FullScreen, User, Search } from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const store = useStore()
|
||||
|
||||
const sidebar = computed(() => store.state.app.sidebar)
|
||||
const activeMenu = computed(() => route.path)
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
path: '/index',
|
||||
meta: { title: '工作台', icon: 'home' }
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
meta: { title: '数据大屏', icon: 'monitor' },
|
||||
children: [
|
||||
{ path: '/dashboard/demo', meta: { title: '示例大屏', icon: 'chart' } },
|
||||
{ path: '/dashboard/order', meta: { title: '订单大屏', icon: 'chart' } },
|
||||
{ path: '/dashboard/cost', meta: { title: '成本大屏', icon: 'chart' } },
|
||||
{ path: '/dashboard/energy', meta: { title: '能源大屏', icon: 'monitor' } }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/screens',
|
||||
meta: { title: '大屏管理', icon: 'layout' },
|
||||
children: [
|
||||
{ path: '/screens', meta: { title: '大屏列表', icon: 'list' } },
|
||||
{ path: '/screens/acid-rolling', meta: { title: '酸轧数据大屏', icon: 'chart' } }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/reports',
|
||||
meta: { title: '报表管理', icon: 'document' },
|
||||
children: [
|
||||
{ path: '/reports', meta: { title: '报表列表', icon: 'list' } },
|
||||
{ path: '/reports/acid-rolling', meta: { title: '酸轧产出报表', icon: 'chart' } },
|
||||
{ path: '/reports/acid-stop', meta: { title: '酸轧停机报表', icon: 'settings' } }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
meta: { title: '系统管理', icon: 'settings' },
|
||||
children: [
|
||||
{ path: '/system/user', meta: { title: '用户管理', icon: 'user' } },
|
||||
{ path: '/system/role', meta: { title: '角色管理', icon: 'user' } },
|
||||
{ path: '/system/menu', meta: { title: '菜单管理', icon: 'user' } },
|
||||
{ path: '/system/config', meta: { title: '系统配置', icon: 'settings' } }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/data-source',
|
||||
meta: { title: '数据源配置', icon: 'database' }
|
||||
}
|
||||
]
|
||||
|
||||
const iconMap = {
|
||||
'home': Refresh,
|
||||
'monitor': FullScreen,
|
||||
'chart': Refresh,
|
||||
'document': Search,
|
||||
'settings': Refresh,
|
||||
'database': FullScreen,
|
||||
'user': User,
|
||||
'lock': User,
|
||||
'menu': Refresh,
|
||||
'layout': FullScreen,
|
||||
'list': Search
|
||||
}
|
||||
|
||||
const getIcon = (iconName) => {
|
||||
if (!iconName) return Refresh
|
||||
return iconMap[iconName] || Refresh
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar-container {
|
||||
width: 200px;
|
||||
background: #333333;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.28s ease;
|
||||
border-right: 1px solid #444444;
|
||||
|
||||
&.is-collapse {
|
||||
width: 54px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
background: #2d2d2d;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid #444444;
|
||||
|
||||
.logo-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.logo-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
:deep(.el-scrollbar__wrap) {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border: none;
|
||||
background: transparent;
|
||||
height: 100%;
|
||||
padding: 4px 0;
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
color: #999999;
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin: 0 6px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 2px;
|
||||
transition: all 0.2s ease;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
border-left: 3px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background: #444444 !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: #4a90d9 !important;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
border-left: 3px solid #5F7BA0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title) {
|
||||
color: #999999;
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin: 0 6px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 2px;
|
||||
transition: all 0.2s ease;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
border-left: 3px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background: #444444 !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: #4a90d9 !important;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
border-left: 3px solid #5F7BA0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu .el-menu-item) {
|
||||
padding-left: 40px !important;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
border-left: none;
|
||||
|
||||
&:hover {
|
||||
background: #3a3a3a !important;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: #3d5a7a !important;
|
||||
border-left: 3px solid #5F7BA0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-menu--popup) {
|
||||
background: #3a3a3a !important;
|
||||
border: 1px solid #444444;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 4px;
|
||||
|
||||
.el-menu-item {
|
||||
color: #999999;
|
||||
padding: 0 20px;
|
||||
font-size: 13px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
border-left: none;
|
||||
|
||||
&:hover {
|
||||
background: #444444 !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: #4a90d9 !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
113
src/layout/components/TagsView/index.vue
Normal file
113
src/layout/components/TagsView/index.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="tags-view-wrapper">
|
||||
<el-scrollbar class="tags-view-scroll">
|
||||
<el-tabs
|
||||
v-model="activeTag"
|
||||
type="card"
|
||||
closable
|
||||
@tab-remove="removeTag"
|
||||
@tab-click="handleTabClick"
|
||||
class="tags-view"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="tag in visitedViews"
|
||||
:key="tag.path"
|
||||
:label="tag.title"
|
||||
:name="tag.path"
|
||||
>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const visitedViews = ref([{ path: '/index', title: '工作台' }])
|
||||
|
||||
const activeTag = computed({
|
||||
get: () => route.path,
|
||||
set: (val) => {
|
||||
router.push(val)
|
||||
}
|
||||
})
|
||||
|
||||
const removeTag = (path) => {
|
||||
const index = visitedViews.value.findIndex(v => v.path === path)
|
||||
if (index > 0) {
|
||||
visitedViews.value.splice(index, 1)
|
||||
if (activeTag.value === path) {
|
||||
router.push(visitedViews.value[index - 1]?.path || '/index')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleTabClick = (tab) => {
|
||||
router.push(tab.props.name)
|
||||
}
|
||||
|
||||
watch(() => route.path, (newPath) => {
|
||||
const exists = visitedViews.value.some(v => v.path === newPath)
|
||||
if (!exists) {
|
||||
visitedViews.value.push({ path: newPath, title: route.meta?.title || '页面' })
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-view-wrapper {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
left: 200px;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
z-index: 99;
|
||||
transition: left 0.28s ease;
|
||||
|
||||
.app-wrapper.sidebar-close & {
|
||||
left: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.tags-view-scroll {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tags-view {
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
padding: 0 20px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-right: 8px;
|
||||
|
||||
&.is-active {
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs__active-bar) {
|
||||
background: #409eff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
121
src/layout/index.vue
Normal file
121
src/layout/index.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div :class="['app-wrapper', { 'sidebar-close': !sidebar.opened }]">
|
||||
<Sidebar />
|
||||
<div class="main-container">
|
||||
<Navbar />
|
||||
<TagsView />
|
||||
<Breadcrumb />
|
||||
<main class="app-main">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="cachedViews">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</transition>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import Sidebar from './components/Sidebar/index.vue'
|
||||
import Navbar from './components/Navbar/index.vue'
|
||||
import Breadcrumb from './components/Breadcrumb/index.vue'
|
||||
import TagsView from './components/TagsView/index.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const store = useStore()
|
||||
|
||||
const sidebar = computed(() => store.state.app.sidebar)
|
||||
const cachedViews = computed(() => store.state.tagsView?.cachedViews || [])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
height: 100%;
|
||||
font-family: 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.app-wrapper {
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
min-height: 100vh;
|
||||
transition: padding-left 0.28s ease;
|
||||
padding-left: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-wrapper.sidebar-close {
|
||||
.main-container {
|
||||
padding-left: 54px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-main {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
min-height: calc(100vh - 180px);
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
min-height: calc(100vh - 230px);
|
||||
}
|
||||
|
||||
.components-container {
|
||||
margin: 30px 50px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 30px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.fade-transform-enter-active,
|
||||
.fade-transform-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-transform-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
.fade-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.main-container {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.app-wrapper.sidebar-close .main-container {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.app-wrapper.sidebar-opened .main-container {
|
||||
padding-left: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
13
src/main.js
Normal file
13
src/main.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import './style.css'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
app.use(store)
|
||||
app.use(ElementPlus)
|
||||
app.mount('#app')
|
||||
43
src/modules/dashboardBig/assets/scss/_variables.scss
Normal file
43
src/modules/dashboardBig/assets/scss/_variables.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
$colors: (
|
||||
"primary": #1A5CD7,
|
||||
"info-1": #4394e4,
|
||||
"info": #4b67af,
|
||||
"white": #ffffff,
|
||||
"grey-1": #999999,
|
||||
"dark": #222222,
|
||||
"black-1": #171823,
|
||||
"icon": #5cd9e8,
|
||||
"success": #00d9a6,
|
||||
"warning": #ffa726,
|
||||
"danger": #ff5252,
|
||||
"light-blue": #00d4ff
|
||||
);
|
||||
|
||||
$screen-width: 1920px;
|
||||
$screen-height: 1080px;
|
||||
|
||||
$border-radius-sm: 4px;
|
||||
$border-radius-md: 8px;
|
||||
$border-radius-lg: 12px;
|
||||
|
||||
$shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
$shadow-md: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
$shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
|
||||
$spacing-xs: 4px;
|
||||
$spacing-sm: 8px;
|
||||
$spacing-md: 16px;
|
||||
$spacing-lg: 24px;
|
||||
$spacing-xl: 32px;
|
||||
|
||||
$font-size-xs: 12px;
|
||||
$font-size-sm: 14px;
|
||||
$font-size-md: 16px;
|
||||
$font-size-lg: 18px;
|
||||
$font-size-xl: 24px;
|
||||
$font-size-xxl: 32px;
|
||||
$font-size-title: 48px;
|
||||
|
||||
$transition-fast: 0.2s ease;
|
||||
$transition-normal: 0.3s ease;
|
||||
$transition-slow: 0.5s ease;
|
||||
396
src/modules/dashboardBig/assets/scss/style.scss
Normal file
396
src/modules/dashboardBig/assets/scss/style.scss
Normal file
@@ -0,0 +1,396 @@
|
||||
@import "./_variables.scss";
|
||||
|
||||
.dashboard-container {
|
||||
width: 100%;
|
||||
max-width: $screen-width;
|
||||
height: auto;
|
||||
min-height: calc(100vh - 180px);
|
||||
background: map-get($colors, "black-1");
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
border-radius: $border-radius-lg;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 20%, rgba(26, 92, 215, 0.15) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 80%, rgba(92, 217, 232, 0.1) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 $spacing-xl;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
.header-title {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: bold;
|
||||
color: map-get($colors, "white");
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
width: 4px;
|
||||
height: 24px;
|
||||
background: linear-gradient(180deg, map-get($colors, "primary"), map-get($colors, "icon"));
|
||||
border-radius: 2px;
|
||||
margin-right: $spacing-md;
|
||||
}
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-xl;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
color: map-get($colors, "grey-1");
|
||||
font-size: $font-size-sm;
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: map-get($colors, "icon");
|
||||
}
|
||||
}
|
||||
|
||||
.time-display {
|
||||
font-size: $font-size-lg;
|
||||
color: map-get($colors, "white");
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
grid-auto-rows: minmax(320px, auto);
|
||||
gap: $spacing-md;
|
||||
padding: $spacing-md;
|
||||
height: calc(100% - 80px);
|
||||
min-height: 500px;
|
||||
|
||||
.grid-left {
|
||||
grid-column: span 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.grid-center {
|
||||
grid-column: span 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.grid-right {
|
||||
grid-column: span 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.grid-top {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.grid-bottom {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.dashboard-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
|
||||
.grid-center,
|
||||
.grid-top,
|
||||
.grid-bottom {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: $border-radius-lg;
|
||||
padding: $spacing-md;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 200px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, map-get($colors, "primary"), map-get($colors, "icon"));
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
.card-title {
|
||||
font-size: $font-size-md;
|
||||
color: map-get($colors, "white");
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-action {
|
||||
color: map-get($colors, "grey-1");
|
||||
font-size: $font-size-xs;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: map-get($colors, "icon");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
height: calc(100% - 40px);
|
||||
min-height: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, rgba(26, 92, 215, 0.2) 0%, rgba(92, 217, 232, 0.1) 100%);
|
||||
border: 1px solid rgba(26, 92, 215, 0.3);
|
||||
|
||||
.stat-value {
|
||||
font-size: $font-size-title;
|
||||
font-weight: bold;
|
||||
color: map-get($colors, "white");
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: $font-size-sm;
|
||||
color: map-get($colors, "grey-1");
|
||||
margin-top: $spacing-xs;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-xs;
|
||||
margin-top: $spacing-sm;
|
||||
font-size: $font-size-xs;
|
||||
|
||||
&.up {
|
||||
color: map-get($colors, "success");
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: map-get($colors, "danger");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $spacing-sm 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.rank {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: $font-size-xs;
|
||||
font-weight: bold;
|
||||
margin-right: $spacing-sm;
|
||||
|
||||
&.top3 {
|
||||
background: linear-gradient(135deg, map-get($colors, "warning"), map-get($colors, "danger"));
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:not(.top3) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: map-get($colors, "grey-1");
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
|
||||
.name {
|
||||
font-size: $font-size-sm;
|
||||
color: map-get($colors, "white");
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: $font-size-xs;
|
||||
color: map-get($colors, "grey-1");
|
||||
}
|
||||
}
|
||||
|
||||
.trend {
|
||||
font-size: $font-size-xs;
|
||||
|
||||
&.up {
|
||||
color: map-get($colors, "success");
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: map-get($colors, "danger");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.process-panel {
|
||||
.process-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: $spacing-sm 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: $font-size-sm;
|
||||
color: map-get($colors, "grey-1");
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: $font-size-md;
|
||||
font-weight: 500;
|
||||
color: map-get($colors, "white");
|
||||
|
||||
.unit {
|
||||
font-size: $font-size-xs;
|
||||
color: map-get($colors, "grey-1");
|
||||
margin-left: $spacing-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-ring {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto;
|
||||
|
||||
.ring-bg {
|
||||
stroke: rgba(255, 255, 255, 0.1);
|
||||
stroke-width: 8;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.ring-progress {
|
||||
stroke: url(#progressGradient);
|
||||
stroke-width: 8;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: center;
|
||||
transition: stroke-dashoffset 0.5s ease;
|
||||
}
|
||||
|
||||
.ring-center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
|
||||
.ring-value {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: bold;
|
||||
color: map-get($colors, "white");
|
||||
}
|
||||
|
||||
.ring-label {
|
||||
font-size: $font-size-xs;
|
||||
color: map-get($colors, "grey-1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
th, td {
|
||||
padding: $spacing-sm;
|
||||
text-align: left;
|
||||
font-size: $font-size-xs;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
th {
|
||||
color: map-get($colors, "grey-1");
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
td {
|
||||
color: map-get($colors, "white");
|
||||
}
|
||||
|
||||
tr:hover td {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
380
src/modules/dashboardBig/views/cost.vue
Normal file
380
src/modules/dashboardBig/views/cost.vue
Normal file
@@ -0,0 +1,380 @@
|
||||
<template>
|
||||
<div class="big-screen">
|
||||
<header class="screen-header">
|
||||
<h1 class="title">成本数据大屏</h1>
|
||||
<span class="time">{{ currentTime }}</span>
|
||||
</header>
|
||||
|
||||
<main class="screen-body">
|
||||
<div class="card-grid">
|
||||
<div class="data-card" v-for="card in cards" :key="card.title">
|
||||
<div class="card-title">{{ card.title }}</div>
|
||||
<div class="card-value" :style="{ color: card.color }">{{ card.value }}</div>
|
||||
<div class="card-unit">{{ card.unit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-area">
|
||||
<div class="chart-box">
|
||||
<div class="box-title">成本趋势</div>
|
||||
<div ref="trendChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<div class="box-title">成本构成</div>
|
||||
<div ref="pieChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<div class="box-title">成本对比</div>
|
||||
<div class="compare-list">
|
||||
<div class="compare-item" v-for="item in compareList" :key="item.name">
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<div class="bar-container">
|
||||
<div class="bar-fill" :style="{ width: item.percent + '%', background: item.color }"></div>
|
||||
</div>
|
||||
<span class="value">{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-area">
|
||||
<div class="bottom-box">
|
||||
<div class="box-title">成本类型对比</div>
|
||||
<div class="cost-grid">
|
||||
<div class="cost-item" v-for="item in costItems" :key="item.name">
|
||||
<div class="cost-header">
|
||||
<span class="cost-name">{{ item.name }}</span>
|
||||
<span class="cost-percent">{{ item.percent }}%</span>
|
||||
</div>
|
||||
<div class="cost-bar">
|
||||
<div class="cost-fill" :style="{ width: item.percent + '%', background: item.color }"></div>
|
||||
</div>
|
||||
<div class="cost-value">{{ item.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-box">
|
||||
<div class="box-title">月度成本对比</div>
|
||||
<div ref="barChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="bottom-box">
|
||||
<div class="box-title">成本分析</div>
|
||||
<div class="analysis-list">
|
||||
<div class="analysis-item" v-for="item in analysisList" :key="item.label">
|
||||
<span class="label">{{ item.label }}</span>
|
||||
<span class="value" :style="{ color: item.color }">{{ item.value }}</span>
|
||||
<span class="change" :class="item.change > 0 ? 'up' : 'down'">{{ item.change > 0 ? '↑' : '↓' }} {{ Math.abs(item.change) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const currentTime = ref('')
|
||||
const trendChartRef = ref(null)
|
||||
const pieChartRef = ref(null)
|
||||
const barChartRef = ref(null)
|
||||
let trendChart = null
|
||||
let pieChart = null
|
||||
let barChart = null
|
||||
let timeInterval = null
|
||||
|
||||
const cards = ref([
|
||||
{ title: '总成本', value: '156.8', unit: '万', color: '#f56c6c' },
|
||||
{ title: '材料成本', value: '98.5', unit: '万', color: '#1A5CD7' },
|
||||
{ title: '人工成本', value: '23.4', unit: '万', color: '#5cd9e8' },
|
||||
{ title: '能源成本', value: '18.9', unit: '万', color: '#ff9800' }
|
||||
])
|
||||
|
||||
const compareList = ref([
|
||||
{ name: '材料成本', percent: 62.8, value: '98.5万', color: 'linear-gradient(90deg, #1A5CD7, #5cd9e8)' },
|
||||
{ name: '人工成本', percent: 14.9, value: '23.4万', color: 'linear-gradient(90deg, #5cd9e8, #33cea0)' },
|
||||
{ name: '能源成本', percent: 12.1, value: '18.9万', color: 'linear-gradient(90deg, #ff9800, #ffa726)' },
|
||||
{ name: '其他成本', percent: 10.2, value: '16.0万', color: 'linear-gradient(90deg, #f56c6c, #ff5252)' }
|
||||
])
|
||||
|
||||
const costItems = ref([
|
||||
{ name: '材料成本', percent: 62.8, value: '¥98.5万', color: '#1A5CD7' },
|
||||
{ name: '人工成本', percent: 14.9, value: '¥23.4万', color: '#5cd9e8' },
|
||||
{ name: '能源成本', percent: 12.1, value: '¥18.9万', color: '#ff9800' },
|
||||
{ name: '其他成本', percent: 10.2, value: '¥16.0万', color: '#f56c6c' }
|
||||
])
|
||||
|
||||
const analysisList = ref([
|
||||
{ label: '材料成本占比', value: '62.8%', change: 2.5, color: '#1A5CD7' },
|
||||
{ label: '人工成本占比', value: '14.9%', change: -1.2, color: '#5cd9e8' },
|
||||
{ label: '能源成本占比', value: '12.1%', change: 3.8, color: '#ff9800' },
|
||||
{ label: '成本同比增长', value: '8.5%', change: 5.2, color: '#67c23a' },
|
||||
{ label: '预算执行率', value: '92.3%', change: -1.5, color: '#9c27b0' },
|
||||
{ label: '成本节约率', value: '5.6%', change: 2.1, color: '#33cea0' }
|
||||
])
|
||||
|
||||
const updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString('zh-CN', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const initCharts = () => {
|
||||
if (trendChartRef.value) {
|
||||
trendChart = echarts.init(trendChartRef.value)
|
||||
trendChart.setOption({
|
||||
grid: { top: 30, right: 30, bottom: 30, left: 60 },
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||
axisLine: { lineStyle: { color: '#2a3f5c' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
||||
axisLabel: { color: '#999', formatter: (v) => (v / 10000).toFixed(0) + '万' }
|
||||
},
|
||||
series: [{
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [1420000, 1380000, 1520000, 1490000, 1568000, 1568000],
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(26,92,215,0.5)' },
|
||||
{ offset: 1, color: 'rgba(26,92,215,0.1)' }
|
||||
])
|
||||
},
|
||||
lineStyle: { color: '#1A5CD7', width: 3 },
|
||||
itemStyle: { color: '#1A5CD7' },
|
||||
symbol: 'circle',
|
||||
symbolSize: 8
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (pieChartRef.value) {
|
||||
pieChart = echarts.init(pieChartRef.value)
|
||||
pieChart.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { bottom: 10, textStyle: { color: '#999' } },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '40%'],
|
||||
data: [
|
||||
{ value: 62.8, name: '材料成本', itemStyle: { color: '#1A5CD7' } },
|
||||
{ value: 14.9, name: '人工成本', itemStyle: { color: '#5cd9e8' } },
|
||||
{ value: 12.1, name: '能源成本', itemStyle: { color: '#ff9800' } },
|
||||
{ value: 10.2, name: '其他成本', itemStyle: { color: '#f56c6c' } }
|
||||
],
|
||||
label: { show: false }
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (barChartRef.value) {
|
||||
barChart = echarts.init(barChartRef.value)
|
||||
barChart.setOption({
|
||||
grid: { top: 20, right: 20, bottom: 30, left: 50 },
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月'],
|
||||
axisLine: { lineStyle: { color: '#2a3f5c' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
||||
axisLabel: { color: '#999', formatter: (v) => (v / 10000).toFixed(0) + '万' }
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: [142, 138, 152, 149, 156.8],
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#1A5CD7' },
|
||||
{ offset: 1, color: '#5cd9e8' }
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
initCharts()
|
||||
window.addEventListener('resize', () => {
|
||||
trendChart?.resize()
|
||||
pieChart?.resize()
|
||||
barChart?.resize()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
trendChart?.dispose()
|
||||
pieChart?.dispose()
|
||||
barChart?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.big-screen {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: linear-gradient(135deg, #0a0e27 0%, #1a1f4e 100%);
|
||||
color: #d3d6dd;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 40px;
|
||||
|
||||
.title { font-size: 32px; font-weight: bold; color: #fff; text-shadow: 0 0 20px rgba(26, 92, 215, 0.8); letter-spacing: 4px; }
|
||||
.time { font-size: 18px; color: #5cd9e8; font-family: 'Courier New', monospace; }
|
||||
}
|
||||
|
||||
.screen-body { padding: 0 20px; }
|
||||
|
||||
.card-grid {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 20px 0;
|
||||
|
||||
.data-card {
|
||||
width: 22%;
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(26, 92, 215, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
|
||||
.card-title { font-size: 16px; color: #999; margin-bottom: 10px; }
|
||||
.card-value { font-size: 36px; font-weight: bold; margin-bottom: 5px; }
|
||||
.card-unit { font-size: 14px; color: #666; }
|
||||
}
|
||||
}
|
||||
|
||||
.chart-area {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 20px;
|
||||
padding: 20px 0;
|
||||
|
||||
.chart-box {
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(26, 92, 215, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
|
||||
.box-title { font-size: 16px; color: #5cd9e8; margin-bottom: 15px; padding-left: 10px; border-left: 3px solid #1A5CD7; }
|
||||
.chart { height: 280px; }
|
||||
|
||||
.compare-list {
|
||||
height: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
.compare-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.name { width: 80px; font-size: 14px; color: #d3d6dd; }
|
||||
.bar-container { flex: 1; height: 12px; background: rgba(255,255,255,0.1); border-radius: 6px; overflow: hidden; }
|
||||
.bar-fill { height: 100%; border-radius: 6px; transition: width 0.5s ease; }
|
||||
.value { width: 60px; text-align: right; font-size: 14px; color: #5cd9e8; font-weight: 600; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-area {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 20px;
|
||||
padding: 20px 0;
|
||||
|
||||
.bottom-box {
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(26, 92, 215, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
|
||||
.box-title { font-size: 16px; color: #5cd9e8; margin-bottom: 15px; padding-left: 10px; border-left: 3px solid #1A5CD7; }
|
||||
.chart { height: 220px; }
|
||||
}
|
||||
|
||||
.cost-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
|
||||
.cost-item {
|
||||
.cost-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.cost-name { font-size: 14px; color: #d3d6dd; }
|
||||
.cost-percent { font-size: 14px; color: #5cd9e8; font-weight: 600; }
|
||||
}
|
||||
|
||||
.cost-bar {
|
||||
height: 8px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.cost-fill { height: 100%; border-radius: 4px; transition: width 0.5s ease; }
|
||||
}
|
||||
|
||||
.cost-value { font-size: 14px; color: #999; }
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.analysis-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
background: rgba(26, 92, 215, 0.1);
|
||||
border-radius: 6px;
|
||||
|
||||
.label { font-size: 14px; color: #999; }
|
||||
.value { font-size: 16px; font-weight: bold; }
|
||||
.change {
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
&.up { background: rgba(103,194,58,0.2); color: #67c23a; }
|
||||
&.down { background: rgba(245,108,108,0.2); color: #f56c6c; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
435
src/modules/dashboardBig/views/energy.vue
Normal file
435
src/modules/dashboardBig/views/energy.vue
Normal file
@@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<div class="big-screen">
|
||||
<header class="screen-header">
|
||||
<h1 class="title">能源数据大屏</h1>
|
||||
<span class="time">{{ currentTime }}</span>
|
||||
</header>
|
||||
|
||||
<main class="screen-body">
|
||||
<div class="card-grid">
|
||||
<div class="data-card" v-for="card in cards" :key="card.title">
|
||||
<div class="card-icon" :style="{ background: card.iconBg }">{{ card.icon }}</div>
|
||||
<div class="card-title">{{ card.title }}</div>
|
||||
<div class="card-value" :style="{ color: card.color }">{{ card.value }}</div>
|
||||
<div class="card-unit">{{ card.unit }}</div>
|
||||
<div class="card-trend" :class="card.trend > 0 ? 'up' : 'down'">
|
||||
{{ card.trend > 0 ? '↑' : '↓' }} {{ Math.abs(card.trend) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-area">
|
||||
<div class="chart-box">
|
||||
<div class="box-title">能耗趋势</div>
|
||||
<div ref="trendChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<div class="box-title">能源构成</div>
|
||||
<div ref="pieChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<div class="box-title">实时能耗</div>
|
||||
<div class="realtime-container">
|
||||
<div class="gauge">
|
||||
<svg viewBox="0 0 100 60">
|
||||
<path d="M 10 55 A 40 40 0 0 1 90 55" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="8" stroke-linecap="round"/>
|
||||
<path d="M 10 55 A 40 40 0 0 1 90 55" fill="none" :stroke="gaugeColor" stroke-width="8" stroke-linecap="round"
|
||||
:stroke-dasharray="circumference" :stroke-dashoffset="dashOffset"/>
|
||||
</svg>
|
||||
<div class="gauge-value">{{ gaugeValue }}<span class="unit">kW</span></div>
|
||||
<div class="gauge-label">当前功率</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-area">
|
||||
<div class="bottom-box">
|
||||
<div class="box-title">分项能耗</div>
|
||||
<div class="energy-grid">
|
||||
<div class="energy-item" v-for="item in energyItems" :key="item.name">
|
||||
<div class="energy-header">
|
||||
<span class="energy-icon">{{ item.icon }}</span>
|
||||
<span class="energy-name">{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="energy-value" :style="{ color: item.color }">{{ item.value }}</div>
|
||||
<div class="energy-unit">{{ item.unit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-box">
|
||||
<div class="box-title">能耗对比</div>
|
||||
<div ref="barChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="bottom-box">
|
||||
<div class="box-title">能耗告警</div>
|
||||
<div class="alarm-list">
|
||||
<div class="alarm-item" v-for="alarm in alarmList" :key="alarm.time">
|
||||
<span class="alarm-icon">{{ alarm.icon }}</span>
|
||||
<div class="alarm-content">
|
||||
<div class="alarm-title">{{ alarm.title }}</div>
|
||||
<div class="alarm-value">{{ alarm.value }}</div>
|
||||
</div>
|
||||
<span class="alarm-time">{{ alarm.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const currentTime = ref('')
|
||||
const trendChartRef = ref(null)
|
||||
const pieChartRef = ref(null)
|
||||
const barChartRef = ref(null)
|
||||
let trendChart = null
|
||||
let pieChart = null
|
||||
let barChart = null
|
||||
let timeInterval = null
|
||||
|
||||
const gaugeValue = ref(856)
|
||||
const circumference = 2 * Math.PI * 40
|
||||
const dashOffset = computed(() => circumference * (1 - gaugeValue.value / 1000))
|
||||
const gaugeColor = computed(() => {
|
||||
if (gaugeValue.value > 800) return '#f56c6c'
|
||||
if (gaugeValue.value > 600) return '#ff9800'
|
||||
return '#67c23a'
|
||||
})
|
||||
|
||||
const cards = ref([
|
||||
{ title: '总能耗', value: '2,560', unit: 'kWh', icon: '⚡', iconBg: 'rgba(245,108,108,0.2)', color: '#f56c6c', trend: 3.5 },
|
||||
{ title: '电能', value: '1,820', unit: 'kWh', icon: '🔌', iconBg: 'rgba(26,92,215,0.2)', color: '#1A5CD7', trend: 2.1 },
|
||||
{ title: '天然气', value: '456', unit: 'm³', icon: '🔥', iconBg: 'rgba(255,152,0,0.2)', color: '#ff9800', trend: -1.2 },
|
||||
{ title: '水耗', value: '284', unit: 'm³', icon: '💧', iconBg: 'rgba(64,158,255,0.2)', color: '#409eff', trend: 0.8 }
|
||||
])
|
||||
|
||||
const energyItems = ref([
|
||||
{ name: '电能', value: '1,820', unit: 'kWh', icon: '⚡', color: '#1A5CD7' },
|
||||
{ name: '天然气', value: '456', unit: 'm³', icon: '🔥', color: '#ff9800' },
|
||||
{ name: '水', value: '284', unit: 'm³', icon: '💧', color: '#409eff' },
|
||||
{ name: '压缩空气', value: '156', unit: 'm³', icon: '💨', color: '#9c27b0' }
|
||||
])
|
||||
|
||||
const alarmList = ref([
|
||||
{ icon: '⚠️', title: '电能消耗偏高', value: '当前: 856kW / 阈值: 800kW', time: '14:30' },
|
||||
{ icon: '🔴', title: '天然气压力异常', value: '当前: 0.35MPa / 正常: 0.4-0.6MPa', time: '10:15' },
|
||||
{ icon: '⚠️', title: '水耗超出阈值', value: '当前: 284m³ / 阈值: 250m³', time: '09:00' }
|
||||
])
|
||||
|
||||
const updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString('zh-CN', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const updateGauge = () => {
|
||||
gaugeValue.value = 700 + Math.floor(Math.random() * 300)
|
||||
}
|
||||
|
||||
const initCharts = () => {
|
||||
if (trendChartRef.value) {
|
||||
trendChart = echarts.init(trendChartRef.value)
|
||||
trendChart.setOption({
|
||||
grid: { top: 30, right: 30, bottom: 30, left: 60 },
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'],
|
||||
axisLine: { lineStyle: { color: '#2a3f5c' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
series: [{
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [450, 380, 620, 850, 720, 680, 550],
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(245,108,108,0.5)' },
|
||||
{ offset: 1, color: 'rgba(245,108,108,0.1)' }
|
||||
])
|
||||
},
|
||||
lineStyle: { color: '#f56c6c', width: 3 },
|
||||
itemStyle: { color: '#f56c6c' },
|
||||
symbol: 'circle',
|
||||
symbolSize: 8
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (pieChartRef.value) {
|
||||
pieChart = echarts.init(pieChartRef.value)
|
||||
pieChart.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { bottom: 10, textStyle: { color: '#999' } },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '40%'],
|
||||
data: [
|
||||
{ value: 71.1, name: '电能', itemStyle: { color: '#1A5CD7' } },
|
||||
{ value: 17.8, name: '天然气', itemStyle: { color: '#ff9800' } },
|
||||
{ value: 11.1, name: '水', itemStyle: { color: '#409eff' } },
|
||||
{ value: 6.0, name: '压缩空气', itemStyle: { color: '#9c27b0' } }
|
||||
],
|
||||
label: { show: false }
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (barChartRef.value) {
|
||||
barChart = echarts.init(barChartRef.value)
|
||||
barChart.setOption({
|
||||
grid: { top: 20, right: 20, bottom: 30, left: 50 },
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
axisLine: { lineStyle: { color: '#2a3f5c' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: [2200, 2450, 2380, 2620, 2580, 1890, 1650],
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#f56c6c' },
|
||||
{ offset: 1, color: '#ff5252' }
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timeInterval = setInterval(() => {
|
||||
updateTime()
|
||||
updateGauge()
|
||||
}, 1000)
|
||||
initCharts()
|
||||
window.addEventListener('resize', () => {
|
||||
trendChart?.resize()
|
||||
pieChart?.resize()
|
||||
barChart?.resize()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
trendChart?.dispose()
|
||||
pieChart?.dispose()
|
||||
barChart?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.big-screen {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: linear-gradient(135deg, #0a0e27 0%, #1a1f4e 100%);
|
||||
color: #d3d6dd;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 40px;
|
||||
|
||||
.title { font-size: 32px; font-weight: bold; color: #fff; text-shadow: 0 0 20px rgba(26, 92, 215, 0.8); letter-spacing: 4px; }
|
||||
.time { font-size: 18px; color: #5cd9e8; font-family: 'Courier New', monospace; }
|
||||
}
|
||||
|
||||
.screen-body { padding: 0 20px; }
|
||||
|
||||
.card-grid {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 20px 0;
|
||||
|
||||
.data-card {
|
||||
width: 22%;
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(26, 92, 215, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
|
||||
.card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
|
||||
.card-title { font-size: 14px; color: #999; margin-bottom: 8px; }
|
||||
.card-value { font-size: 32px; font-weight: bold; margin-bottom: 5px; }
|
||||
.card-unit { font-size: 13px; color: #666; margin-bottom: 8px; }
|
||||
.card-trend {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
&.up { background: rgba(103,194,58,0.2); color: #67c23a; }
|
||||
&.down { background: rgba(245,108,108,0.2); color: #f56c6c; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-area {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 20px;
|
||||
padding: 20px 0;
|
||||
|
||||
.chart-box {
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(26, 92, 215, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
|
||||
.box-title { font-size: 16px; color: #5cd9e8; margin-bottom: 15px; padding-left: 10px; border-left: 3px solid #1A5CD7; }
|
||||
.chart { height: 280px; }
|
||||
|
||||
.realtime-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 280px;
|
||||
|
||||
.gauge {
|
||||
position: relative;
|
||||
width: 180px;
|
||||
height: 120px;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
.gauge-value {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -30%);
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #5cd9e8;
|
||||
|
||||
.unit {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
font-weight: normal;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.gauge-label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 40%);
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-area {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 20px;
|
||||
padding: 20px 0;
|
||||
|
||||
.bottom-box {
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(26, 92, 215, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
|
||||
.box-title { font-size: 16px; color: #5cd9e8; margin-bottom: 15px; padding-left: 10px; border-left: 3px solid #1A5CD7; }
|
||||
.chart { height: 220px; }
|
||||
}
|
||||
|
||||
.energy-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
|
||||
.energy-item {
|
||||
background: rgba(26, 92, 215, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
|
||||
.energy-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.energy-icon { font-size: 18px; }
|
||||
.energy-name { font-size: 14px; color: #999; }
|
||||
}
|
||||
|
||||
.energy-value { font-size: 24px; font-weight: bold; margin-bottom: 3px; }
|
||||
.energy-unit { font-size: 12px; color: #666; }
|
||||
}
|
||||
}
|
||||
|
||||
.alarm-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.alarm-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: rgba(230, 162, 60, 0.15);
|
||||
border-left: 4px solid #e6a23c;
|
||||
border-radius: 0 6px 6px 0;
|
||||
|
||||
.alarm-icon { font-size: 18px; margin-right: 12px; }
|
||||
.alarm-content {
|
||||
flex: 1;
|
||||
|
||||
.alarm-title { font-size: 14px; color: #fff; margin-bottom: 3px; }
|
||||
.alarm-value { font-size: 12px; color: #999; }
|
||||
}
|
||||
.alarm-time { font-size: 12px; color: #5cd9e8; }
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
554
src/modules/dashboardBig/views/index.vue
Normal file
554
src/modules/dashboardBig/views/index.vue
Normal file
@@ -0,0 +1,554 @@
|
||||
<template>
|
||||
<div class="big-screen">
|
||||
<!-- 顶部标题 -->
|
||||
<header class="screen-header">
|
||||
<h1 class="title">大数据可视化平台</h1>
|
||||
<span class="time">{{ currentTime }}</span>
|
||||
</header>
|
||||
|
||||
<!-- 主体区域 -->
|
||||
<main class="screen-body">
|
||||
<!-- 数据卡片区域 -->
|
||||
<div class="card-grid">
|
||||
<div class="data-card" v-for="card in cards" :key="card.title">
|
||||
<div class="card-title">{{ card.title }}</div>
|
||||
<div class="card-value" :style="{ color: card.color }">{{ card.value }}</div>
|
||||
<div class="card-unit">{{ card.unit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="chart-area">
|
||||
<div class="chart-box">
|
||||
<div class="box-title">产量趋势</div>
|
||||
<div ref="lineChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<div class="box-title">运行状态</div>
|
||||
<div ref="pieChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<div class="box-title">班组排名</div>
|
||||
<div class="ranking-list">
|
||||
<div class="ranking-item" v-for="(item, index) in rankingList" :key="item.name">
|
||||
<span class="rank" :class="'rank-' + (index + 1)">{{ index + 1 }}</span>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<span class="value">{{ item.value }}</span>
|
||||
<span class="unit">吨</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部区域 -->
|
||||
<div class="bottom-area">
|
||||
<div class="bottom-box">
|
||||
<div class="box-title">工艺参数</div>
|
||||
<div class="params-grid">
|
||||
<div class="param-item" v-for="param in paramsList" :key="param.label">
|
||||
<div class="param-label">{{ param.label }}</div>
|
||||
<div class="param-value" :style="{ color: param.color }">{{ param.value }}</div>
|
||||
<div class="param-unit">{{ param.unit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-box">
|
||||
<div class="box-title">生产进度</div>
|
||||
<div class="progress-container">
|
||||
<div class="progress-ring">
|
||||
<svg class="ring-svg" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="45" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="6"/>
|
||||
<circle cx="50" cy="50" r="45" fill="none" stroke="#1A5CD7" stroke-width="6"
|
||||
:stroke-dasharray="circumference"
|
||||
:stroke-dashoffset="dashOffset"
|
||||
stroke-linecap="round"
|
||||
transform="rotate(-90 50 50)"/>
|
||||
</svg>
|
||||
<div class="progress-text">
|
||||
<div class="progress-value">{{ progressValue }}%</div>
|
||||
<div class="progress-label">完成率</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-box">
|
||||
<div class="box-title">实时告警</div>
|
||||
<div class="alarm-list">
|
||||
<div class="alarm-item" v-for="alarm in alarmList" :key="alarm.time">
|
||||
<span class="alarm-icon">{{ alarm.icon }}</span>
|
||||
<div class="alarm-content">
|
||||
<div class="alarm-title">{{ alarm.title }}</div>
|
||||
<div class="alarm-time">{{ alarm.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const currentTime = ref('')
|
||||
const lineChartRef = ref(null)
|
||||
const pieChartRef = ref(null)
|
||||
let lineChart = null
|
||||
let pieChart = null
|
||||
let timeInterval = null
|
||||
|
||||
const circumference = 2 * Math.PI * 45
|
||||
const progressValue = ref(82.3)
|
||||
const dashOffset = computed(() => circumference * (1 - progressValue.value / 100))
|
||||
|
||||
const cards = ref([
|
||||
{ title: '今日产量', value: '12,580', unit: '吨', color: '#5cd9e8' },
|
||||
{ title: '设备利用率', value: '94.6', unit: '%', color: '#33cea0' },
|
||||
{ title: '产品良品率', value: '98.2', unit: '%', color: '#1A5CD7' },
|
||||
{ title: '今日能耗', value: '2,560', unit: 'kWh', color: '#ff9800' }
|
||||
])
|
||||
|
||||
const rankingList = ref([
|
||||
{ name: '班组A', value: 1250 },
|
||||
{ name: '班组B', value: 1180 },
|
||||
{ name: '班组C', value: 1050 },
|
||||
{ name: '班组D', value: 980 },
|
||||
{ name: '班组E', value: 850 }
|
||||
])
|
||||
|
||||
const paramsList = ref([
|
||||
{ label: '温度', value: '850', unit: '℃', color: '#ff9800' },
|
||||
{ label: '速度', value: '120', unit: 'm/min', color: '#5cd9e8' },
|
||||
{ label: '厚度', value: '2.0', unit: 'mm', color: '#33cea0' },
|
||||
{ label: '宽度', value: '1250', unit: 'mm', color: '#1A5CD7' },
|
||||
{ label: '张力', value: '150', unit: 'kN', color: '#ff5252' },
|
||||
{ label: '压力', value: '2.5', unit: 'MPa', color: '#9c27b0' }
|
||||
])
|
||||
|
||||
const alarmList = ref([
|
||||
{ icon: '⚠️', title: '电能消耗偏高', time: '14:30:00' },
|
||||
{ icon: '🔴', title: '天然气压力异常', time: '10:15:00' },
|
||||
{ icon: '⚠️', title: '水耗超出阈值', time: '09:00:00' }
|
||||
])
|
||||
|
||||
const updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const initCharts = () => {
|
||||
if (lineChartRef.value) {
|
||||
lineChart = echarts.init(lineChartRef.value)
|
||||
lineChart.setOption({
|
||||
grid: { top: 30, right: 20, bottom: 30, left: 50 },
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'line', lineStyle: { color: '#1A5CD7' } } },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||
axisLine: { lineStyle: { color: '#2a3f5c' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
series: [{
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [8500, 9200, 8800, 10500, 11200, 12580],
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(26,92,215,0.5)' },
|
||||
{ offset: 1, color: 'rgba(26,92,215,0.1)' }
|
||||
])
|
||||
},
|
||||
lineStyle: { color: '#1A5CD7', width: 3 },
|
||||
itemStyle: { color: '#1A5CD7' },
|
||||
symbol: 'circle',
|
||||
symbolSize: 8
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (pieChartRef.value) {
|
||||
pieChart = echarts.init(pieChartRef.value)
|
||||
pieChart.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { bottom: 10, textStyle: { color: '#999' } },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '40%'],
|
||||
data: [
|
||||
{ value: 45, name: '运行中', itemStyle: { color: '#67c23a' } },
|
||||
{ value: 35, name: '待机', itemStyle: { color: '#409eff' } },
|
||||
{ value: 15, name: '维护', itemStyle: { color: '#ff9800' } },
|
||||
{ value: 5, name: '故障', itemStyle: { color: '#f56c6c' } }
|
||||
],
|
||||
label: { show: false }
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
initCharts()
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
lineChart?.resize()
|
||||
pieChart?.resize()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
lineChart?.dispose()
|
||||
pieChart?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.big-screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 180px);
|
||||
background: linear-gradient(135deg, #0a0e27 0%, #1a1f4e 100%);
|
||||
color: #d3d6dd;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 30px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 20px rgba(26, 92, 215, 0.8);
|
||||
letter-spacing: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 16px;
|
||||
color: #5cd9e8;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.screen-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 15px 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 15px;
|
||||
padding: 15px 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
.data-card {
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(26, 92, 215, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card-unit {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-area {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
padding: 15px 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
.chart-box {
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(26, 92, 215, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.box-title {
|
||||
font-size: 14px;
|
||||
color: #5cd9e8;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 8px;
|
||||
border-left: 3px solid #1A5CD7;
|
||||
}
|
||||
|
||||
.chart {
|
||||
flex: 1;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
min-height: 200px;
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: rgba(26, 92, 215, 0.1);
|
||||
border-radius: 6px;
|
||||
|
||||
.rank {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
|
||||
&.rank-1 { background: linear-gradient(135deg, #ffd700, #ffb700); color: #fff; }
|
||||
&.rank-2 { background: linear-gradient(135deg, #c0c0c0, #a0a0a0); color: #fff; }
|
||||
&.rank-3 { background: linear-gradient(135deg, #cd7f32, #b87333); color: #fff; }
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: #d3d6dd;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #5cd9e8;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-area {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
padding: 15px 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
.bottom-box {
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(26, 92, 215, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.box-title {
|
||||
font-size: 14px;
|
||||
color: #5cd9e8;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 8px;
|
||||
border-left: 3px solid #1A5CD7;
|
||||
}
|
||||
}
|
||||
|
||||
.params-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
|
||||
.param-item {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: rgba(26, 92, 215, 0.1);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.param-label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.param-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.param-unit {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-height: 180px;
|
||||
|
||||
.progress-ring {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
|
||||
.ring-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
|
||||
.progress-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #5cd9e8;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alarm-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
min-height: 180px;
|
||||
|
||||
.alarm-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background: rgba(230, 162, 60, 0.15);
|
||||
border-left: 4px solid #e6a23c;
|
||||
border-radius: 0 6px 6px 0;
|
||||
|
||||
.alarm-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.alarm-content {
|
||||
flex: 1;
|
||||
|
||||
.alarm-title {
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.alarm-time {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.card-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.chart-area,
|
||||
.bottom-area {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.chart-area .chart-box:last-child,
|
||||
.bottom-area .bottom-box:last-child {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.card-grid,
|
||||
.chart-area,
|
||||
.bottom-area {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.chart-area .chart-box:last-child,
|
||||
.bottom-area .bottom-box:last-child {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.screen-header .title {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
310
src/modules/dashboardBig/views/order.vue
Normal file
310
src/modules/dashboardBig/views/order.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="big-screen">
|
||||
<header class="screen-header">
|
||||
<h1 class="title">订单数据大屏</h1>
|
||||
<span class="time">{{ currentTime }}</span>
|
||||
</header>
|
||||
|
||||
<main class="screen-body">
|
||||
<div class="card-grid">
|
||||
<div class="data-card" v-for="card in cards" :key="card.title">
|
||||
<div class="card-title">{{ card.title }}</div>
|
||||
<div class="card-value" :style="{ color: card.color }">{{ card.value }}</div>
|
||||
<div class="card-unit">{{ card.unit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-area">
|
||||
<div class="chart-box">
|
||||
<div class="box-title">订单趋势</div>
|
||||
<div ref="trendChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<div class="box-title">订单状态分布</div>
|
||||
<div ref="pieChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<div class="box-title">客户订单排行</div>
|
||||
<div class="ranking-list">
|
||||
<div class="ranking-item" v-for="(item, index) in rankingList" :key="item.name">
|
||||
<span class="rank" :class="'rank-' + (index + 1)">{{ index + 1 }}</span>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<span class="value">{{ item.value }}</span>
|
||||
<span class="unit">单</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-list-box">
|
||||
<div class="box-title">订单列表</div>
|
||||
<div class="order-table">
|
||||
<div class="table-header">
|
||||
<span>订单号</span>
|
||||
<span>客户</span>
|
||||
<span>金额</span>
|
||||
<span>状态</span>
|
||||
<span>时间</span>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<div class="table-row" v-for="order in orderList" :key="order.orderNo">
|
||||
<span class="order-no">{{ order.orderNo }}</span>
|
||||
<span>{{ order.customer }}</span>
|
||||
<span class="amount">{{ formatAmount(order.amount) }}</span>
|
||||
<span :class="['status', order.status]">{{ order.status }}</span>
|
||||
<span class="time">{{ order.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const currentTime = ref('')
|
||||
const trendChartRef = ref(null)
|
||||
const pieChartRef = ref(null)
|
||||
let trendChart = null
|
||||
let pieChart = null
|
||||
let timeInterval = null
|
||||
|
||||
const cards = ref([
|
||||
{ title: '今日订单', value: '45', unit: '单', color: '#5cd9e8' },
|
||||
{ title: '待处理订单', value: '12', unit: '单', color: '#ff9800' },
|
||||
{ title: '已完成订单', value: '156', unit: '单', color: '#67c23a' },
|
||||
{ title: '订单金额', value: '258', unit: '万元', color: '#1A5CD7' }
|
||||
])
|
||||
|
||||
const rankingList = ref([
|
||||
{ name: '周口钢铁', value: 125 },
|
||||
{ name: '南阳重工', value: 98 },
|
||||
{ name: '洛阳机械', value: 86 },
|
||||
{ name: '开封汽配', value: 72 },
|
||||
{ name: '商丘金属', value: 65 }
|
||||
])
|
||||
|
||||
const orderList = ref([
|
||||
{ orderNo: 'ORD20260515001', customer: '周口钢铁', amount: 125000, status: '生产中', time: '10:30' },
|
||||
{ orderNo: 'ORD20260515002', customer: '南阳重工', amount: 89000, status: '已完成', time: '09:45' },
|
||||
{ orderNo: 'ORD20260515003', customer: '洛阳机械', amount: 156000, status: '待生产', time: '11:20' },
|
||||
{ orderNo: 'ORD20260515004', customer: '开封汽配', amount: 67000, status: '生产中', time: '08:15' },
|
||||
{ orderNo: 'ORD20260515005', customer: '商丘金属', amount: 45000, status: '已完成', time: '07:30' }
|
||||
])
|
||||
|
||||
const formatAmount = (amount) => {
|
||||
return '¥' + (amount / 10000).toFixed(2) + '万'
|
||||
}
|
||||
|
||||
const updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString('zh-CN', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const initCharts = () => {
|
||||
if (trendChartRef.value) {
|
||||
trendChart = echarts.init(trendChartRef.value)
|
||||
trendChart.setOption({
|
||||
grid: { top: 30, right: 30, bottom: 30, left: 60 },
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||
axisLine: { lineStyle: { color: '#2a3f5c' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: [35, 42, 38, 45, 40, 48],
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#1A5CD7' },
|
||||
{ offset: 1, color: '#5cd9e8' }
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (pieChartRef.value) {
|
||||
pieChart = echarts.init(pieChartRef.value)
|
||||
pieChart.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { bottom: 10, textStyle: { color: '#999' } },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '40%'],
|
||||
data: [
|
||||
{ value: 45, name: '生产中', itemStyle: { color: '#409eff' } },
|
||||
{ value: 12, name: '待生产', itemStyle: { color: '#e6a23c' } },
|
||||
{ value: 156, name: '已完成', itemStyle: { color: '#67c23a' } }
|
||||
],
|
||||
label: { show: false }
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
initCharts()
|
||||
window.addEventListener('resize', () => {
|
||||
trendChart?.resize()
|
||||
pieChart?.resize()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
trendChart?.dispose()
|
||||
pieChart?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.big-screen {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: linear-gradient(135deg, #0a0e27 0%, #1a1f4e 100%);
|
||||
color: #d3d6dd;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 40px;
|
||||
|
||||
.title { font-size: 32px; font-weight: bold; color: #fff; text-shadow: 0 0 20px rgba(26, 92, 215, 0.8); letter-spacing: 4px; }
|
||||
.time { font-size: 18px; color: #5cd9e8; font-family: 'Courier New', monospace; }
|
||||
}
|
||||
|
||||
.screen-body { padding: 0 20px; }
|
||||
|
||||
.card-grid {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 20px 0;
|
||||
|
||||
.data-card {
|
||||
width: 22%;
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(26, 92, 215, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
|
||||
.card-title { font-size: 16px; color: #999; margin-bottom: 10px; }
|
||||
.card-value { font-size: 36px; font-weight: bold; margin-bottom: 5px; }
|
||||
.card-unit { font-size: 14px; color: #666; }
|
||||
}
|
||||
}
|
||||
|
||||
.chart-area {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 20px;
|
||||
padding: 20px 0;
|
||||
|
||||
.chart-box {
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(26, 92, 215, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
|
||||
.box-title { font-size: 16px; color: #5cd9e8; margin-bottom: 15px; padding-left: 10px; border-left: 3px solid #1A5CD7; }
|
||||
.chart { height: 280px; }
|
||||
|
||||
.ranking-list {
|
||||
height: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
background: rgba(26, 92, 215, 0.1);
|
||||
border-radius: 6px;
|
||||
|
||||
.rank {
|
||||
width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 12px; font-weight: bold; margin-right: 12px; background: rgba(255,255,255,0.1);
|
||||
&.rank-1 { background: linear-gradient(135deg, #ffd700, #ffb700); color: #fff; }
|
||||
&.rank-2 { background: linear-gradient(135deg, #c0c0c0, #a0a0a0); color: #fff; }
|
||||
&.rank-3 { background: linear-gradient(135deg, #cd7f32, #b87333); color: #fff; }
|
||||
}
|
||||
|
||||
.name { flex: 1; font-size: 14px; color: #d3d6dd; }
|
||||
.value { font-size: 16px; font-weight: bold; color: #5cd9e8; margin-right: 5px; }
|
||||
.unit { font-size: 12px; color: #666; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-list-box {
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(26, 92, 215, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-top: 20px;
|
||||
|
||||
.box-title { font-size: 16px; color: #5cd9e8; margin-bottom: 15px; padding-left: 10px; border-left: 3px solid #1A5CD7; }
|
||||
|
||||
.order-table {
|
||||
.table-header {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 2fr 1.5fr 1fr 1fr;
|
||||
gap: 15px;
|
||||
padding: 10px 15px;
|
||||
background: rgba(26, 92, 215, 0.2);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 2fr 1.5fr 1fr 1fr;
|
||||
gap: 15px;
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
font-size: 14px;
|
||||
|
||||
.order-no { color: #5cd9e8; font-family: 'Courier New', monospace; }
|
||||
.amount { color: #ff9800; font-weight: 600; }
|
||||
.status {
|
||||
padding: 2px 8px; border-radius: 4px; font-size: 12px; text-align: center;
|
||||
&.已完成 { background: rgba(103,194,58,0.2); color: #67c23a; }
|
||||
&.生产中 { background: rgba(64,158,255,0.2); color: #409eff; }
|
||||
&.待生产 { background: rgba(230,162,60,0.2); color: #e6a23c; }
|
||||
}
|
||||
.time { color: #999; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
156
src/router/index.js
Normal file
156
src/router/index.js
Normal file
@@ -0,0 +1,156 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
export const constantRoutes = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layout/index.vue'),
|
||||
redirect: '/index',
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: { title: '工作台', icon: 'dashboard' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/demo',
|
||||
redirect: '/dashboard/demo'
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
component: () => import('@/layout/index.vue'),
|
||||
meta: { title: '数据大屏', icon: 'dashboard' },
|
||||
children: [
|
||||
{
|
||||
path: 'demo',
|
||||
name: 'DashboardDemo',
|
||||
component: () => import('@/modules/dashboardBig/views/index.vue'),
|
||||
meta: { title: '示例大屏', icon: 'example' }
|
||||
},
|
||||
{
|
||||
path: 'order',
|
||||
name: 'OrderDashboard',
|
||||
component: () => import('@/modules/dashboardBig/views/order.vue'),
|
||||
meta: { title: '订单大屏', icon: 'order' }
|
||||
},
|
||||
{
|
||||
path: 'cost',
|
||||
name: 'CostDashboard',
|
||||
component: () => import('@/modules/dashboardBig/views/cost.vue'),
|
||||
meta: { title: '成本大屏', icon: 'cost' }
|
||||
},
|
||||
{
|
||||
path: 'energy',
|
||||
name: 'EnergyDashboard',
|
||||
component: () => import('@/modules/dashboardBig/views/energy.vue'),
|
||||
meta: { title: '能源大屏', icon: 'energy' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/screens',
|
||||
component: () => import('@/layout/index.vue'),
|
||||
meta: { title: '大屏管理', icon: 'monitor' },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'ScreenList',
|
||||
component: () => import('@/views/screens/index.vue'),
|
||||
meta: { title: '大屏列表', icon: 'list' }
|
||||
},
|
||||
{
|
||||
path: 'acid-rolling',
|
||||
name: 'AcidRollingScreen',
|
||||
component: () => import('@/views/screens/acid-rolling/index.vue'),
|
||||
meta: { title: '酸轧数据大屏', icon: 'chart' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/reports',
|
||||
component: () => import('@/layout/index.vue'),
|
||||
meta: { title: '报表管理', icon: 'document' },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'ReportList',
|
||||
component: () => import('@/views/reports/index.vue'),
|
||||
meta: { title: '报表列表', icon: 'list' }
|
||||
},
|
||||
{
|
||||
path: 'acid-rolling',
|
||||
name: 'AcidRollingReport',
|
||||
component: () => import('@/views/reports/acid-rolling/index.vue'),
|
||||
meta: { title: '酸轧产出报表', icon: 'output' }
|
||||
},
|
||||
{
|
||||
path: 'acid-stop',
|
||||
name: 'AcidStopReport',
|
||||
component: () => import('@/views/reports/acid-stop/index.vue'),
|
||||
meta: { title: '酸轧停机报表', icon: 'stop' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
component: () => import('@/layout/index.vue'),
|
||||
meta: { title: '系统管理', icon: 'system' },
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
name: 'UserManagement',
|
||||
component: () => import('@/views/system/user/index.vue'),
|
||||
meta: { title: '用户管理', icon: 'user' }
|
||||
},
|
||||
{
|
||||
path: 'role',
|
||||
name: 'RoleManagement',
|
||||
component: () => import('@/views/system/role/index.vue'),
|
||||
meta: { title: '角色管理', icon: 'role' }
|
||||
},
|
||||
{
|
||||
path: 'menu',
|
||||
name: 'MenuManagement',
|
||||
component: () => import('@/views/system/menu/index.vue'),
|
||||
meta: { title: '菜单管理', icon: 'menu' }
|
||||
},
|
||||
{
|
||||
path: 'config',
|
||||
name: 'SystemConfig',
|
||||
component: () => import('@/views/system/index.vue'),
|
||||
meta: { title: '系统配置', icon: 'config' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/data-source',
|
||||
component: () => import('@/layout/index.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'DataSource',
|
||||
component: () => import('@/views/data-source/index.vue'),
|
||||
meta: { title: '数据源配置', icon: 'database' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
scrollBehavior: () => ({ y: 0 }),
|
||||
routes: constantRoutes
|
||||
})
|
||||
|
||||
export function resetRouter() {
|
||||
const newRouter = createRouter({
|
||||
history: createWebHistory(),
|
||||
scrollBehavior: () => ({ y: 0 }),
|
||||
routes: constantRoutes
|
||||
})
|
||||
router.matcher = newRouter.matcher
|
||||
}
|
||||
|
||||
export default router
|
||||
12
src/store/index.js
Normal file
12
src/store/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createStore } from 'vuex'
|
||||
import app from './modules/app'
|
||||
import settings from './modules/settings'
|
||||
import permission from './modules/permission'
|
||||
|
||||
export default createStore({
|
||||
modules: {
|
||||
app,
|
||||
settings,
|
||||
permission
|
||||
}
|
||||
})
|
||||
40
src/store/modules/app.js
Normal file
40
src/store/modules/app.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const state = {
|
||||
sidebar: {
|
||||
opened: true,
|
||||
withoutAnimation: false
|
||||
},
|
||||
device: 'desktop'
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
TOGGLE_SIDEBAR: (state) => {
|
||||
state.sidebar.opened = !state.sidebar.opened
|
||||
state.sidebar.withoutAnimation = false
|
||||
},
|
||||
CLOSE_SIDEBAR: (state, withoutAnimation) => {
|
||||
state.sidebar.opened = false
|
||||
state.sidebar.withoutAnimation = withoutAnimation
|
||||
},
|
||||
TOGGLE_DEVICE: (state, device) => {
|
||||
state.device = device
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
toggleSideBar({ commit }) {
|
||||
commit('TOGGLE_SIDEBAR')
|
||||
},
|
||||
closeSideBar({ commit }, { withoutAnimation }) {
|
||||
commit('CLOSE_SIDEBAR', withoutAnimation)
|
||||
},
|
||||
toggleDevice({ commit }, device) {
|
||||
commit('TOGGLE_DEVICE', device)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
mutations,
|
||||
actions
|
||||
}
|
||||
73
src/store/modules/permission.js
Normal file
73
src/store/modules/permission.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import { constantRoutes } from '@/router'
|
||||
import { getMenuList } from '@/api/system'
|
||||
|
||||
const state = {
|
||||
routes: [],
|
||||
addRoutes: []
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
SET_ROUTES: (state, routes) => {
|
||||
state.addRoutes = routes
|
||||
state.routes = constantRoutes.concat(routes)
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
generateRoutes({ commit }) {
|
||||
return new Promise(resolve => {
|
||||
getMenuList().then(response => {
|
||||
const menuData = response.data || []
|
||||
const accessedRoutes = filterAsyncRoutes(menuData)
|
||||
commit('SET_ROUTES', accessedRoutes)
|
||||
resolve(accessedRoutes)
|
||||
}).catch(() => {
|
||||
commit('SET_ROUTES', constantRoutes)
|
||||
resolve(constantRoutes)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function filterAsyncRoutes(routes) {
|
||||
return routes.filter(route => {
|
||||
if (route.component && route.component !== '') {
|
||||
route.component = loadComponent(route.component)
|
||||
} else {
|
||||
route.component = () => import('@/views/home/index.vue')
|
||||
}
|
||||
if (route.children && route.children.length > 0) {
|
||||
route.children = filterAsyncRoutes(route.children)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
function loadComponent(componentPath) {
|
||||
const path = componentPath.replace(/^\//, '')
|
||||
const componentMap = {
|
||||
'home/index.vue': () => import('@/views/home/index.vue'),
|
||||
'dashboard/demo/index.vue': () => import('@/views/dashboard/demo/index.vue'),
|
||||
'dashboard/order/index.vue': () => import('@/views/dashboard/order/index.vue'),
|
||||
'dashboard/cost/index.vue': () => import('@/views/dashboard/cost/index.vue'),
|
||||
'dashboard/energy/index.vue': () => import('@/views/dashboard/energy/index.vue'),
|
||||
'screens/index.vue': () => import('@/views/screens/index.vue'),
|
||||
'screens/acid-rolling/index.vue': () => import('@/views/screens/acid-rolling/index.vue'),
|
||||
'reports/index.vue': () => import('@/views/reports/index.vue'),
|
||||
'reports/acid-rolling/index.vue': () => import('@/views/reports/acid-rolling/index.vue'),
|
||||
'reports/acid-stop/index.vue': () => import('@/views/reports/acid-stop/index.vue'),
|
||||
'system/user/index.vue': () => import('@/views/system/user/index.vue'),
|
||||
'system/role/index.vue': () => import('@/views/system/role/index.vue'),
|
||||
'system/menu/index.vue': () => import('@/views/system/menu/index.vue'),
|
||||
'system/config/index.vue': () => import('@/views/system/config/index.vue'),
|
||||
'data-source/index.vue': () => import('@/views/data-source/index.vue')
|
||||
}
|
||||
return componentMap[path] || (() => import('@/views/home/index.vue'))
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
mutations,
|
||||
actions
|
||||
}
|
||||
27
src/store/modules/settings.js
Normal file
27
src/store/modules/settings.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const state = {
|
||||
sidebarLogo: true,
|
||||
fixedHeader: true,
|
||||
tagsView: true,
|
||||
title: '数据大屏管理系统'
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
CHANGE_SETTING: (state, { key, value }) => {
|
||||
if (state.hasOwnProperty(key)) {
|
||||
state[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
changeSetting({ commit }, data) {
|
||||
commit('CHANGE_SETTING', data)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
mutations,
|
||||
actions
|
||||
}
|
||||
281
src/style.css
Normal file
281
src/style.css
Normal file
@@ -0,0 +1,281 @@
|
||||
/* 基础样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 布局类 */
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.components-container {
|
||||
margin: 30px 50px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 文字对齐 */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 浮动 */
|
||||
.fr {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.fl {
|
||||
float: left;
|
||||
}
|
||||
|
||||
/* 清除浮动 */
|
||||
.clearfix {
|
||||
&:after {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
font-size: 0;
|
||||
content: " ";
|
||||
clear: both;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 链接样式 */
|
||||
a, a:focus, a:hover {
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* 按钮栏 */
|
||||
.sub-navbar {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%);
|
||||
|
||||
.subtitle {
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* 筛选容器 */
|
||||
.filter-container {
|
||||
padding-bottom: 10px;
|
||||
|
||||
.filter-item {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 分页容器 */
|
||||
.pagination-container {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #d3dce6;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #99a9bf;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #667a8a;
|
||||
}
|
||||
|
||||
/* 卡片通用样式 */
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 按钮容器 */
|
||||
.btn-container {
|
||||
display: inline-block;
|
||||
|
||||
.el-button {
|
||||
margin-right: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
color: #67c23a;
|
||||
border-color: #67c23a;
|
||||
|
||||
&:hover {
|
||||
background: #e8f5e9;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
color: #f56c6c;
|
||||
border-color: #f56c6c;
|
||||
|
||||
&:hover {
|
||||
background: #fef0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-detail {
|
||||
color: #409eff;
|
||||
border-color: #409eff;
|
||||
|
||||
&:hover {
|
||||
background: #ecf5ff;
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.table-container {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
.el-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.el-table th {
|
||||
background: #f5f7fa;
|
||||
font-weight: bold;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.el-table td {
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.form-container {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
&.status-success {
|
||||
background: #e8f5e9;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.status-warning {
|
||||
background: #fef5e7;
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
&.status-danger {
|
||||
background: #fef0f0;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
&.status-info {
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
/* 图表容器 */
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 响应式布局 */
|
||||
@media screen and (max-width: 1200px) {
|
||||
.app-container {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 大屏专用样式 */
|
||||
.big-screen {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: #0a1628;
|
||||
color: #d3d6dd;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 全局过渡动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-left-enter-active,
|
||||
.slide-left-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-left-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
.slide-left-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
34
src/utils/request.js
Normal file
34
src/utils/request.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 15000
|
||||
})
|
||||
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
console.error('Request error:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
if (res.code === 200) {
|
||||
return res.data
|
||||
} else {
|
||||
console.error('Response error:', res.message)
|
||||
return Promise.reject(new Error(res.message || 'Error'))
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error('Response error:', error.message)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
||||
546
src/views/dashboard/cost/index.vue
Normal file
546
src/views/dashboard/cost/index.vue
Normal file
@@ -0,0 +1,546 @@
|
||||
<template>
|
||||
<div class="cost-screen">
|
||||
<!-- 顶部标题 -->
|
||||
<header class="screen-header">
|
||||
<h1 class="title">成本分析大屏</h1>
|
||||
<span class="time">{{ currentTime }}</span>
|
||||
</header>
|
||||
|
||||
<!-- 主体区域 -->
|
||||
<main class="screen-body">
|
||||
<!-- 成本概览卡片 -->
|
||||
<div class="card-grid">
|
||||
<div class="data-card" v-for="card in costOverview" :key="card.title">
|
||||
<div class="card-title">{{ card.title }}</div>
|
||||
<div class="card-value" :style="{ color: card.color }">{{ card.value }}</div>
|
||||
<div class="card-unit">{{ card.unit }}</div>
|
||||
<div class="card-trend" :class="card.trend > 0 ? 'up' : 'down'">
|
||||
{{ card.trend > 0 ? '↑' : '↓' }} {{ Math.abs(card.trend) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="chart-area">
|
||||
<div class="chart-box">
|
||||
<div class="box-title">成本构成分析</div>
|
||||
<div ref="pieChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<div class="box-title">月度成本趋势</div>
|
||||
<div ref="lineChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<div class="box-title">成本类型分布</div>
|
||||
<div ref="barChartRef" class="chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部区域 -->
|
||||
<div class="bottom-area">
|
||||
<div class="bottom-box">
|
||||
<div class="box-title">成本明细</div>
|
||||
<el-table :data="costDetails" border size="small" max-height="250">
|
||||
<el-table-column prop="item" label="成本项目" />
|
||||
<el-table-column prop="budget" label="预算" align="right" />
|
||||
<el-table-column prop="actual" label="实际" align="right" />
|
||||
<el-table-column prop="diff" label="差异" align="right">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.diff < 0 ? '#67c23a' : '#f56c6c' }">
|
||||
{{ scope.row.diff > 0 ? '+' : '' }}{{ scope.row.diff }}%
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="bottom-box">
|
||||
<div class="box-title">部门成本排名</div>
|
||||
<div class="ranking-list">
|
||||
<div class="ranking-item" v-for="(item, index) in deptRanking" :key="item.name">
|
||||
<span class="rank" :class="'rank-' + (index + 1)">{{ index + 1 }}</span>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<div class="bar-wrapper">
|
||||
<div class="bar-fill" :style="{ width: item.percent + '%', background: item.color }"></div>
|
||||
</div>
|
||||
<span class="value">{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-box">
|
||||
<div class="box-title">成本预警</div>
|
||||
<div class="alarm-list">
|
||||
<div class="alarm-item" v-for="alarm in alarms" :key="alarm.time" :class="alarm.level">
|
||||
<span class="alarm-icon">{{ alarm.icon }}</span>
|
||||
<div class="alarm-content">
|
||||
<div class="alarm-title">{{ alarm.title }}</div>
|
||||
<div class="alarm-time">{{ alarm.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const currentTime = ref('')
|
||||
const pieChartRef = ref(null)
|
||||
const lineChartRef = ref(null)
|
||||
const barChartRef = ref(null)
|
||||
let pieChart = null
|
||||
let lineChart = null
|
||||
let barChart = null
|
||||
let timeInterval = null
|
||||
|
||||
const costOverview = ref([
|
||||
{ title: '本月总成本', value: '1,568,000', unit: '元', color: '#f56c6c', trend: 3.2 },
|
||||
{ title: '单位成本', value: '2,850', unit: '元/吨', color: '#e6a23c', trend: -1.5 },
|
||||
{ title: '预算执行率', value: '96.8', unit: '%', color: '#67c23a', trend: 0.8 },
|
||||
{ title: '成本环比', value: '-2.3', unit: '%', color: '#409eff', trend: -2.3 }
|
||||
])
|
||||
|
||||
const costDetails = ref([
|
||||
{ item: '原材料成本', budget: '1,000,000', actual: '985,000', diff: -1.5 },
|
||||
{ item: '人工成本', budget: '250,000', actual: '234,000', diff: -6.4 },
|
||||
{ item: '能源成本', budget: '180,000', actual: '189,000', diff: 5.0 },
|
||||
{ item: '设备折旧', budget: '120,000', actual: '115,000', diff: -4.2 },
|
||||
{ item: '维修费用', budget: '80,000', actual: '45,000', diff: -43.8 }
|
||||
])
|
||||
|
||||
const deptRanking = ref([
|
||||
{ name: '生产部', value: '680,000', percent: 85, color: '#f56c6c' },
|
||||
{ name: '技术部', value: '320,000', percent: 40, color: '#e6a23c' },
|
||||
{ name: '质检部', value: '210,000', percent: 26, color: '#409eff' },
|
||||
{ name: '采购部', value: '185,000', percent: 23, color: '#67c23a' },
|
||||
{ name: '仓储部', value: '173,000', percent: 22, color: '#909399' }
|
||||
])
|
||||
|
||||
const alarms = ref([
|
||||
{ icon: '🔴', title: '能源成本超预算5%', time: '15:30:00', level: 'danger' },
|
||||
{ icon: '⚠️', title: '原材料成本接近预算上限', time: '10:20:00', level: 'warning' },
|
||||
{ icon: '⚠️', title: '维修费用大幅低于预算', time: '09:15:00', level: 'success' }
|
||||
])
|
||||
|
||||
const updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const initCharts = () => {
|
||||
if (pieChartRef.value) {
|
||||
pieChart = echarts.init(pieChartRef.value)
|
||||
pieChart.setOption({
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c}万 ({d}%)' },
|
||||
legend: { bottom: 10, textStyle: { color: '#999' } },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['45%', '75%'],
|
||||
center: ['50%', '45%'],
|
||||
data: [
|
||||
{ value: 985, name: '原材料', itemStyle: { color: '#f56c6c' } },
|
||||
{ value: 234, name: '人工', itemStyle: { color: '#e6a23c' } },
|
||||
{ value: 189, name: '能源', itemStyle: { color: '#409eff' } },
|
||||
{ value: 115, name: '折旧', itemStyle: { color: '#67c23a' } },
|
||||
{ value: 45, name: '维修', itemStyle: { color: '#909399' } }
|
||||
],
|
||||
label: { show: false }
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (lineChartRef.value) {
|
||||
lineChart = echarts.init(lineChartRef.value)
|
||||
lineChart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['实际', '预算'], bottom: 10, textStyle: { color: '#999' } },
|
||||
grid: { top: 20, right: 20, bottom: 40, left: 50 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||
axisLine: { lineStyle: { color: '#2a3f5c' } },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '实际',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [1450, 1520, 1380, 1610, 1580, 1568],
|
||||
lineStyle: { color: '#f56c6c', width: 2 },
|
||||
itemStyle: { color: '#f56c6c' }
|
||||
},
|
||||
{
|
||||
name: '预算',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [1500, 1500, 1500, 1500, 1500, 1500],
|
||||
lineStyle: { color: '#67c23a', width: 2, type: 'dashed' },
|
||||
itemStyle: { color: '#67c23a' }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
if (barChartRef.value) {
|
||||
barChart = echarts.init(barChartRef.value)
|
||||
barChart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { top: 20, right: 20, bottom: 30, left: 50 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['原材料', '人工', '能源', '折旧', '维修'],
|
||||
axisLine: { lineStyle: { color: '#2a3f5c' } },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: [985, 234, 189, 115, 45],
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#f56c6c' },
|
||||
{ offset: 1, color: '#e6a23c' }
|
||||
])
|
||||
},
|
||||
barWidth: '50%'
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
initCharts()
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
pieChart?.resize()
|
||||
lineChart?.resize()
|
||||
barChart?.resize()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
pieChart?.dispose()
|
||||
lineChart?.dispose()
|
||||
barChart?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cost-screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 180px);
|
||||
background: linear-gradient(135deg, #1a1f4e 0%, #2d1b3d 100%);
|
||||
color: #d3d6dd;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 30px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 20px rgba(230, 162, 60, 0.8);
|
||||
letter-spacing: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 16px;
|
||||
color: #e6a23c;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.screen-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 15px 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 15px;
|
||||
padding: 15px 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
.data-card {
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(230, 162, 60, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card-unit {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.card-trend {
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
|
||||
&.up { color: #f56c6c; }
|
||||
&.down { color: #67c23a; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-area {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
padding: 15px 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
.chart-box {
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(230, 162, 60, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.box-title {
|
||||
font-size: 14px;
|
||||
color: #e6a23c;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 8px;
|
||||
border-left: 3px solid #e6a23c;
|
||||
}
|
||||
|
||||
.chart {
|
||||
flex: 1;
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-area {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
padding: 15px 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
.bottom-box {
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(230, 162, 60, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.box-title {
|
||||
font-size: 14px;
|
||||
color: #e6a23c;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 8px;
|
||||
border-left: 3px solid #e6a23c;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
|
||||
th, td {
|
||||
padding: 8px 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: rgba(230, 162, 60, 0.1);
|
||||
border-radius: 6px;
|
||||
|
||||
.rank {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
|
||||
&.rank-1 { background: linear-gradient(135deg, #ffd700, #ffb700); color: #fff; }
|
||||
&.rank-2 { background: linear-gradient(135deg, #c0c0c0, #a0a0a0); color: #fff; }
|
||||
&.rank-3 { background: linear-gradient(135deg, #cd7f32, #b87333); color: #fff; }
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 60px;
|
||||
font-size: 13px;
|
||||
color: #d3d6dd;
|
||||
}
|
||||
|
||||
.bar-wrapper {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
margin: 0 10px;
|
||||
overflow: hidden;
|
||||
|
||||
.bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #e6a23c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alarm-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
.alarm-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid;
|
||||
|
||||
&.danger {
|
||||
background: rgba(245, 108, 108, 0.15);
|
||||
border-color: #f56c6c;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: rgba(230, 162, 60, 0.15);
|
||||
border-color: #e6a23c;
|
||||
}
|
||||
|
||||
&.success {
|
||||
background: rgba(103, 194, 58, 0.15);
|
||||
border-color: #67c23a;
|
||||
}
|
||||
|
||||
.alarm-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.alarm-content {
|
||||
flex: 1;
|
||||
|
||||
.alarm-title {
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.alarm-time {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.card-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.chart-area,
|
||||
.bottom-area {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.chart-area .chart-box:last-child,
|
||||
.bottom-area .bottom-box:last-child {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.card-grid,
|
||||
.chart-area,
|
||||
.bottom-area {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.chart-area .chart-box:last-child,
|
||||
.bottom-area .bottom-box:last-child {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.screen-header .title {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
448
src/views/dashboard/demo/index.vue
Normal file
448
src/views/dashboard/demo/index.vue
Normal file
@@ -0,0 +1,448 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-header">
|
||||
<h2 class="dashboard-title">示例大屏</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="refreshData">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新数据
|
||||
</el-button>
|
||||
<el-button @click="toggleFullscreen">
|
||||
<el-icon><FullScreen /></el-icon>
|
||||
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-body">
|
||||
<div class="stats-row">
|
||||
<div class="stat-card" v-for="stat in stats" :key="stat.title">
|
||||
<div class="stat-icon" :class="stat.iconClass">{{ stat.icon }}</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-title">{{ stat.title }}</div>
|
||||
<div class="stat-trend" :class="stat.trend > 0 ? 'up' : 'down'">
|
||||
{{ stat.trend > 0 ? '↑' : '↓' }} {{ Math.abs(stat.trend) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-row">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<h3>数据趋势</h3>
|
||||
</div>
|
||||
<div ref="lineChart" class="chart-content"></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<h3>分类占比</h3>
|
||||
</div>
|
||||
<div ref="pieChart" class="chart-content"></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<h3>实时排名</h3>
|
||||
</div>
|
||||
<div class="ranking-list">
|
||||
<div class="ranking-item" v-for="(item, index) in rankingData" :key="item.name">
|
||||
<span class="rank-badge" :class="'rank-' + (index + 1)">{{ index + 1 }}</span>
|
||||
<span class="rank-name">{{ item.name }}</span>
|
||||
<span class="rank-value">{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tables-row">
|
||||
<div class="table-card">
|
||||
<div class="table-header">
|
||||
<h3>数据列表</h3>
|
||||
<el-button link size="small">查看全部</el-button>
|
||||
</div>
|
||||
<el-table :data="tableData" border :height="280">
|
||||
<el-table-column prop="name" label="名称" width="150" />
|
||||
<el-table-column prop="value" label="数值" width="150" />
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === '正常' ? 'success' : 'warning'">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="time" label="时间" width="180" />
|
||||
<el-table-column prop="action" label="操作" width="120">
|
||||
<template #default>
|
||||
<el-button link size="small">详情</el-button>
|
||||
<el-button link size="small">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { Refresh, FullScreen } from '@element-plus/icons-vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const isFullscreen = ref(false)
|
||||
const lineChart = ref(null)
|
||||
const pieChart = ref(null)
|
||||
let lineChartInstance = null
|
||||
let pieChartInstance = null
|
||||
|
||||
const stats = [
|
||||
{ title: '总数据量', value: '128,543', icon: '📊', iconClass: 'icon-blue', trend: 12.5 },
|
||||
{ title: '处理完成', value: '98,341', icon: '✅', iconClass: 'icon-green', trend: 8.3 },
|
||||
{ title: '待处理', value: '30,202', icon: '⏳', iconClass: 'icon-orange', trend: -5.2 },
|
||||
{ title: '成功率', value: '98.7%', icon: '🎯', iconClass: 'icon-purple', trend: 2.1 }
|
||||
]
|
||||
|
||||
const rankingData = [
|
||||
{ name: '数据中心A', value: '12,580' },
|
||||
{ name: '数据中心B', value: '10,320' },
|
||||
{ name: '数据中心C', value: '8,760' },
|
||||
{ name: '数据中心D', value: '7,450' },
|
||||
{ name: '数据中心E', value: '6,890' }
|
||||
]
|
||||
|
||||
const tableData = [
|
||||
{ name: '订单数据', value: '12,345', status: '正常', time: '2024-01-15 10:30' },
|
||||
{ name: '用户数据', value: '8,765', status: '正常', time: '2024-01-15 10:28' },
|
||||
{ name: '日志数据', value: '45,678', status: '正常', time: '2024-01-15 10:25' },
|
||||
{ name: '设备数据', value: '3,210', status: '警告', time: '2024-01-15 10:20' },
|
||||
{ name: '系统数据', value: '15,890', status: '正常', time: '2024-01-15 10:15' },
|
||||
{ name: '监控数据', value: '22,456', status: '正常', time: '2024-01-15 10:10' },
|
||||
{ name: '告警数据', value: '1,234', status: '警告', time: '2024-01-15 10:05' },
|
||||
{ name: '报表数据', value: '5,678', status: '正常', time: '2024-01-15 10:00' }
|
||||
]
|
||||
|
||||
const refreshData = () => {
|
||||
console.log('刷新数据')
|
||||
}
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
}
|
||||
|
||||
const initCharts = () => {
|
||||
if (lineChart.value) {
|
||||
lineChartInstance = echarts.init(lineChart.value)
|
||||
lineChartInstance.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
|
||||
},
|
||||
yAxis: { type: 'value' },
|
||||
series: [{
|
||||
name: '数据量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [1200, 1320, 1010, 1340, 1900, 2300, 2200, 1800, 1900, 2300, 2900, 3300],
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(64, 158, 255, 0.05)' }
|
||||
])
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (pieChart.value) {
|
||||
pieChartInstance = echarts.init(pieChart.value)
|
||||
pieChartInstance.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { orient: 'vertical', right: '5%', top: 'center' },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['35%', '50%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { show: false, position: 'center' },
|
||||
emphasis: {
|
||||
label: { show: true, fontSize: 18, fontWeight: 'bold' }
|
||||
},
|
||||
labelLine: { show: false },
|
||||
data: [
|
||||
{ value: 35, name: '类型A', itemStyle: { color: '#409EFF' } },
|
||||
{ value: 25, name: '类型B', itemStyle: { color: '#67C23A' } },
|
||||
{ value: 20, name: '类型C', itemStyle: { color: '#E6A23C' } },
|
||||
{ value: 15, name: '类型D', itemStyle: { color: '#F56C6C' } },
|
||||
{ value: 5, name: '其他', itemStyle: { color: '#909399' } }
|
||||
]
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
lineChartInstance?.resize()
|
||||
pieChartInstance?.resize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCharts()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
lineChartInstance?.dispose()
|
||||
pieChartInstance?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard-container {
|
||||
width: 100%;
|
||||
max-width: 1920px;
|
||||
height: auto;
|
||||
min-height: calc(100vh - 180px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
|
||||
&.icon-blue { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
||||
&.icon-green { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); }
|
||||
&.icon-orange { background: linear-gradient(135deg, #fc4a1a 0%, #f7b733 100%); }
|
||||
&.icon-purple { background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); }
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
|
||||
&.up { color: #67C23A; }
|
||||
&.down { color: #F56C6C; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 320px;
|
||||
|
||||
.chart-header {
|
||||
margin-bottom: 15px;
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
flex: 1;
|
||||
min-height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-top: 10px;
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
|
||||
.rank-badge {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
|
||||
&.rank-1 { background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%); }
|
||||
&.rank-2 { background: linear-gradient(135deg, #fd79a8 0%, #e84393 100%); }
|
||||
&.rank-3 { background: linear-gradient(135deg, #a29bfe 0%, #6c5ce7 100%); }
|
||||
&.rank-4, &.rank-5 { background: #909399; }
|
||||
}
|
||||
|
||||
.rank-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.rank-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #409EFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tables-row {
|
||||
flex: 1;
|
||||
min-height: 350px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #ebf0f5;
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.stats-row {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
.chart-card:last-child {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.stats-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
.chart-card:last-child {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
588
src/views/dashboard/energy/index.vue
Normal file
588
src/views/dashboard/energy/index.vue
Normal file
@@ -0,0 +1,588 @@
|
||||
<template>
|
||||
<div class="energy-screen">
|
||||
<!-- 顶部标题 -->
|
||||
<header class="screen-header">
|
||||
<h1 class="title">能源监控大屏</h1>
|
||||
<span class="time">{{ currentTime }}</span>
|
||||
</header>
|
||||
|
||||
<!-- 主体区域 -->
|
||||
<main class="screen-body">
|
||||
<!-- 能源概览卡片 -->
|
||||
<div class="card-grid">
|
||||
<div class="data-card" v-for="card in energyOverview" :key="card.title">
|
||||
<div class="card-title">{{ card.title }}</div>
|
||||
<div class="card-value" :style="{ color: card.color }">{{ card.value }}</div>
|
||||
<div class="card-unit">{{ card.unit }}</div>
|
||||
<div class="card-trend" :class="card.trend > 0 ? 'up' : 'down'">
|
||||
{{ card.trend > 0 ? '↑' : '↓' }} {{ Math.abs(card.trend) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="chart-area">
|
||||
<div class="chart-box">
|
||||
<div class="box-title">实时用电量</div>
|
||||
<div ref="gaugeChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<div class="box-title">能源消耗趋势</div>
|
||||
<div ref="lineChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<div class="box-title">能源类型分布</div>
|
||||
<div ref="pieChartRef" class="chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部区域 -->
|
||||
<div class="bottom-area">
|
||||
<div class="bottom-box">
|
||||
<div class="box-title">设备能耗排名</div>
|
||||
<div class="ranking-list">
|
||||
<div class="ranking-item" v-for="(item, index) in equipmentRanking" :key="item.name">
|
||||
<span class="rank" :class="'rank-' + (index + 1)">{{ index + 1 }}</span>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<div class="bar-wrapper">
|
||||
<div class="bar-fill" :style="{ width: item.percent + '%', background: item.color }"></div>
|
||||
</div>
|
||||
<span class="value">{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-box">
|
||||
<div class="box-title">生产班次能耗</div>
|
||||
<div ref="barChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="bottom-box">
|
||||
<div class="box-title">实时告警</div>
|
||||
<div class="alarm-list">
|
||||
<div class="alarm-item" v-for="alarm in alarms" :key="alarm.time" :class="alarm.level">
|
||||
<span class="alarm-icon">{{ alarm.icon }}</span>
|
||||
<div class="alarm-content">
|
||||
<div class="alarm-title">{{ alarm.title }}</div>
|
||||
<div class="alarm-time">{{ alarm.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const currentTime = ref('')
|
||||
const gaugeChartRef = ref(null)
|
||||
const lineChartRef = ref(null)
|
||||
const pieChartRef = ref(null)
|
||||
const barChartRef = ref(null)
|
||||
let gaugeChart = null
|
||||
let lineChart = null
|
||||
let pieChart = null
|
||||
let barChart = null
|
||||
let timeInterval = null
|
||||
|
||||
const energyOverview = ref([
|
||||
{ title: '今日用电', value: '25,600', unit: 'kWh', color: '#5cd9e8', trend: 5.2 },
|
||||
{ title: '今日用气', value: '1,850', unit: 'm³', color: '#33cea0', trend: -2.1 },
|
||||
{ title: '今日用水', value: '320', unit: 't', color: '#409eff', trend: 1.8 },
|
||||
{ title: '综合能耗', value: '38.5', unit: 'kgce/t', color: '#ff9800', trend: -3.5 }
|
||||
])
|
||||
|
||||
const equipmentRanking = ref([
|
||||
{ name: '酸轧机组', value: '8,500', percent: 85, color: '#f56c6c' },
|
||||
{ name: '退火炉', value: '5,200', percent: 52, color: '#e6a23c' },
|
||||
{ name: '轧机', value: '4,800', percent: 48, color: '#409eff' },
|
||||
{ name: '卷取机', value: '3,100', percent: 31, color: '#67c23a' },
|
||||
{ name: '运输链', value: '2,400', percent: 24, color: '#909399' }
|
||||
])
|
||||
|
||||
const alarms = ref([
|
||||
{ icon: '🔴', title: '用电负荷超限', time: '15:45:00', level: 'danger' },
|
||||
{ icon: '⚠️', title: '空压机效率下降', time: '12:30:00', level: 'warning' },
|
||||
{ icon: '✅', title: '能源回收系统正常', time: '10:00:00', level: 'success' }
|
||||
])
|
||||
|
||||
const updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const initCharts = () => {
|
||||
if (gaugeChartRef.value) {
|
||||
gaugeChart = echarts.init(gaugeChartRef.value)
|
||||
gaugeChart.setOption({
|
||||
series: [{
|
||||
type: 'gauge',
|
||||
startAngle: 200,
|
||||
endAngle: -20,
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitNumber: 10,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#67c23a' },
|
||||
{ offset: 0.5, color: '#e6a23c' },
|
||||
{ offset: 1, color: '#f56c6c' }
|
||||
])
|
||||
},
|
||||
progress: {
|
||||
show: true,
|
||||
width: 20
|
||||
},
|
||||
pointer: { show: false },
|
||||
axisLine: {
|
||||
lineStyle: { width: 20, color: [[1, 'rgba(255,255,255,0.1)']] }
|
||||
},
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { show: false },
|
||||
anchor: { show: false },
|
||||
title: { show: false },
|
||||
detail: {
|
||||
valueAnimation: true,
|
||||
fontSize: 32,
|
||||
color: '#5cd9e8',
|
||||
offsetCenter: [0, '30%'],
|
||||
formatter: '{value}%'
|
||||
},
|
||||
data: [{ value: 68 }]
|
||||
}, {
|
||||
type: 'gauge',
|
||||
startAngle: 200,
|
||||
endAngle: -20,
|
||||
min: 0,
|
||||
max: 100,
|
||||
itemStyle: {
|
||||
color: 'rgba(92, 217, 232, 0.2)'
|
||||
},
|
||||
progress: { show: false },
|
||||
pointer: { show: false },
|
||||
axisLine: { lineStyle: { width: 20, color: [[1, 'rgba(255,255,255,0.05)']] } },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { show: false },
|
||||
anchor: { show: false },
|
||||
title: { show: false },
|
||||
detail: { show: false },
|
||||
data: [{ value: 100 }]
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (lineChartRef.value) {
|
||||
lineChart = echarts.init(lineChartRef.value)
|
||||
lineChart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['用电', '用气'], bottom: 10, textStyle: { color: '#999' } },
|
||||
grid: { top: 20, right: 20, bottom: 40, left: 50 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||
axisLine: { lineStyle: { color: '#2a3f5c' } },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '用电',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [620, 680, 650, 720, 780, 820],
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(92, 217, 232, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(92, 217, 232, 0.05)' }
|
||||
])
|
||||
},
|
||||
lineStyle: { color: '#5cd9e8', width: 2 },
|
||||
itemStyle: { color: '#5cd9e8' }
|
||||
},
|
||||
{
|
||||
name: '用气',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [45, 48, 46, 52, 55, 58],
|
||||
lineStyle: { color: '#33cea0', width: 2 },
|
||||
itemStyle: { color: '#33cea0' }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
if (pieChartRef.value) {
|
||||
pieChart = echarts.init(pieChartRef.value)
|
||||
pieChart.setOption({
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||
legend: { bottom: 10, textStyle: { color: '#999' } },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['45%', '75%'],
|
||||
center: ['50%', '45%'],
|
||||
data: [
|
||||
{ value: 65, name: '电能', itemStyle: { color: '#5cd9e8' } },
|
||||
{ value: 20, name: '天然气', itemStyle: { color: '#ff9800' } },
|
||||
{ value: 10, name: '水', itemStyle: { color: '#409eff' } },
|
||||
{ value: 5, name: '其他', itemStyle: { color: '#909399' } }
|
||||
],
|
||||
label: { show: false }
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (barChartRef.value) {
|
||||
barChart = echarts.init(barChartRef.value)
|
||||
barChart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { top: 20, right: 20, bottom: 30, left: 50 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['早班', '中班', '晚班'],
|
||||
axisLine: { lineStyle: { color: '#2a3f5c' } },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: [9800, 12500, 8500],
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#5cd9e8' },
|
||||
{ offset: 1, color: '#409eff' }
|
||||
])
|
||||
},
|
||||
barWidth: '50%'
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
initCharts()
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
gaugeChart?.resize()
|
||||
lineChart?.resize()
|
||||
pieChart?.resize()
|
||||
barChart?.resize()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
gaugeChart?.dispose()
|
||||
lineChart?.dispose()
|
||||
pieChart?.dispose()
|
||||
barChart?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.energy-screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 180px);
|
||||
background: linear-gradient(135deg, #0d2616 0%, #1a3a2d 100%);
|
||||
color: #d3d6dd;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 30px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 20px rgba(92, 217, 232, 0.8);
|
||||
letter-spacing: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 16px;
|
||||
color: #5cd9e8;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.screen-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 15px 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 15px;
|
||||
padding: 15px 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
.data-card {
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(92, 217, 232, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card-unit {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.card-trend {
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
|
||||
&.up { color: #f56c6c; }
|
||||
&.down { color: #67c23a; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-area {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
padding: 15px 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
.chart-box {
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(92, 217, 232, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.box-title {
|
||||
font-size: 14px;
|
||||
color: #5cd9e8;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 8px;
|
||||
border-left: 3px solid #5cd9e8;
|
||||
}
|
||||
|
||||
.chart {
|
||||
flex: 1;
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-area {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
padding: 15px 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
.bottom-box {
|
||||
background: rgba(19, 25, 47, 0.8);
|
||||
border: 1px solid rgba(92, 217, 232, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.box-title {
|
||||
font-size: 14px;
|
||||
color: #5cd9e8;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 8px;
|
||||
border-left: 3px solid #5cd9e8;
|
||||
}
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: rgba(92, 217, 232, 0.1);
|
||||
border-radius: 6px;
|
||||
|
||||
.rank {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
|
||||
&.rank-1 { background: linear-gradient(135deg, #ffd700, #ffb700); color: #fff; }
|
||||
&.rank-2 { background: linear-gradient(135deg, #c0c0c0, #a0a0a0); color: #fff; }
|
||||
&.rank-3 { background: linear-gradient(135deg, #cd7f32, #b87333); color: #fff; }
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 60px;
|
||||
font-size: 13px;
|
||||
color: #d3d6dd;
|
||||
}
|
||||
|
||||
.bar-wrapper {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
margin: 0 10px;
|
||||
overflow: hidden;
|
||||
|
||||
.bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #5cd9e8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alarm-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
.alarm-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid;
|
||||
|
||||
&.danger {
|
||||
background: rgba(245, 108, 108, 0.15);
|
||||
border-color: #f56c6c;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: rgba(230, 162, 60, 0.15);
|
||||
border-color: #e6a23c;
|
||||
}
|
||||
|
||||
&.success {
|
||||
background: rgba(103, 194, 58, 0.15);
|
||||
border-color: #67c23a;
|
||||
}
|
||||
|
||||
.alarm-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.alarm-content {
|
||||
flex: 1;
|
||||
|
||||
.alarm-title {
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.alarm-time {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.card-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.chart-area,
|
||||
.bottom-area {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.chart-area .chart-box:last-child,
|
||||
.bottom-area .bottom-box:last-child {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.card-grid,
|
||||
.chart-area,
|
||||
.bottom-area {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.chart-area .chart-box:last-child,
|
||||
.bottom-area .bottom-box:last-child {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.screen-header .title {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
24
src/views/dashboard/order/index.vue
Normal file
24
src/views/dashboard/order/index.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="filter-container">
|
||||
<el-button type="primary">刷新数据</el-button>
|
||||
</div>
|
||||
<el-card title="订单大屏">
|
||||
<p>这是订单大屏页面,展示订单数据。</p>
|
||||
<el-table :data="tableData" border>
|
||||
<el-table-column prop="orderNo" label="订单号" />
|
||||
<el-table-column prop="customer" label="客户" />
|
||||
<el-table-column prop="amount" label="金额" />
|
||||
<el-table-column prop="status" label="状态" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const tableData = [
|
||||
{ orderNo: 'ORD001', customer: '客户A', amount: '10000', status: '生产中' },
|
||||
{ orderNo: 'ORD002', customer: '客户B', amount: '20000', status: '已完成' },
|
||||
{ orderNo: 'ORD003', customer: '客户C', amount: '15000', status: '待生产' }
|
||||
]
|
||||
</script>
|
||||
70
src/views/data-source/index.vue
Normal file
70
src/views/data-source/index.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="data-source-container">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" icon="Plus">添加数据源</el-button>
|
||||
<el-button type="success" icon="Refresh">测试连接</el-button>
|
||||
</div>
|
||||
|
||||
<div class="data-source-list">
|
||||
<el-table :data="dataSources" border>
|
||||
<el-table-column prop="name" label="数据源名称" width="180" />
|
||||
<el-table-column prop="type" label="类型" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.type === 'klpl3' ? 'primary' : 'info'">
|
||||
{{ scope.row.type === 'klpl3' ? 'KLPL3接口' : '数据库' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="host" label="地址" width="250" />
|
||||
<el-table-column prop="port" label="端口" width="100" />
|
||||
<el-table-column prop="database" label="数据库/接口" width="150" />
|
||||
<el-table-column prop="status" label="连接状态" width="120">
|
||||
<template #default="scope">
|
||||
<el-badge :value="scope.row.status === 'connected' ? '已连接' : '未连接'" :type="scope.row.status === 'connected' ? 'success' : 'danger'" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button size="small" icon="DataAnalysis">配置</el-button>
|
||||
<el-button size="small" icon="Refresh">测试</el-button>
|
||||
<el-button size="small" icon="Delete" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const dataSources = ref([
|
||||
{ id: 1, name: 'KLPL3主接口', type: 'klpl3', host: 'http://klpl3.kailaisi.com', port: '8080', database: '/api/v1', status: 'connected' },
|
||||
{ id: 2, name: '生产数据库', type: 'database', host: '192.168.1.100', port: '3306', database: 'production_db', status: 'connected' },
|
||||
{ id: 3, name: '报表数据库', type: 'database', host: '192.168.1.101', port: '5432', database: 'report_db', status: 'connected' },
|
||||
{ id: 4, name: 'KLPL3备用接口', type: 'klpl3', host: 'http://klpl3-backup.kailaisi.com', port: '8080', database: '/api/v1', status: 'disconnected' }
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.data-source-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.data-source-list {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
--el-table-header-text-color: #606266;
|
||||
--el-table-row-hover-bg-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
278
src/views/home/index.vue
Normal file
278
src/views/home/index.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<div class="home-container">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon blue">📊</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ stats.totalScreens }}</span>
|
||||
<span class="stat-label">大屏总数</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon green">📈</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ stats.activeScreens }}</span>
|
||||
<span class="stat-label">运行中</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon orange">📁</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ stats.totalReports }}</span>
|
||||
<span class="stat-label">报表数量</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon purple">🔄</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ stats.dataSources }}</span>
|
||||
<span class="stat-label">数据源数</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-row">
|
||||
<div class="chart-card">
|
||||
<h3 class="chart-title">大屏类型分布</h3>
|
||||
<div ref="typeChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3 class="chart-title">数据更新趋势</h3>
|
||||
<div ref="trendChartRef" class="chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recent-activity">
|
||||
<h3 class="activity-title">最近活动</h3>
|
||||
<el-table :data="recentActivities" border>
|
||||
<el-table-column prop="time" label="时间" width="180" />
|
||||
<el-table-column prop="action" label="操作" width="200" />
|
||||
<el-table-column prop="user" label="操作人" width="120" />
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === '成功' ? 'success' : 'warning'">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const stats = ref({
|
||||
totalScreens: 24,
|
||||
activeScreens: 18,
|
||||
totalReports: 156,
|
||||
dataSources: 8
|
||||
})
|
||||
|
||||
const recentActivities = ref([
|
||||
{ time: '2026-05-14 14:30', action: '创建酸轧数据大屏', user: '管理员', status: '成功' },
|
||||
{ time: '2026-05-14 14:25', action: '更新报表配置', user: '张三', status: '成功' },
|
||||
{ time: '2026-05-14 13:45', action: '同步数据源', user: '管理员', status: '成功' },
|
||||
{ time: '2026-05-14 10:20', action: '删除过期大屏', user: '李四', status: '成功' },
|
||||
{ time: '2026-05-14 09:15', action: '配置新数据源', user: '管理员', status: '成功' }
|
||||
])
|
||||
|
||||
const typeChartRef = ref(null)
|
||||
const trendChartRef = ref(null)
|
||||
|
||||
const initCharts = () => {
|
||||
if (typeChartRef.value) {
|
||||
const typeChart = echarts.init(typeChartRef.value)
|
||||
typeChart.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
right: '5%',
|
||||
top: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '大屏类型',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['35%', '50%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{ value: 8, name: '生产监控' },
|
||||
{ value: 6, name: '数据分析' },
|
||||
{ value: 5, name: '实时报表' },
|
||||
{ value: 3, name: '设备管理' },
|
||||
{ value: 2, name: '其他' }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
if (trendChartRef.value) {
|
||||
const trendChart = echarts.init(trendChartRef.value)
|
||||
trendChart.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['09:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '数据更新',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [120, 132, 101, 134, 190, 230, 210],
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(64, 158, 255, 0.05)' }
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initCharts()
|
||||
window.addEventListener('resize', initCharts)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
margin-right: 16px;
|
||||
|
||||
&.blue { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
||||
&.green { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); }
|
||||
&.orange { background: linear-gradient(135deg, #fc4a1a 0%, #f7b733 100%); }
|
||||
&.purple { background: linear-gradient(135deg, #434343 0%, #000000 100%); }
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
.recent-activity {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
--el-table-header-text-color: #606266;
|
||||
--el-table-row-hover-bg-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
314
src/views/reports/acid-rolling/index.vue
Normal file
314
src/views/reports/acid-rolling/index.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<div class="report-container">
|
||||
<div class="query-form">
|
||||
<el-form :model="queryForm" inline>
|
||||
<el-form-item label="开始时间">
|
||||
<el-date-picker
|
||||
v-model="queryForm.startTime"
|
||||
type="datetime"
|
||||
placeholder="选择开始时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间">
|
||||
<el-date-picker
|
||||
v-model="queryForm.endTime"
|
||||
type="datetime"
|
||||
placeholder="选择结束时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="入场钢卷号">
|
||||
<el-input v-model="queryForm.batchNo" placeholder="请输入入场钢卷号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="当前钢卷号">
|
||||
<el-input v-model="queryForm.currentNo" placeholder="请输入当前钢卷号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="逻辑库位">
|
||||
<el-select v-model="queryForm.warehouse" placeholder="请选择逻辑库位">
|
||||
<el-option label="酸轧成品库" value="acid-rolling" />
|
||||
<el-option label="原料库" value="raw-material" />
|
||||
<el-option label="成品库" value="finished" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品名称">
|
||||
<el-input v-model="queryForm.productName" placeholder="请输入产品名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="规格">
|
||||
<el-select v-model="queryForm.spec" placeholder="请选择规格">
|
||||
<el-option label="SPHC" value="SPHC" />
|
||||
<el-option label="SPHD" value="SPHD" />
|
||||
<el-option label="SPHE" value="SPHE" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="材质">
|
||||
<el-select v-model="queryForm.material" placeholder="请选择材质">
|
||||
<el-option label="Q235" value="Q235" />
|
||||
<el-option label="Q345" value="Q345" />
|
||||
<el-option label="SPHC" value="SPHC" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="厂家">
|
||||
<el-select v-model="queryForm.factory" placeholder="请选择厂家">
|
||||
<el-option label="本厂" value="local" />
|
||||
<el-option label="外厂" value="external" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search">查询</el-button>
|
||||
<el-button type="success" icon="Download">导出</el-button>
|
||||
<el-button type="default" icon="Settings">列设置</el-button>
|
||||
<el-button type="primary" icon="Save">保存产出报表</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="summary-section">
|
||||
<h3 class="section-title">统计信息</h3>
|
||||
<div class="summary-cards">
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">总钢卷数量</span>
|
||||
<span class="summary-value">{{ summaryData.totalQuantity }}</span>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">总重量</span>
|
||||
<span class="summary-value">{{ summaryData.totalWeight }}t</span>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">均重</span>
|
||||
<span class="summary-value">{{ summaryData.avgWeight }}t</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-section">
|
||||
<div class="table-header">
|
||||
<h3 class="section-title">明细信息</h3>
|
||||
<div class="table-tools">
|
||||
<el-input v-model="searchKeyword" placeholder="输入关键词筛选" style="width: 200px;" />
|
||||
<el-select v-model="filterField" placeholder="选择筛选字段">
|
||||
<el-option label="入场钢卷号" value="batchNo" />
|
||||
<el-option label="当前钢卷号" value="currentNo" />
|
||||
<el-option label="产品名称" value="productName" />
|
||||
</el-select>
|
||||
<el-select v-model="sortField" placeholder="选择排序字段">
|
||||
<el-option label="生产时间" value="productionTime" />
|
||||
<el-option label="重量" value="weight" />
|
||||
</el-select>
|
||||
<el-select v-model="sortOrder" placeholder="排序方式">
|
||||
<el-option label="升序" value="asc" />
|
||||
<el-option label="降序" value="desc" />
|
||||
</el-select>
|
||||
<span class="page-info">共 {{ filteredData.length }} 条</span>
|
||||
<el-select v-model="pageSize" placeholder="每页条数">
|
||||
<el-option label="10" :value="10" />
|
||||
<el-option label="20" :value="20" />
|
||||
<el-option label="50" :value="50" />
|
||||
<el-option label="1000" :value="1000" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="filteredData" border :height="400">
|
||||
<el-table-column prop="batchNo" label="入场钢卷号" width="160" />
|
||||
<el-table-column prop="currentNo" label="当前钢卷号" width="160" />
|
||||
<el-table-column prop="productionTime" label="生产时间" width="180" />
|
||||
<el-table-column prop="warehouse" label="逻辑库区" width="120" />
|
||||
<el-table-column prop="qualityStatus" label="质量状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.qualityStatus === '合格' ? 'success' : 'danger'">
|
||||
{{ scope.row.qualityStatus }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="productType" label="产品类型" width="120" />
|
||||
<el-table-column prop="width" label="宽度" width="100" />
|
||||
<el-table-column prop="thickness" label="厚度" width="100" />
|
||||
<el-table-column prop="weight" label="重量" width="100" />
|
||||
<el-table-column prop="length" label="长度" width="100" />
|
||||
<el-table-column prop="stockStatus" label="在库状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.stockStatus === '在库' ? 'success' : 'warning'">
|
||||
{{ scope.row.stockStatus }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:total="filteredData.length"
|
||||
layout="prev, pager, next, jumper"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const queryForm = ref({
|
||||
startTime: '2026-05-13 07:00:00',
|
||||
endTime: '2026-05-14 07:00:00',
|
||||
batchNo: '',
|
||||
currentNo: '',
|
||||
warehouse: '',
|
||||
productName: '',
|
||||
spec: '',
|
||||
material: '',
|
||||
factory: ''
|
||||
})
|
||||
|
||||
const summaryData = ref({
|
||||
totalQuantity: 0,
|
||||
totalWeight: 0.00,
|
||||
avgWeight: 0
|
||||
})
|
||||
|
||||
const searchKeyword = ref('')
|
||||
const filterField = ref('')
|
||||
const sortField = ref('productionTime')
|
||||
const sortOrder = ref('desc')
|
||||
const pageSize = ref(10)
|
||||
const currentPage = ref(1)
|
||||
|
||||
const productionData = ref([
|
||||
{ batchNo: 'B20260514001', currentNo: 'C20260514001', productionTime: '2026-05-14 08:30:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHC', width: '1250mm', thickness: '2.0mm', weight: '6.85t', length: '12500mm', stockStatus: '在库' },
|
||||
{ batchNo: 'B20260514002', currentNo: 'C20260514002', productionTime: '2026-05-14 09:15:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHD', width: '1500mm', thickness: '1.8mm', weight: '7.23t', length: '14500mm', stockStatus: '在库' },
|
||||
{ batchNo: 'B20260514003', currentNo: 'C20260514003', productionTime: '2026-05-14 10:00:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHE', width: '1000mm', thickness: '2.5mm', weight: '6.98t', length: '11000mm', stockStatus: '已出库' },
|
||||
{ batchNo: 'B20260514004', currentNo: 'C20260514004', productionTime: '2026-05-14 10:45:00', warehouse: '酸轧成品库', qualityStatus: '不合格', productType: 'SPHC', width: '1250mm', thickness: '2.0mm', weight: '7.12t', length: '12800mm', stockStatus: '待处理' },
|
||||
{ batchNo: 'B20260514005', currentNo: 'C20260514005', productionTime: '2026-05-14 11:30:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHD', width: '1500mm', thickness: '1.6mm', weight: '6.75t', length: '15200mm', stockStatus: '在库' },
|
||||
{ batchNo: 'B20260514006', currentNo: 'C20260514006', productionTime: '2026-05-14 14:00:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHC', width: '1250mm', thickness: '2.2mm', weight: '7.35t', length: '11800mm', stockStatus: '在库' }
|
||||
])
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = [...productionData.value]
|
||||
|
||||
if (searchKeyword.value && filterField.value) {
|
||||
data = data.filter(item =>
|
||||
item[filterField.value]?.includes(searchKeyword.value)
|
||||
)
|
||||
}
|
||||
|
||||
data.sort((a, b) => {
|
||||
const aVal = a[sortField.value]
|
||||
const bVal = b[sortField.value]
|
||||
if (sortOrder.value === 'asc') {
|
||||
return aVal > bVal ? 1 : -1
|
||||
}
|
||||
return aVal < bVal ? 1 : -1
|
||||
})
|
||||
|
||||
summaryData.value = {
|
||||
totalQuantity: data.length,
|
||||
totalWeight: data.reduce((sum, item) => sum + parseFloat(item.weight), 0).toFixed(2),
|
||||
avgWeight: data.length > 0 ? (data.reduce((sum, item) => sum + parseFloat(item.weight), 0) / data.length).toFixed(2) : 0
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.report-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.query-form {
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-input), :deep(.el-select) {
|
||||
width: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 12px;
|
||||
border-left: 3px solid #409eff;
|
||||
}
|
||||
|
||||
.summary-cards {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
|
||||
.summary-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.table-section {
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.table-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
:deep(.el-input), :deep(.el-select) {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
--el-table-header-text-color: #606266;
|
||||
--el-table-row-hover-bg-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.el-pagination) {
|
||||
padding: 16px;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
}
|
||||
</style>
|
||||
308
src/views/reports/acid-stop/index.vue
Normal file
308
src/views/reports/acid-stop/index.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<template>
|
||||
<div class="report-container">
|
||||
<div class="tabs-header">
|
||||
<el-tabs v-model="activeTab" type="card">
|
||||
<el-tab-pane label="日" name="day" />
|
||||
<el-tab-pane label="月" name="month" />
|
||||
<el-tab-pane label="年" name="year" />
|
||||
</el-tabs>
|
||||
<el-date-picker v-model="selectedDate" type="date" placeholder="选择日期" />
|
||||
</div>
|
||||
|
||||
<div class="summary-section">
|
||||
<h3 class="section-title">停机汇总</h3>
|
||||
<div class="summary-cards">
|
||||
<div class="summary-card">
|
||||
<div class="card-icon">⏱️</div>
|
||||
<div class="card-content">
|
||||
<span class="card-label">停机时间</span>
|
||||
<span class="card-value highlight">{{ summaryData.stopTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="card-icon">🔄</div>
|
||||
<div class="card-content">
|
||||
<span class="card-label">停机次数</span>
|
||||
<span class="card-value">{{ summaryData.stopCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="card-icon">📊</div>
|
||||
<div class="card-content">
|
||||
<span class="card-label">作业率</span>
|
||||
<span class="card-value highlight">{{ summaryData.rate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-section">
|
||||
<div class="chart-panel">
|
||||
<h3 class="section-title">班组停机分布</h3>
|
||||
<div ref="teamChartRef" class="chart-content"></div>
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot active"></span> 停机</span>
|
||||
<span class="legend-item"><span class="legend-dot inactive"></span> 正常</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-panel">
|
||||
<h3 class="section-title">停机类型分布</h3>
|
||||
<div ref="typeChartRef" class="chart-content"></div>
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot type1"></span> 来料缺陷</span>
|
||||
<span class="legend-item"><span class="legend-dot type2"></span> 机械故障</span>
|
||||
<span class="legend-item"><span class="legend-dot type3"></span> 换辊</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-section">
|
||||
<h3 class="section-title">停机详情</h3>
|
||||
<el-table :data="stopData" border :height="300">
|
||||
<el-table-column prop="timeRange" label="时间范围" width="200" />
|
||||
<el-table-column prop="duration" label="持续时间" width="120" />
|
||||
<el-table-column prop="team" label="机组" width="100" />
|
||||
<el-table-column prop="remark" label="备注" width="300" />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const activeTab = ref('day')
|
||||
const selectedDate = ref('2026-05-14')
|
||||
|
||||
const summaryData = ref({
|
||||
stopTime: '60 min',
|
||||
stopCount: '4次',
|
||||
rate: '95.83%'
|
||||
})
|
||||
|
||||
const stopData = ref([
|
||||
{ timeRange: '2026-05-14 13:22 - 2026-05-14 13:34', duration: '12min', team: 'MLL', remark: '带尾夹送断带' },
|
||||
{ timeRange: '2026-05-14 12:47 - 2026-05-14 12:54', duration: '7min', team: 'MLL', remark: '处理表面缺陷' },
|
||||
{ timeRange: '2026-05-14 10:27 - 2026-05-14 12:54', duration: '33min', team: 'MLL', remark: '开卷机故障' },
|
||||
{ timeRange: '2026-05-14 10:27 - 2026-05-14 11:00', duration: '33min', team: 'MLL', remark: '换1-4机架辊,修4机架弯辊阀' },
|
||||
{ timeRange: '2026-05-14 04:03 - 2026-05-14 04:11', duration: '8min', team: 'MLL', remark: '换辊' }
|
||||
])
|
||||
|
||||
const teamChartRef = ref(null)
|
||||
const typeChartRef = ref(null)
|
||||
|
||||
const initCharts = () => {
|
||||
if (teamChartRef.value) {
|
||||
const chart = echarts.init(teamChartRef.value)
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [
|
||||
{
|
||||
name: '班组停机',
|
||||
type: 'pie',
|
||||
radius: ['50%', '75%'],
|
||||
center: ['50%', '50%'],
|
||||
data: [
|
||||
{ value: 1, name: '停机', itemStyle: { color: '#409eff' } },
|
||||
{ value: 0, name: '正常', itemStyle: { color: '#e4e7ed' } }
|
||||
],
|
||||
label: { show: false },
|
||||
emphasis: {
|
||||
label: { show: true, fontSize: 24, fontWeight: 'bold' }
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
if (typeChartRef.value) {
|
||||
const chart = echarts.init(typeChartRef.value)
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [
|
||||
{
|
||||
name: '停机类型',
|
||||
type: 'pie',
|
||||
radius: ['50%', '75%'],
|
||||
center: ['50%', '50%'],
|
||||
data: [
|
||||
{ value: 45, name: '来料缺陷', itemStyle: { color: '#409eff' } },
|
||||
{ value: 35, name: '机械故障', itemStyle: { color: '#67c23a' } },
|
||||
{ value: 20, name: '换辊', itemStyle: { color: '#e6a23c' } }
|
||||
],
|
||||
label: { show: false },
|
||||
emphasis: {
|
||||
label: { show: true, fontSize: 18, fontWeight: 'bold' }
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initCharts()
|
||||
window.addEventListener('resize', initCharts)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.report-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.tabs-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.el-tabs) {
|
||||
flex: 1;
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
padding: 0 20px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
background: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 12px;
|
||||
border-left: 3px solid #409eff;
|
||||
}
|
||||
|
||||
.summary-cards {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
|
||||
.card-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
|
||||
&.highlight {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
|
||||
&.active { background: #409eff; }
|
||||
&.inactive { background: #e4e7ed; }
|
||||
&.type1 { background: #409eff; }
|
||||
&.type2 { background: #67c23a; }
|
||||
&.type3 { background: #e6a23c; }
|
||||
}
|
||||
|
||||
.table-section {
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
|
||||
:deep(.el-table) {
|
||||
--el-table-header-text-color: #606266;
|
||||
--el-table-row-hover-bg-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
107
src/views/reports/index.vue
Normal file
107
src/views/reports/index.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="reports-container">
|
||||
<div class="search-bar">
|
||||
<el-input placeholder="搜索报表名称" v-model="searchKeyword" class="search-input">
|
||||
<template #append><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-select v-model="category" placeholder="选择分类">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="生产报表" value="production" />
|
||||
<el-option label="质量报表" value="quality" />
|
||||
<el-option label="库存报表" value="inventory" />
|
||||
<el-option label="销售报表" value="sales" />
|
||||
</el-select>
|
||||
<el-button type="primary" icon="Plus">新建报表</el-button>
|
||||
</div>
|
||||
|
||||
<div class="reports-list">
|
||||
<el-table :data="filteredReports" border>
|
||||
<el-table-column prop="name" label="报表名称" width="200" />
|
||||
<el-table-column prop="category" label="分类" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ getCategoryLabel(scope.row.category) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dataSource" label="数据源" width="150" />
|
||||
<el-table-column prop="updateTime" label="更新时间" width="180" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 'active' ? 'success' : 'warning'">
|
||||
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button size="small" icon="View">预览</el-button>
|
||||
<el-button size="small" icon="Edit">编辑</el-button>
|
||||
<el-button size="small" icon="Download">导出</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
|
||||
const searchKeyword = ref('')
|
||||
const category = ref('')
|
||||
|
||||
const reports = ref([
|
||||
{ id: 1, name: '酸轧产出报表', category: 'production', dataSource: 'klpl3', updateTime: '2026-05-14 14:30', status: 'active' },
|
||||
{ id: 2, name: '库存总览报表', category: 'inventory', dataSource: 'klpl3', updateTime: '2026-05-14 14:25', status: 'active' },
|
||||
{ id: 3, name: '产品质量报表', category: 'quality', dataSource: 'klpl3', updateTime: '2026-05-14 14:20', status: 'active' },
|
||||
{ id: 4, name: '销售统计报表', category: 'sales', dataSource: 'klpl3', updateTime: '2026-05-14 13:45', status: 'active' },
|
||||
{ id: 5, name: '生产日报表', category: 'production', dataSource: 'klpl3', updateTime: '2026-05-14 09:00', status: 'active' },
|
||||
{ id: 6, name: '设备状态报表', category: 'production', dataSource: 'klpl3', updateTime: '2026-05-13 18:00', status: 'disabled' }
|
||||
])
|
||||
|
||||
const filteredReports = computed(() => {
|
||||
return reports.value.filter(item => {
|
||||
const matchKeyword = !searchKeyword.value || item.name.includes(searchKeyword.value)
|
||||
const matchCategory = !category.value || item.category === category.value
|
||||
return matchKeyword && matchCategory
|
||||
})
|
||||
})
|
||||
|
||||
const getCategoryLabel = (cat) => {
|
||||
const map = {
|
||||
production: '生产报表',
|
||||
quality: '质量报表',
|
||||
inventory: '库存报表',
|
||||
sales: '销售报表'
|
||||
}
|
||||
return map[cat] || cat
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.reports-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.search-input {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.reports-list {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
--el-table-header-text-color: #606266;
|
||||
--el-table-row-hover-bg-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
990
src/views/screens/acid-rolling/index.vue
Normal file
990
src/views/screens/acid-rolling/index.vue
Normal file
@@ -0,0 +1,990 @@
|
||||
<template>
|
||||
<div id="big-screen" class="screen-container">
|
||||
<!-- 顶部标题区 -->
|
||||
<header class="screen-header">
|
||||
<div class="header-left">
|
||||
<div class="decoration-line"></div>
|
||||
<h1 class="screen-title">酸轧产线数据大屏</h1>
|
||||
<div class="decoration-line"></div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="current-time">{{ currentTime }}</span>
|
||||
<span class="data-update-time">数据更新时间:{{ lastUpdateTime }}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<main class="screen-body">
|
||||
<!-- 数据卡片 -->
|
||||
<section class="card-row">
|
||||
<div class="data-card" v-for="(card, index) in kpiCards" :key="card.title">
|
||||
<div class="card-header">{{ card.title }}</div>
|
||||
<div class="card-value">
|
||||
<span class="value-num">{{ card.value }}</span>
|
||||
<span class="value-unit">{{ card.unit }}</span>
|
||||
</div>
|
||||
<div class="card-trend" :class="card.trendClass">
|
||||
{{ card.trend }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 图表区 -->
|
||||
<section class="chart-row">
|
||||
<div class="chart-panel">
|
||||
<div class="panel-header">
|
||||
<div class="status-dot online"></div>
|
||||
<span class="panel-title">OEE趋势分析</span>
|
||||
</div>
|
||||
<div ref="trendChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-panel">
|
||||
<div class="panel-header">
|
||||
<div class="status-dot online"></div>
|
||||
<span class="panel-title">7大损失占比</span>
|
||||
</div>
|
||||
<div ref="lossChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-panel">
|
||||
<div class="panel-header">
|
||||
<div class="status-dot online"></div>
|
||||
<span class="panel-title">产量达成率</span>
|
||||
</div>
|
||||
<div ref="outputChartRef" class="chart"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 表格区 -->
|
||||
<section class="table-row">
|
||||
<div class="table-panel">
|
||||
<div class="panel-header">
|
||||
<div class="status-dot online"></div>
|
||||
<span class="panel-title">日明细(用于趋势分析)</span>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<el-table :data="oeeData.summaryList" border>
|
||||
<el-table-column prop="date" label="日期" />
|
||||
<el-table-column prop="oee" label="OEE (%)" />
|
||||
<el-table-column prop="availability" label="时间稼动率 (%)" />
|
||||
<el-table-column prop="performanceTon" label="性能稼动率 (%)" />
|
||||
<el-table-column prop="quality" label="良品率 (%)" />
|
||||
<el-table-column prop="efficiency" label="达成率 (%)" />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-panel">
|
||||
<div class="panel-header">
|
||||
<div class="status-dot online"></div>
|
||||
<span class="panel-title">停机/损失事件明细</span>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<el-table :data="oeeData.eventList" border>
|
||||
<el-table-column prop="eventTime" label="事件时间" />
|
||||
<el-table-column prop="eventType" label="事件类型" />
|
||||
<el-table-column prop="lossType" label="损失类型" />
|
||||
<el-table-column prop="duration" label="持续时间" />
|
||||
<el-table-column prop="reason" label="原因" />
|
||||
<el-table-column prop="handleStatus" label="处理状态" />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 右侧信息区 -->
|
||||
<aside class="side-panel">
|
||||
<div class="info-card">
|
||||
<div class="info-title">OEE 计算公式</div>
|
||||
<div class="formula-box">
|
||||
<div class="formula-main">OEE = A × P × Q</div>
|
||||
<div class="formula-detail">
|
||||
<p><span class="formula-label">A</span>(时间稼动率)= (负荷时间 − 停机时间) / 负荷时间</p>
|
||||
<p><span class="formula-label">P</span>(性能稼动率)= (理论节拍 × 产量) / 实际运转时间</p>
|
||||
<p><span class="formula-label">Q</span>(良品率)= 良品数 / 总产量</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-card">
|
||||
<div class="info-title">7大损失分类</div>
|
||||
<div class="loss-box">
|
||||
<div class="loss-item" v-for="(loss, index) in oeeData.loss7List" :key="index">
|
||||
<span class="loss-num">{{ index + 1 }}</span>
|
||||
<span class="loss-name">{{ loss.lossName }}</span>
|
||||
<span class="loss-ratio">{{ loss.lossRatio }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-card">
|
||||
<div class="info-title">生产状态监控</div>
|
||||
<div class="status-box">
|
||||
<div class="status-item">
|
||||
<div class="status-dot running"></div>
|
||||
<span>运行中</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="status-dot standby"></div>
|
||||
<span>待机</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="status-dot maintenance"></div>
|
||||
<span>维护</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="status-dot fault"></div>
|
||||
<span>故障</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auto-refresh-control">
|
||||
<el-switch v-model="autoRefresh" @change="toggleAutoRefresh" />
|
||||
<span>自动刷新(30秒)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-card">
|
||||
<div class="info-title">查询控制</div>
|
||||
<div class="control-box">
|
||||
<el-select v-model="lineType" style="width: 100%; margin-bottom: 12px">
|
||||
<el-option label="酸轧线" value="acid" />
|
||||
<el-option label="镀锌一线" value="galvanize1" />
|
||||
</el-select>
|
||||
<el-date-picker v-model="queryRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width: 100%" />
|
||||
<div class="btn-group">
|
||||
<el-button type="primary" @click="handleSearch" style="width: 48%">查询</el-button>
|
||||
<el-button @click="handleRefresh" :loading="isRefreshing" style="width: 48%">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { getOeeData } from '@/api/screen'
|
||||
|
||||
const lineType = ref('acid')
|
||||
const queryRange = ref(['2026-05-01', '2026-05-15'])
|
||||
const lastUpdateTime = ref('')
|
||||
const isRefreshing = ref(false)
|
||||
const autoRefresh = ref(false)
|
||||
const trendChartRef = ref(null)
|
||||
const lossChartRef = ref(null)
|
||||
const outputChartRef = ref(null)
|
||||
let trendChart = null
|
||||
let lossChart = null
|
||||
let outputChart = null
|
||||
let refreshTimer = null
|
||||
let timeTimer = null
|
||||
|
||||
const currentTime = ref('')
|
||||
|
||||
const updateCurrentTime = () => {
|
||||
currentTime.value = new Date().toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
}
|
||||
|
||||
const oeeData = ref({
|
||||
kpi: {
|
||||
oee: 0,
|
||||
availability: 0,
|
||||
performanceTon: 0,
|
||||
quality: 0,
|
||||
totalOutputTon: 0,
|
||||
targetOutputTon: 0
|
||||
},
|
||||
summaryList: [],
|
||||
loss7List: [],
|
||||
eventList: []
|
||||
})
|
||||
|
||||
const kpiCards = computed(() => [
|
||||
{ title: 'OEE', value: oeeData.value.kpi.oee, unit: '%', trend: '+1.2%', trendClass: 'up' },
|
||||
{ title: '时间稼动率', value: oeeData.value.kpi.availability, unit: '%', trend: '+0.5%', trendClass: 'up' },
|
||||
{ title: '性能稼动率', value: oeeData.value.kpi.performanceTon, unit: '%', trend: '-0.3%', trendClass: 'down' },
|
||||
{ title: '良品率', value: oeeData.value.kpi.quality, unit: '%', trend: '持平', trendClass: 'same' },
|
||||
{ title: '总产量', value: oeeData.value.kpi.totalOutputTon, unit: '吨', trend: '+2.1%', trendClass: 'up' },
|
||||
{ title: '目标达成', value: ((oeeData.value.kpi.totalOutputTon / oeeData.value.kpi.targetOutputTon) * 100).toFixed(1), unit: '%', trend: '+1.5%', trendClass: 'up' }
|
||||
])
|
||||
|
||||
const initOeeData = () => ({
|
||||
kpi: {
|
||||
oee: 87.5,
|
||||
availability: 92.3,
|
||||
performanceTon: 89.8,
|
||||
quality: 97.5,
|
||||
totalOutputTon: 12580,
|
||||
targetOutputTon: 15000
|
||||
},
|
||||
summaryList: [
|
||||
{ date: '05-10', oee: 85.2, availability: 90.5, performanceTon: 88.2, quality: 96.8, totalOutputTon: 2150, targetOutputTon: 2500, efficiency: 86.0 },
|
||||
{ date: '05-11', oee: 86.8, availability: 91.2, performanceTon: 89.5, quality: 97.2, totalOutputTon: 2280, targetOutputTon: 2500, efficiency: 91.2 },
|
||||
{ date: '05-12', oee: 88.5, availability: 93.8, performanceTon: 91.2, quality: 97.8, totalOutputTon: 2420, targetOutputTon: 2500, efficiency: 96.8 },
|
||||
{ date: '05-13', oee: 87.2, availability: 92.1, performanceTon: 89.6, quality: 97.4, totalOutputTon: 2350, targetOutputTon: 2500, efficiency: 94.0 },
|
||||
{ date: '05-14', oee: 89.1, availability: 94.5, performanceTon: 92.1, quality: 98.0, totalOutputTon: 2480, targetOutputTon: 2500, efficiency: 99.2 },
|
||||
{ date: '05-15', oee: 87.5, availability: 92.3, performanceTon: 89.8, quality: 97.5, totalOutputTon: 2300, targetOutputTon: 2500, efficiency: 92.0 }
|
||||
],
|
||||
loss7List: [
|
||||
{ lossName: '故障停机损失', lossTime: 125, lossRatio: 35.2, description: '设备故障导致停机' },
|
||||
{ lossName: '换模换线损失', lossTime: 85, lossRatio: 23.8, description: '产品切换换模时间' },
|
||||
{ lossName: '空转与短暂停机', lossTime: 55, lossRatio: 15.4, description: '短暂停机调整' },
|
||||
{ lossName: '速度损失', lossTime: 45, lossRatio: 12.6, description: '未达到理论速度' },
|
||||
{ lossName: '质量缺陷与返工', lossTime: 25, lossRatio: 7.0, description: '次品返工时间' },
|
||||
{ lossName: '启动损失', lossTime: 15, lossRatio: 4.2, description: '开机预热时间' },
|
||||
{ lossName: '管理损失', lossTime: 6, lossRatio: 1.8, description: '管理原因等待' }
|
||||
],
|
||||
eventList: [
|
||||
{ eventTime: '13:22', eventType: '停机', lossType: '故障停机损失', duration: '12分钟', reason: '带尾夹送断带', handleStatus: '已处理' },
|
||||
{ eventTime: '10:15', eventType: '停机', lossType: '质量缺陷', duration: '7分钟', reason: '处理表面缺陷', handleStatus: '已处理' },
|
||||
{ eventTime: '08:00', eventType: '停机', lossType: '换模换线', duration: '33分钟', reason: '换1-4机架辊', handleStatus: '已处理' },
|
||||
{ eventTime: '昨日22:30', eventType: '停机', lossType: '故障停机', duration: '18分钟', reason: '液压系统故障', handleStatus: '已处理' },
|
||||
{ eventTime: '昨日16:45', eventType: '减速', lossType: '速度损失', duration: '45分钟', reason: '来料质量问题', handleStatus: '已处理' }
|
||||
]
|
||||
})
|
||||
|
||||
const loadOeeData = async () => {
|
||||
isRefreshing.value = true
|
||||
try {
|
||||
const response = await getOeeData({ lineType: lineType.value })
|
||||
if (response) {
|
||||
oeeData.value = response
|
||||
} else {
|
||||
oeeData.value = initOeeData()
|
||||
}
|
||||
lastUpdateTime.value = new Date().toLocaleTimeString('zh-CN')
|
||||
updateCharts()
|
||||
} catch (error) {
|
||||
console.error('加载OEE数据失败:', error)
|
||||
oeeData.value = initOeeData()
|
||||
} finally {
|
||||
isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateCharts = () => {
|
||||
updateTrendChart()
|
||||
updateLossChart()
|
||||
updateOutputChart()
|
||||
}
|
||||
|
||||
const updateTrendChart = () => {
|
||||
if (trendChart) {
|
||||
trendChart.setOption({
|
||||
grid: { top: 20, right: 20, bottom: 30, left: 50 },
|
||||
tooltip: { trigger: 'axis', backgroundColor: 'rgba(19, 25, 47, 0.9)', borderColor: '#1A5CD7', textStyle: { color: '#d3d6dd' } },
|
||||
legend: { data: ['OEE', '时间稼动率', '性能稼动率', '良品率'], bottom: 0, textStyle: { color: '#d3d6dd' } },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: oeeData.value.summaryList.map(item => item.date),
|
||||
axisLine: { lineStyle: { color: 'rgba(26, 92, 215, 0.3)' } },
|
||||
axisLabel: { color: '#999', fontSize: 11 }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 80,
|
||||
max: 100,
|
||||
axisLine: { show: false },
|
||||
splitLine: { lineStyle: { color: 'rgba(26, 92, 215, 0.1)' } },
|
||||
axisLabel: { color: '#999', fontSize: 11 }
|
||||
},
|
||||
series: [
|
||||
{ name: 'OEE', type: 'line', smooth: true, data: oeeData.value.summaryList.map(item => item.oee), lineStyle: { color: '#1A5CD7', width: 3 }, itemStyle: { color: '#1A5CD7' }, symbol: 'circle', symbolSize: 8 },
|
||||
{ name: '时间稼动率', type: 'line', smooth: true, data: oeeData.value.summaryList.map(item => item.availability), lineStyle: { color: '#5cd9e8', width: 2 }, itemStyle: { color: '#5cd9e8' }, symbol: 'circle', symbolSize: 6 },
|
||||
{ name: '性能稼动率', type: 'line', smooth: true, data: oeeData.value.summaryList.map(item => item.performanceTon), lineStyle: { color: '#33cea0', width: 2 }, itemStyle: { color: '#33cea0' }, symbol: 'circle', symbolSize: 6 },
|
||||
{ name: '良品率', type: 'line', smooth: true, data: oeeData.value.summaryList.map(item => item.quality), lineStyle: { color: '#ff9800', width: 2 }, itemStyle: { color: '#ff9800' }, symbol: 'circle', symbolSize: 6 }
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const updateLossChart = () => {
|
||||
if (lossChart) {
|
||||
lossChart.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c}% ({d}%)',
|
||||
backgroundColor: 'rgba(19, 25, 47, 0.9)',
|
||||
borderColor: '#1A5CD7',
|
||||
textStyle: { color: '#d3d6dd' }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['45%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 6,
|
||||
borderColor: 'rgba(19, 25, 47, 0.7)',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
color: '#d3d6dd',
|
||||
fontSize: 11,
|
||||
formatter: '{b}\n{c}%'
|
||||
},
|
||||
labelLine: {
|
||||
lineStyle: { color: 'rgba(26, 92, 215, 0.5)' }
|
||||
},
|
||||
data: oeeData.value.loss7List.map((item, index) => ({
|
||||
value: item.lossRatio,
|
||||
name: item.lossName,
|
||||
itemStyle: { color: ['#1A5CD7', '#5cd9e8', '#33cea0', '#ff9800', '#ff5722', '#9c27b0', '#673ab7'][index] }
|
||||
}))
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const updateOutputChart = () => {
|
||||
if (outputChart) {
|
||||
const data = oeeData.value.summaryList
|
||||
outputChart.setOption({
|
||||
grid: { top: 20, right: 20, bottom: 30, left: 50 },
|
||||
tooltip: { trigger: 'axis', backgroundColor: 'rgba(19, 25, 47, 0.9)', borderColor: '#1A5CD7', textStyle: { color: '#d3d6dd' } },
|
||||
legend: { data: ['实际产量', '目标产量'], bottom: 0, textStyle: { color: '#d3d6dd' } },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.map(item => item.date),
|
||||
axisLine: { lineStyle: { color: 'rgba(26, 92, 215, 0.3)' } },
|
||||
axisLabel: { color: '#999', fontSize: 11 }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
splitLine: { lineStyle: { color: 'rgba(26, 92, 215, 0.1)' } },
|
||||
axisLabel: { color: '#999', fontSize: 11 }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '实际产量',
|
||||
type: 'bar',
|
||||
data: data.map(item => item.totalOutputTon),
|
||||
itemStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: '#1A5CD7' }, { offset: 1, color: '#5cd9e8' }] }, borderRadius: [4, 4, 0, 0] }
|
||||
},
|
||||
{
|
||||
name: '目标产量',
|
||||
type: 'bar',
|
||||
data: data.map(item => item.targetOutputTon),
|
||||
itemStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: 'rgba(255, 152, 0, 0.3)' }, { offset: 1, color: 'rgba(255, 152, 0, 0.1)' }] }, borderRadius: [4, 4, 0, 0] }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
loadOeeData()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadOeeData()
|
||||
}
|
||||
|
||||
const toggleAutoRefresh = (val) => {
|
||||
if (val) {
|
||||
refreshTimer = setInterval(() => {
|
||||
loadOeeData()
|
||||
}, 30000)
|
||||
} else {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const initCharts = () => {
|
||||
if (trendChartRef.value) {
|
||||
trendChart = echarts.init(trendChartRef.value)
|
||||
updateTrendChart()
|
||||
}
|
||||
if (lossChartRef.value) {
|
||||
lossChart = echarts.init(lossChartRef.value)
|
||||
updateLossChart()
|
||||
}
|
||||
if (outputChartRef.value) {
|
||||
outputChart = echarts.init(outputChartRef.value)
|
||||
updateOutputChart()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
oeeData.value = initOeeData()
|
||||
initCharts()
|
||||
loadOeeData()
|
||||
updateCurrentTime()
|
||||
timeTimer = setInterval(updateCurrentTime, 1000)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
trendChart?.resize()
|
||||
lossChart?.resize()
|
||||
outputChart?.resize()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
trendChart?.dispose()
|
||||
lossChart?.dispose()
|
||||
outputChart?.dispose()
|
||||
if (refreshTimer) clearInterval(refreshTimer)
|
||||
if (timeTimer) clearInterval(timeTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$primary: #1A5CD7;
|
||||
$info: #5cd9e8;
|
||||
$success: #33cea0;
|
||||
$warning: #ff9800;
|
||||
$error: #ff5722;
|
||||
$text: #d3d6dd;
|
||||
$bg-card: rgba(19, 25, 47, 0.7);
|
||||
$border: rgba(26, 92, 215, 0.3);
|
||||
|
||||
#big-screen {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #0a0e1a 0%, #171823 50%, #0a0e1a 100%);
|
||||
color: $text;
|
||||
font-family: 'Microsoft YaHei', sans-serif;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 20%, rgba($primary, 0.08) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 80%, rgba($info, 0.05) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 40px;
|
||||
border-bottom: 1px solid $border;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.screen-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 30px rgba($primary, 0.6);
|
||||
margin: 0;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
.decoration-line {
|
||||
width: 80px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, $primary, transparent);
|
||||
}
|
||||
|
||||
.current-time {
|
||||
font-size: 20px;
|
||||
color: $info;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.data-update-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
padding: 4px 12px;
|
||||
background: rgba($primary, 0.1);
|
||||
border-radius: 4px;
|
||||
border: 1px solid $border;
|
||||
}
|
||||
}
|
||||
|
||||
.screen-body {
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.data-card {
|
||||
background: $bg-card;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid $border;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
animation: pulse 4s ease-in-out infinite;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, $primary, $info, $primary);
|
||||
background-size: 200% 100%;
|
||||
animation: gradientMove 3s ease infinite;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 30px rgba($primary, 0.3);
|
||||
transform: translateY(-4px);
|
||||
border-color: $primary;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-bottom: 12px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.value-num {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: $info;
|
||||
text-shadow: 0 0 20px rgba($info, 0.5);
|
||||
}
|
||||
|
||||
.value-unit {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.card-trend {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
display: inline-block;
|
||||
|
||||
&.up {
|
||||
color: $success;
|
||||
background: rgba($success, 0.15);
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: $error;
|
||||
background: rgba($error, 0.15);
|
||||
}
|
||||
|
||||
&.same {
|
||||
color: #999;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.chart-panel {
|
||||
background: $bg-card;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid $border;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 25px rgba($primary, 0.25);
|
||||
border-color: rgba($primary, 0.5);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.panel-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: $text;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.table-panel {
|
||||
background: $bg-card;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid $border;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 25px rgba($primary, 0.25);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.panel-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: $text;
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba($primary, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
|
||||
&::before {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table th) {
|
||||
background: rgba($primary, 0.1);
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
:deep(.el-table td) {
|
||||
color: $text;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
:deep(.el-table tr:hover td) {
|
||||
background: rgba($primary, 0.1);
|
||||
}
|
||||
|
||||
:deep(.el-table--enable-row-hover .el-table__body tr:hover>td) {
|
||||
background: rgba($primary, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
top: 120px;
|
||||
width: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.info-card {
|
||||
background: $bg-card;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid $border;
|
||||
|
||||
.info-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: $text;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid $border;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
background: $primary;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.formula-box {
|
||||
.formula-main {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: $info;
|
||||
text-align: center;
|
||||
margin-bottom: 14px;
|
||||
text-shadow: 0 0 15px rgba($info, 0.5);
|
||||
}
|
||||
|
||||
.formula-detail {
|
||||
p {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.8;
|
||||
margin: 8px 0;
|
||||
padding-left: 12px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 8px;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: $primary;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.formula-label {
|
||||
color: $info;
|
||||
font-weight: bold;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loss-box {
|
||||
.loss-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.loss-num {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: rgba($primary, 0.2);
|
||||
color: $info;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.loss-name {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.loss-ratio {
|
||||
font-size: 12px;
|
||||
color: $info;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status-box {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.auto-refresh-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid $border;
|
||||
|
||||
:deep(.el-switch__core) {
|
||||
background: rgba($primary, 0.3);
|
||||
|
||||
&.is-checked {
|
||||
background: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.control-box {
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
|
||||
:deep(.el-button) {
|
||||
background: rgba($primary, 0.2);
|
||||
border-color: $primary;
|
||||
color: $text;
|
||||
|
||||
&:hover {
|
||||
background: $primary;
|
||||
border-color: $primary;
|
||||
}
|
||||
|
||||
&.el-button--primary {
|
||||
background: $primary;
|
||||
border-color: $primary;
|
||||
|
||||
&:hover {
|
||||
background: darken($primary, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
|
||||
&.online {
|
||||
background: $success;
|
||||
box-shadow: 0 0 10px $success;
|
||||
}
|
||||
|
||||
&.running {
|
||||
background: $success;
|
||||
animation: blink 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&.standby {
|
||||
background: $primary;
|
||||
animation: blink 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&.maintenance {
|
||||
background: $warning;
|
||||
animation: blink 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&.fault {
|
||||
background: $error;
|
||||
animation: blink 0.8s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 20px rgba($primary, 0.1);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 30px rgba($primary, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gradientMove {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1920px) {
|
||||
#big-screen {
|
||||
transform: scale(0.95);
|
||||
transform-origin: top left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
201
src/views/screens/index.vue
Normal file
201
src/views/screens/index.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div class="screens-container">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" icon="Plus">新建大屏</el-button>
|
||||
<el-button type="success" icon="Refresh">刷新数据</el-button>
|
||||
</div>
|
||||
|
||||
<div class="screens-grid">
|
||||
<div
|
||||
v-for="screen in screenList"
|
||||
:key="screen.id"
|
||||
class="screen-card"
|
||||
:class="{ active: screen.status === 'running' }"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div class="screen-icon">{{ screen.icon }}</div>
|
||||
<div class="screen-info">
|
||||
<h3 class="screen-name">{{ screen.name }}</h3>
|
||||
<span class="screen-type">{{ screen.type }}</span>
|
||||
</div>
|
||||
<el-tag :type="screen.status === 'running' ? 'success' : 'warning'">
|
||||
{{ screen.status === 'running' ? '运行中' : '停止' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<p class="screen-desc">{{ screen.description }}</p>
|
||||
<div class="card-footer">
|
||||
<span class="update-time">更新时间: {{ screen.updateTime }}</span>
|
||||
<div class="actions">
|
||||
<el-button size="small" icon="VideoPlay" @click="startScreen(screen)">启动</el-button>
|
||||
<el-button size="small" icon="Edit">编辑</el-button>
|
||||
<el-button size="small" icon="Delete" type="danger">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const screenList = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '酸轧数据大屏',
|
||||
type: '生产监控',
|
||||
icon: '📊',
|
||||
description: '实时展示酸轧生产线的生产数据和统计信息',
|
||||
status: 'running',
|
||||
updateTime: '2026-05-14 14:30'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '库存总览大屏',
|
||||
type: '数据分析',
|
||||
icon: '📦',
|
||||
description: '展示仓库库存的实时状态和统计信息',
|
||||
status: 'running',
|
||||
updateTime: '2026-05-14 14:25'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '设备状态大屏',
|
||||
type: '设备管理',
|
||||
icon: '🔧',
|
||||
description: '监控生产设备的运行状态和维护信息',
|
||||
status: 'stopped',
|
||||
updateTime: '2026-05-14 10:00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '销售看板',
|
||||
type: '实时报表',
|
||||
icon: '📈',
|
||||
description: '展示销售数据的实时统计和趋势分析',
|
||||
status: 'running',
|
||||
updateTime: '2026-05-14 14:15'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '能源消耗大屏',
|
||||
type: '数据分析',
|
||||
icon: '⚡',
|
||||
description: '监控能源消耗情况和节能分析',
|
||||
status: 'stopped',
|
||||
updateTime: '2026-05-13 16:45'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '质量检测大屏',
|
||||
type: '生产监控',
|
||||
icon: '✅',
|
||||
description: '展示产品质量检测数据和合格率统计',
|
||||
status: 'running',
|
||||
updateTime: '2026-05-14 14:20'
|
||||
}
|
||||
])
|
||||
|
||||
const startScreen = (screen) => {
|
||||
screen.status = 'running'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.screens-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.screens-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.screen-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
border-left: 4px solid #e4e7ed;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.active {
|
||||
border-left-color: #67c23a;
|
||||
box-shadow: 0 4px 16px rgba(103, 194, 58, 0.15);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.screen-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.screen-info {
|
||||
flex: 1;
|
||||
|
||||
.screen-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.screen-type {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
background: #f5f7fa;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.screen-desc {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.update-time {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
34
src/views/system/config/index.vue
Normal file
34
src/views/system/config/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="filter-container">
|
||||
<el-button type="primary">保存配置</el-button>
|
||||
</div>
|
||||
<el-card title="系统配置">
|
||||
<el-form :model="config" label-width="120px">
|
||||
<el-form-item label="系统名称">
|
||||
<el-input v-model="config.systemName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="系统版本">
|
||||
<el-input v-model="config.version" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据刷新间隔">
|
||||
<el-select v-model="config.refreshInterval">
|
||||
<el-option label="10秒" :value="10" />
|
||||
<el-option label="30秒" :value="30" />
|
||||
<el-option label="1分钟" :value="60" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const config = reactive({
|
||||
systemName: '数据管理平台',
|
||||
version: '1.0.0',
|
||||
refreshInterval: 30
|
||||
})
|
||||
</script>
|
||||
180
src/views/system/index.vue
Normal file
180
src/views/system/index.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div class="system-container">
|
||||
<el-tabs type="border-card" class="system-tabs">
|
||||
<el-tab-pane label="基础设置">
|
||||
<div class="setting-section">
|
||||
<h3 class="section-title">系统信息</h3>
|
||||
<el-form :model="systemInfo" label-width="120px">
|
||||
<el-form-item label="系统名称">
|
||||
<el-input v-model="systemInfo.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="版本号">
|
||||
<el-input v-model="systemInfo.version" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="运行时间">
|
||||
<el-input v-model="systemInfo.uptime" disabled />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="setting-section">
|
||||
<h3 class="section-title">大屏设置</h3>
|
||||
<el-form :model="screenSettings" label-width="120px">
|
||||
<el-form-item label="自动刷新间隔">
|
||||
<el-select v-model="screenSettings.refreshInterval">
|
||||
<el-option label="10秒" :value="10" />
|
||||
<el-option label="30秒" :value="30" />
|
||||
<el-option label="1分钟" :value="60" />
|
||||
<el-option label="5分钟" :value="300" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="默认主题">
|
||||
<el-select v-model="screenSettings.theme">
|
||||
<el-option label="深色主题" value="dark" />
|
||||
<el-option label="浅色主题" value="light" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="用户管理">
|
||||
<div class="user-section">
|
||||
<el-button type="primary" icon="Plus" class="add-btn">添加用户</el-button>
|
||||
<el-table :data="users" border>
|
||||
<el-table-column prop="username" label="用户名" width="150" />
|
||||
<el-table-column prop="name" label="真实姓名" width="120" />
|
||||
<el-table-column prop="role" label="角色" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ scope.row.role }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-switch :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button size="small" icon="Edit">编辑</el-button>
|
||||
<el-button size="small" icon="Key">重置密码</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="权限管理">
|
||||
<div class="permission-section">
|
||||
<el-button type="primary" icon="Plus" class="add-btn">添加角色</el-button>
|
||||
<el-table :data="roles" border>
|
||||
<el-table-column prop="name" label="角色名称" width="150" />
|
||||
<el-table-column prop="description" label="角色描述" width="200" />
|
||||
<el-table-column prop="permissions" label="权限" width="300">
|
||||
<template #default="scope">
|
||||
<span v-for="(p, idx) in scope.row.permissions" :key="idx" class="perm-tag">{{ p }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button size="small" icon="Edit">编辑权限</el-button>
|
||||
<el-button size="small" icon="Delete" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div class="footer-actions">
|
||||
<el-button type="primary">保存设置</el-button>
|
||||
<el-button>取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const systemInfo = ref({
|
||||
name: '数据大屏管理系统',
|
||||
version: '1.0.0',
|
||||
uptime: '7天 12小时 30分钟'
|
||||
})
|
||||
|
||||
const screenSettings = ref({
|
||||
refreshInterval: 30,
|
||||
theme: 'dark'
|
||||
})
|
||||
|
||||
const users = ref([
|
||||
{ id: 1, username: 'admin', name: '管理员', role: '超级管理员', status: true },
|
||||
{ id: 2, username: 'zhang', name: '张三', role: '操作员', status: true },
|
||||
{ id: 3, username: 'li', name: '李四', role: '查看员', status: true },
|
||||
{ id: 4, username: 'wang', name: '王五', role: '操作员', status: false }
|
||||
])
|
||||
|
||||
const roles = ref([
|
||||
{ id: 1, name: '超级管理员', description: '拥有系统全部权限', permissions: ['大屏管理', '报表管理', '数据源配置', '系统设置'] },
|
||||
{ id: 2, name: '操作员', description: '可操作大屏和报表', permissions: ['大屏管理', '报表管理'] },
|
||||
{ id: 3, name: '查看员', description: '仅可查看数据', permissions: ['大屏查看', '报表查看'] }
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.system-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.system-tabs {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.setting-section {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.user-section, .permission-section {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
|
||||
.add-btn {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.perm-tag {
|
||||
background: #f0f5ff;
|
||||
color: #409eff;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
--el-table-header-text-color: #606266;
|
||||
--el-table-row-hover-bg-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
121
src/views/system/menu/index.vue
Normal file
121
src/views/system/menu/index.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="search-area">
|
||||
<el-input placeholder="菜单名称" class="search-item" style="width: 200px" />
|
||||
<el-select placeholder="菜单类型" class="search-item" style="width: 150px">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="目录" value="0" />
|
||||
<el-option label="菜单" value="1" />
|
||||
<el-option label="按钮" value="2" />
|
||||
</el-select>
|
||||
<el-button type="primary" class="search-item">查询</el-button>
|
||||
<el-button class="search-item">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<el-button type="primary">新增菜单</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<el-table :data="menuList" border :tree-props="{ children: 'children' }">
|
||||
<el-table-column prop="menuId" label="菜单ID" width="80" />
|
||||
<el-table-column prop="menuName" label="菜单名称" />
|
||||
<el-table-column prop="path" label="路由路径" />
|
||||
<el-table-column prop="component" label="组件路径" />
|
||||
<el-table-column prop="menuType" label="类型" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getMenuTypeTag(scope.row.menuType)">
|
||||
{{ getMenuTypeName(scope.row.menuType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="icon" label="图标" width="80" />
|
||||
<el-table-column prop="sort" label="排序" width="80" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button type="text">编辑</el-button>
|
||||
<el-button type="text">新增子菜单</el-button>
|
||||
<el-button type="text">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const menuList = ref([
|
||||
{
|
||||
menuId: 1, menuName: '系统管理', path: '/system', component: 'Layout', menuType: '0', icon: 'system', sort: 1, status: 1,
|
||||
children: [
|
||||
{ menuId: 11, menuName: '用户管理', path: '/system/user', component: 'system/user/index', menuType: '1', icon: 'user', sort: 1, status: 1 },
|
||||
{ menuId: 12, menuName: '角色管理', path: '/system/role', component: 'system/role/index', menuType: '1', icon: 'role', sort: 2, status: 1 },
|
||||
{ menuId: 13, menuName: '菜单管理', path: '/system/menu', component: 'system/menu/index', menuType: '1', icon: 'menu', sort: 3, status: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
menuId: 2, menuName: '大屏管理', path: '/screens', component: 'Layout', menuType: '0', icon: 'monitor', sort: 2, status: 1,
|
||||
children: [
|
||||
{ menuId: 21, menuName: '大屏列表', path: '/screens', component: 'screens/index', menuType: '1', icon: 'list', sort: 1, status: 1 },
|
||||
{ menuId: 22, menuName: '酸轧大屏', path: '/screens/acid-rolling', component: 'screens/acid-rolling/index', menuType: '1', icon: 'chart', sort: 2, status: 1 }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const getMenuTypeName = (type) => {
|
||||
const map = { '0': '目录', '1': '菜单', '2': '按钮' }
|
||||
return map[type] || '未知'
|
||||
}
|
||||
|
||||
const getMenuTypeTag = (type) => {
|
||||
const map = { '0': 'warning', '1': 'primary', '2': 'info' }
|
||||
return map[type] || 'default'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-area {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
.search-item {
|
||||
display: inline-block;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.el-button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
109
src/views/system/role/index.vue
Normal file
109
src/views/system/role/index.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="search-area">
|
||||
<el-input placeholder="角色名称" class="search-item" style="width: 200px" />
|
||||
<el-select placeholder="状态" class="search-item" style="width: 150px">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="启用" value="1" />
|
||||
<el-option label="禁用" value="0" />
|
||||
</el-select>
|
||||
<el-button type="primary" class="search-item">查询</el-button>
|
||||
<el-button class="search-item">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<el-button type="primary">新增角色</el-button>
|
||||
<el-button>批量删除</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<el-table :data="roleList" border>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="roleId" label="角色ID" width="80" />
|
||||
<el-table-column prop="roleName" label="角色名称" />
|
||||
<el-table-column prop="roleKey" label="角色标识" />
|
||||
<el-table-column prop="description" label="描述" />
|
||||
<el-table-column prop="sort" label="排序" width="80" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button type="text">编辑</el-button>
|
||||
<el-button type="text">权限设置</el-button>
|
||||
<el-button type="text" :type="scope.row.status === 1 ? 'warning' : 'success'">
|
||||
{{ scope.row.status === 1 ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
:current-page="1"
|
||||
:page-size="50"
|
||||
:total="50"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const roleList = ref([
|
||||
{ roleId: 1, roleName: '超级管理员', roleKey: 'admin', description: '系统超级管理员', sort: 1, status: 1, createTime: '2024-01-01 10:00:00' },
|
||||
{ roleId: 2, roleName: '普通用户', roleKey: 'user', description: '普通用户权限', sort: 2, status: 1, createTime: '2024-01-02 10:00:00' },
|
||||
{ roleId: 3, roleName: '大屏管理员', roleKey: 'screen_admin', description: '大屏管理权限', sort: 3, status: 0, createTime: '2024-01-03 10:00:00' }
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-area {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
.search-item {
|
||||
display: inline-block;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.el-button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
121
src/views/system/user/index.vue
Normal file
121
src/views/system/user/index.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="search-area">
|
||||
<el-input placeholder="用户名" class="search-item" style="width: 200px" />
|
||||
<el-input placeholder="手机号" class="search-item" style="width: 200px" />
|
||||
<el-select placeholder="状态" class="search-item" style="width: 150px">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="启用" value="1" />
|
||||
<el-option label="禁用" value="0" />
|
||||
</el-select>
|
||||
<el-button type="primary" class="search-item">查询</el-button>
|
||||
<el-button class="search-item">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<el-button type="primary">新增用户</el-button>
|
||||
<el-button>批量删除</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<el-table :data="userList" border>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="userId" label="用户ID" width="80" />
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="realName" label="真实姓名" />
|
||||
<el-table-column prop="phone" label="手机号" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
<el-table-column prop="roleName" label="角色" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button type="text">编辑</el-button>
|
||||
<el-button type="text">分配角色</el-button>
|
||||
<el-button type="text" :type="scope.row.status === 1 ? 'warning' : 'success'">
|
||||
{{ scope.row.status === 1 ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
:current-page="1"
|
||||
:page-size="50"
|
||||
:total="100"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const userList = ref([
|
||||
{ userId: 1, username: 'admin', realName: '管理员', phone: '13800138000', email: 'admin@example.com', roleName: '超级管理员', status: 1, createTime: '2024-01-01 10:00:00' },
|
||||
{ userId: 2, username: 'user1', realName: '张三', phone: '13800138001', email: 'user1@example.com', roleName: '普通用户', status: 1, createTime: '2024-01-02 10:00:00' },
|
||||
{ userId: 3, username: 'user2', realName: '李四', phone: '13800138002', email: 'user2@example.com', roleName: '普通用户', status: 0, createTime: '2024-01-03 10:00:00' }
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-area {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
.search-item {
|
||||
display: inline-block;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.el-button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-table) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper),
|
||||
:deep(.el-table__body-wrapper) {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
32
vite.config.js
Normal file
32
vite.config.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/da': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/pocket': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/l2': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
174
启动指南.md
Normal file
174
启动指南.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# 数据大屏管理系统 - 启动指南
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
klp-oa/
|
||||
├── klp-oa/ # Java后端项目(已存在)
|
||||
│ ├── klp-admin/ # 启动模块
|
||||
│ ├── klp-wms/ # 仓储管理模块(新增大屏接口)
|
||||
│ ├── klp-framework/ # 框架核心
|
||||
│ └── pom.xml # Maven配置
|
||||
└── screen/ # Vue前端项目
|
||||
├── src/
|
||||
│ ├── modules/dashboardBig/ # 大屏模块
|
||||
│ ├── views/ # 管理页面
|
||||
│ ├── router/ # 路由配置
|
||||
│ └── api/ # API封装
|
||||
├── server/ # Node.js模拟后端
|
||||
└── package.json # npm依赖
|
||||
```
|
||||
|
||||
## 🚀 启动步骤
|
||||
|
||||
### 第一步:启动后端服务(Java)
|
||||
|
||||
1. **打开 IDE**(IntelliJ IDEA 或 Eclipse)
|
||||
2. **导入项目**:打开 `klp-oa/klp-oa/` 目录
|
||||
3. **配置数据库连接**:
|
||||
- 编辑 `klp-admin/src/main/resources/application-dev.yml`
|
||||
- 确保数据库配置正确:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
datasource:
|
||||
dynamic:
|
||||
datasource:
|
||||
master:
|
||||
url: jdbc:mysql://140.143.206.120:13306/klp-oa-test?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
|
||||
username: klp
|
||||
password: KeLunPu@123
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
acid:
|
||||
url: jdbc:mysql://140.143.206.120:13306/klp_pocketfactory?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
|
||||
username: klp
|
||||
password: KeLunPu@123
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
```
|
||||
|
||||
4. **运行启动类**:
|
||||
- 找到 `klp-admin/src/main/java/com/klp/KlpAdminApplication.java`
|
||||
- 右键运行 `Run 'KlpAdminApplication'`
|
||||
|
||||
5. **验证后端服务**:
|
||||
- 访问 http://localhost:8080/api/wms/product/dashboard/overview
|
||||
- 应该返回 JSON 数据
|
||||
|
||||
### 第二步:启动前端服务(Vue)
|
||||
|
||||
1. **打开命令行**(PowerShell 或 CMD)
|
||||
2. **进入前端目录**:
|
||||
```bash
|
||||
cd d:\klp-oa\screen
|
||||
```
|
||||
|
||||
3. **安装依赖**(首次运行):
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
4. **启动开发服务器**:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
5. **访问前端页面**:
|
||||
- 打开浏览器访问 http://localhost:5173/
|
||||
|
||||
## 🌐 访问地址
|
||||
|
||||
| 页面 | URL |
|
||||
|------|-----|
|
||||
| **系统首页** | http://localhost:5173/ |
|
||||
| **主数据大屏** | http://localhost:5173/dashboard |
|
||||
| **订单大屏** | http://localhost:5173/dashboard/order |
|
||||
| **成本大屏** | http://localhost:5173/dashboard/cost |
|
||||
| **能源大屏** | http://localhost:5173/dashboard/energy |
|
||||
|
||||
## 🔗 API接口地址
|
||||
|
||||
| 接口 | URL |
|
||||
|------|-----|
|
||||
| 大屏概览 | http://localhost:8080/api/wms/product/dashboard/overview |
|
||||
| 产品排行 | http://localhost:8080/api/wms/productSalesScript/dashboard/ranking |
|
||||
| 生产计划 | http://localhost:8080/api/wms/business/dashboard/currentPlan |
|
||||
| 工艺参数 | http://localhost:8080/api/wms/business/dashboard/currentProcess |
|
||||
| 酸轧产出报表 | http://localhost:8080/api/wms/acid-rolling/report/output |
|
||||
| 酸轧停机报表 | http://localhost:8080/api/wms/acid-rolling/report/stop |
|
||||
|
||||
## ⚙️ 配置说明
|
||||
|
||||
### 前端API配置
|
||||
|
||||
编辑 `screen/.env` 文件:
|
||||
|
||||
```env
|
||||
# 默认连接Java后端
|
||||
VUE_APP_API_BASE_URL=http://localhost:8080/api
|
||||
|
||||
# 备用:连接Node.js模拟后端
|
||||
# VUE_APP_API_BASE_URL=http://localhost:3000/api
|
||||
```
|
||||
|
||||
### 大屏数据来源
|
||||
|
||||
当前系统使用模拟数据,如需连接真实的 KLPL3 接口:
|
||||
|
||||
1. 在后端 `DashboardController.java` 中修改接口实现
|
||||
2. 调用 KLPL3 真实接口获取数据
|
||||
3. 注意接口地址和认证方式
|
||||
|
||||
## 📋 功能清单
|
||||
|
||||
### 已实现功能
|
||||
|
||||
- ✅ 系统首页(统计卡片、图表、活动列表)
|
||||
- ✅ 大屏管理(大屏列表、启动/停止)
|
||||
- ✅ 酸轧数据大屏(生产监控)
|
||||
- ✅ 订单大屏
|
||||
- ✅ 成本大屏
|
||||
- ✅ 能源大屏
|
||||
- ✅ 酸轧产出报表
|
||||
- ✅ 酸轧停机报表
|
||||
- ✅ 数据源配置
|
||||
- ✅ 系统设置
|
||||
|
||||
### 预留接口位置
|
||||
|
||||
在 `DashboardController.java` 中预留了 KLPL3 接口对接位置:
|
||||
|
||||
```java
|
||||
// TODO: 对接KLPL3真实接口
|
||||
// @GetMapping("/wms/product/dashboard/overview")
|
||||
// public R<DashboardOverviewVO> getDashboardOverview() {
|
||||
// // 调用KLPL3接口
|
||||
// // return klpL3Service.getDashboardData();
|
||||
// }
|
||||
```
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### Q1: 前端页面空白?
|
||||
|
||||
**解决方案**:
|
||||
1. 检查浏览器控制台(F12)查看错误信息
|
||||
2. 确保后端服务正常运行
|
||||
3. 清除浏览器缓存(Ctrl+Shift+Delete)
|
||||
|
||||
### Q2: 数据库连接失败?
|
||||
|
||||
**解决方案**:
|
||||
1. 检查数据库配置文件中的地址、端口、用户名、密码
|
||||
2. 确保数据库服务正常运行
|
||||
3. 检查网络连接(是否能访问数据库服务器)
|
||||
|
||||
### Q3: 大屏页面显示异常?
|
||||
|
||||
**解决方案**:
|
||||
1. 确保浏览器窗口分辨率足够(建议1920×1080)
|
||||
2. 检查浏览器是否支持 WebGL(ECharts需要)
|
||||
3. 禁用浏览器扩展后重试
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如有其他问题,请联系开发人员。
|
||||
Reference in New Issue
Block a user