Commit b599ac8f authored by luwei's avatar luwei

数据管理功能

parent 6ca52b5b
Pipeline #362 failed with stages
from contextlib import contextmanager
from typing import Generator
from sqlalchemy import create_engine, text
from sqlalchemy import create_engine
from sqlalchemy.engine import URL
from sqlalchemy.orm import Session, declarative_base, sessionmaker
......@@ -38,40 +38,3 @@ def db_session() -> Generator[Session, None, None]:
yield session
finally:
session.close()
def init_database() -> None:
admin_engine = create_engine(_build_mysql_url(), pool_pre_ping=True)
try:
with admin_engine.begin() as connection:
connection.execute(
text(
f"CREATE DATABASE IF NOT EXISTS `{settings.mysql_database}` "
'CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'
)
)
finally:
admin_engine.dispose()
with engine.begin() as connection:
connection.execute(
text(
"""
CREATE TABLE IF NOT EXISTS categories (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL COMMENT '分类名称',
type ENUM('data_file', 'data_package') NOT NULL COMMENT '分类类型',
parent_id BIGINT DEFAULT NULL COMMENT '父分类ID',
sort_order INT DEFAULT 0 COMMENT '排序',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_type (type),
INDEX idx_parent (parent_id)
) COMMENT='数据分类表'
"""
)
)
from app.models import data_management # noqa: F401
Base.metadata.create_all(bind=engine)
\ No newline at end of file
......@@ -4,7 +4,6 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from app.api.data_management import router as data_management_router
from app.database import init_database
from app.utils.response import error_response, success_response
......@@ -47,10 +46,6 @@ def create_app() -> FastAPI:
async def health_check():
return success_response(data={'status': 'ok'}, message='服务正常')
@app.on_event('startup')
def startup_event():
init_database()
app.include_router(data_management_router, prefix='/api/data', tags=['数据管理'])
return app
......
......@@ -39,6 +39,7 @@ class DataFile(Base):
id: Mapped[int] = mapped_column(BIGINT, primary_key=True, autoincrement=True)
filename: Mapped[str] = mapped_column(String(255), nullable=False, comment='原始文件名')
stored_name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, comment='存储文件名')
file_path: Mapped[str] = mapped_column(String(255), nullable=False, comment='文件路径')
category_id: Mapped[int | None] = mapped_column(BIGINT, nullable=True, comment='分类ID')
data_count: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text('0'), comment='数据条数')
uploaded_at: Mapped[str] = mapped_column(TIMESTAMP, nullable=False, server_default=text('CURRENT_TIMESTAMP'))
......
......@@ -8,6 +8,8 @@
"name": "frontend",
"version": "0.0.0",
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"echarts": "^6.0.0",
"pinia": "^3.0.4",
"vue": "^3.5.31",
"vue-router": "^5.0.4"
......@@ -500,7 +502,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
"integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"vue": "^3.2.0"
......@@ -1939,6 +1940,22 @@
"node": ">= 0.4"
}
},
"node_modules/echarts": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "6.0.0"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/electron-to-chromium": {
"version": "1.5.338",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.338.tgz",
......@@ -4067,6 +4084,21 @@
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/zrender": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
}
}
}
......@@ -10,14 +10,15 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"echarts": "^6.0.0",
"pinia": "^3.0.4",
"vue": "^3.5.31",
"vue-router": "^5.0.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.5",
"axios": "^1.12.2",
"element-plus": "^2.11.3",
"@vitejs/plugin-vue": "^6.0.5",
"sass-embedded": "^1.99.0",
"vite": "^8.0.3",
"vite-plugin-vue-devtools": "^8.1.1"
......
<script setup>
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import * as echarts from 'echarts'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
const props = defineProps({
records: {
......@@ -8,124 +9,221 @@ const props = defineProps({
},
})
const canvasRef = ref(null)
const chartRef = ref(null)
let chartInstance = null
const numericPoints = computed(() => {
return props.records
const chartData = computed(() => {
const source = Array.isArray(props.records) ? props.records : []
return source
.map((item, idx) => ({
x: idx,
idx,
time: item.time || `第${idx + 1}条`,
current: Number(item.current),
voltage: Number(item.voltage),
temperature: Number(item.temperature),
actual_temperature: Number(item.actual_temperature),
}))
.filter((item) => {
return (
Number.isFinite(item.current) ||
Number.isFinite(item.voltage) ||
Number.isFinite(item.temperature)
Number.isFinite(item.actual_temperature)
)
})
})
const draw = () => {
const canvas = canvasRef.value
if (!canvas) {
return
const stats = computed(() => {
const temps = chartData.value
.map((item) => item.actual_temperature)
.filter((value) => Number.isFinite(value))
return {
count: chartData.value.length,
max: temps.length ? Math.max(...temps) : 0,
min: temps.length ? Math.min(...temps) : 0,
}
})
const dpr = window.devicePixelRatio || 1
const width = canvas.clientWidth
const height = canvas.clientHeight
canvas.width = width * dpr
canvas.height = height * dpr
const formatValue = (value) => {
return Number.isFinite(value) ? Number(value).toFixed(2) : '--'
}
const ctx = canvas.getContext('2d')
ctx.scale(dpr, dpr)
ctx.clearRect(0, 0, width, height)
const getSeriesData = (key) => {
return chartData.value.map((item) => (Number.isFinite(item[key]) ? item[key] : null))
}
if (numericPoints.value.length < 2) {
ctx.fillStyle = '#64748b'
ctx.font = '14px Poppins, Noto Sans SC, sans-serif'
ctx.fillText('暂无足够数据生成曲线', 20, height / 2)
const renderChart = () => {
if (!chartRef.value) {
return
}
const padding = { top: 18, right: 20, bottom: 32, left: 44 }
const innerWidth = width - padding.left - padding.right
const innerHeight = height - padding.top - padding.bottom
if (!chartInstance) {
chartInstance = echarts.init(chartRef.value)
}
const allValues = numericPoints.value
.flatMap((item) => [item.current, item.voltage, item.temperature])
.filter((value) => Number.isFinite(value))
const hasData = chartData.value.length > 0
const minValue = Math.min(...allValues)
const maxValue = Math.max(...allValues)
const diff = maxValue - minValue || 1
ctx.strokeStyle = 'rgba(15, 23, 42, 0.14)'
ctx.lineWidth = 1
ctx.beginPath()
ctx.moveTo(padding.left, padding.top)
ctx.lineTo(padding.left, height - padding.bottom)
ctx.lineTo(width - padding.right, height - padding.bottom)
ctx.stroke()
const drawLine = (key, color) => {
ctx.beginPath()
let started = false
numericPoints.value.forEach((point, index) => {
const value = point[key]
if (!Number.isFinite(value)) {
return
}
const x = padding.left + (index / (numericPoints.value.length - 1)) * innerWidth
const y = padding.top + ((maxValue - value) / diff) * innerHeight
if (!started) {
ctx.moveTo(x, y)
started = true
} else {
ctx.lineTo(x, y)
}
})
chartInstance.setOption(
{
animation: true,
color: ['#409EFF', '#67C23A', '#FF6B6B'],
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255,255,255,0.96)',
borderColor: '#e2e8f0',
borderWidth: 1,
textStyle: {
color: '#334155',
},
extraCssText: 'box-shadow: 0 10px 30px rgba(15,23,42,0.16); border-radius: 12px;',
formatter(params) {
if (!params?.length) {
return ''
}
if (started) {
ctx.strokeStyle = color
ctx.lineWidth = 2
ctx.stroke()
}
}
const lines = [`<div style="margin-bottom:6px;font-weight:600;">${params[0].axisValue}</div>`]
params.forEach((item) => {
lines.push(
`<div style="display:flex;align-items:center;gap:6px;min-width:160px;justify-content:space-between;">
<span>${item.marker}${item.seriesName}</span>
<strong>${formatValue(item.data)}</strong>
</div>`,
)
})
return lines.join('')
},
},
legend: {
bottom: 0,
itemWidth: 18,
itemHeight: 10,
textStyle: {
color: '#475569',
},
data: ['电流(A)', '电压(V)', '温度(℃)'],
},
grid: {
top: 24,
left: 24,
right: 24,
bottom: 56,
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: chartData.value.map((item) => item.time),
axisLabel: {
color: '#64748b',
rotate: 35,
},
axisLine: {
lineStyle: {
color: '#94a3b8',
},
},
},
yAxis: {
type: 'value',
axisLabel: {
color: '#64748b',
},
splitLine: {
lineStyle: {
type: 'dashed',
color: 'rgba(148, 163, 184, 0.45)',
},
},
},
series: [
{
name: '电流(A)',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 7,
data: getSeriesData('current'),
},
{
name: '电压(V)',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 7,
data: getSeriesData('voltage'),
},
{
name: '温度(℃)',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 7,
data: getSeriesData('actual_temperature'),
},
],
graphic: hasData
? []
: [
{
type: 'text',
left: 'center',
top: 'middle',
style: {
text: '暂无足够数据生成曲线',
fill: '#64748b',
fontSize: 14,
},
},
],
},
true,
)
}
drawLine('current', '#0ea5e9')
drawLine('voltage', '#f97316')
drawLine('temperature', '#ef4444')
const resizeChart = () => {
chartInstance?.resize()
}
onMounted(async () => {
await nextTick()
draw()
renderChart()
window.addEventListener('resize', resizeChart)
})
watch(
() => props.records,
async () => {
await nextTick()
draw()
renderChart()
},
{ deep: true },
)
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeChart)
chartInstance?.dispose()
chartInstance = null
})
</script>
<template>
<div class="curve-box">
<div class="legend-row">
<span class="legend-item"><i class="dot dot-current" />电流</span>
<span class="legend-item"><i class="dot dot-voltage" />电压</span>
<span class="legend-item"><i class="dot dot-temperature" />温度</span>
<div class="stats-row">
<div class="stat-card">
<div class="stat-label">总条数</div>
<div class="stat-value">{{ stats.count }}</div>
</div>
<div class="stat-card">
<div class="stat-label">最高温度</div>
<div class="stat-value">{{ formatValue(stats.max) }} °C</div>
</div>
<div class="stat-card">
<div class="stat-label">最低温度</div>
<div class="stat-value">{{ formatValue(stats.min) }} °C</div>
</div>
</div>
<canvas ref="canvasRef" class="curve-canvas" />
<div ref="chartRef" class="echarts-box" />
</div>
</template>
......@@ -134,46 +232,38 @@ watch(
height: 100%;
display: flex;
flex-direction: column;
gap: 10px;
gap: 14px;
}
.legend-row {
display: flex;
gap: 16px;
color: #334155;
font-size: 13px;
.stats-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.legend-item {
display: inline-flex;
align-items: center;
gap: 6px;
.stat-card {
padding: 14px 16px;
border-radius: 14px;
background: #f3f6fb;
}
.dot {
width: 9px;
height: 9px;
border-radius: 50%;
}
.dot-current {
background: #0ea5e9;
}
.dot-voltage {
background: #f97316;
.stat-label {
font-size: 13px;
color: #64748b;
margin-bottom: 8px;
}
.dot-temperature {
background: #ef4444;
.stat-value {
font-size: 18px;
font-weight: 700;
color: #0f172a;
}
.curve-canvas {
width: 100%;
.echarts-box {
flex: 1;
min-height: 280px;
border-radius: 12px;
border: 1px solid rgba(15, 23, 42, 0.12);
min-height: 360px;
border-radius: 14px;
border: 1px solid rgba(15, 23, 42, 0.1);
background: linear-gradient(180deg, #ffffff, #f8fafc);
}
</style>
This diff is collapsed.
CREATE DATABASE IF NOT EXISTS thermal_control_system
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE thermal_control_system;
CREATE TABLE IF NOT EXISTS categories (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL COMMENT '分类名称',
type ENUM('data_file', 'data_package') NOT NULL COMMENT '分类类型',
parent_id BIGINT DEFAULT NULL COMMENT '父分类ID',
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_type (type),
INDEX idx_parent (parent_id)
) COMMENT='数据分类表';
CREATE TABLE IF NOT EXISTS data_files (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
filename VARCHAR(255) NOT NULL COMMENT '原始文件名',
stored_name VARCHAR(255) NOT NULL UNIQUE COMMENT '存储文件名',
file_path VARCHAR(500) NOT NULL COMMENT '文件路径',
category_id BIGINT DEFAULT NULL COMMENT '分类ID',
data_count INT NOT NULL DEFAULT 0 COMMENT '数据条数',
uploaded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
remark TEXT NULL COMMENT '备注',
INDEX idx_file_category (category_id)
) COMMENT='数据文件表';
ALTER TABLE data_files
ADD COLUMN IF NOT EXISTS file_path VARCHAR(500) NOT NULL COMMENT '文件路径';
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment