初始化:静态菜单版 数据大屏管理系统,对接KLPL3数据库

This commit is contained in:
2026-05-15 18:18:51 +08:00
commit 39fed2c08c
58 changed files with 12751 additions and 0 deletions

2
.env Normal file
View File

@@ -0,0 +1,2 @@
VUE_APP_API_BASE_URL=http://localhost:8080/api
VUE_APP_TITLE=数据大屏管理系统

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
dist/

13
index.html Normal file
View 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
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

3224
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,9 @@
<template>
<router-view />
</template>
<script setup>
</script>
<style scoped>
</style>

24
src/api/acidRolling.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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'
})
}

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

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

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

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

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

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

View 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
View 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
View 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')

View 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;

View 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);
}
}

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

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

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

View 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
View 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
View 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
View 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
}

View 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
}

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

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

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

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

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

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

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

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

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

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

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

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

View 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
View 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
View 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. 检查浏览器是否支持 WebGLECharts需要
3. 禁用浏览器扩展后重试
## 📞 技术支持
如有其他问题,请联系开发人员。