Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
T
thermal-control-system
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
luwei
thermal-control-system
Commits
b599ac8f
Commit
b599ac8f
authored
Apr 16, 2026
by
luwei
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
数据管理功能
parent
6ca52b5b
Pipeline
#362
failed with stages
Changes
10
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
580 additions
and
229 deletions
+580
-229
database.py
backend/app/database.py
+1
-38
main.py
backend/app/main.py
+0
-5
data_management.py
backend/app/models/data_management.py
+1
-0
data_management_service.py
backend/app/services/data_management_service.py
+96
-39
温度2_20260416165018140191_19日15时38分33秒转换结果_回路76_-对应控点1.csv
...温度2_20260416165018140191_19日15时38分33秒转换结果_回路76_-对应控点1.csv
+0
-0
package-lock.json
frontend/package-lock.json
+33
-1
package.json
frontend/package.json
+2
-1
DataCurve.vue
frontend/src/views/DataManagement/components/DataCurve.vue
+196
-106
index.vue
frontend/src/views/DataManagement/index.vue
+219
-39
init_tables.sql
sql/init_tables.sql
+32
-0
No files found.
backend/app/database.py
View file @
b599ac8f
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
backend/app/main.py
View file @
b599ac8f
...
...
@@ -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
...
...
backend/app/models/data_management.py
View file @
b599ac8f
...
...
@@ -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'
))
...
...
backend/app/services/data_management_service.py
View file @
b599ac8f
import
csv
import
io
from
datetime
import
datetime
from
pathlib
import
Path
...
...
@@ -13,9 +14,10 @@ from app.models import Category, DataFile
class
DataManagementService
:
def
__init__
(
self
)
->
None
:
self
.
_base_dir
=
Path
(
__file__
)
.
resolve
()
.
parents
[
3
]
self
.
_base_dir
=
Path
(
__file__
)
.
resolve
()
.
parents
[
2
]
self
.
_upload_dir
=
self
.
_base_dir
/
'uploads'
self
.
_data_dir
=
self
.
_upload_dir
/
'data'
self
.
_legacy_data_dir
=
Path
(
__file__
)
.
resolve
()
.
parents
[
3
]
/
'uploads'
/
'data'
self
.
_upload_dir
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
self
.
_data_dir
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
...
...
@@ -146,6 +148,7 @@ class DataManagementService:
'id'
:
item
.
id
,
'filename'
:
item
.
filename
,
'stored_name'
:
item
.
stored_name
,
'file_path'
:
item
.
file_path
,
'category_id'
:
item
.
category_id
,
'uploaded_at'
:
item
.
uploaded_at
.
strftime
(
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
)
if
item
.
uploaded_at
else
''
,
'data_count'
:
item
.
data_count
,
...
...
@@ -155,38 +158,39 @@ class DataManagementService:
def
upload_file
(
self
,
filename
:
str
,
content
:
bytes
,
category_id
:
str
=
'all'
)
->
dict
[
str
,
Any
]:
suffix
=
Path
(
filename
)
.
suffix
.
lower
()
if
suffix
not
in
{
'.xlsx'
,
'.xls'
}:
raise
ValueError
(
'仅支持
Excel 文件(.xlsx/.xls
)'
)
if
suffix
not
in
{
'.xlsx'
,
'.xls'
,
'.csv'
}:
raise
ValueError
(
'仅支持
数据文件(.xlsx/.xls/.csv
)'
)
category_db_id
:
int
|
None
if
category_id
in
{
''
,
'all'
,
None
}:
category_db_id
=
None
else
:
raise
ValueError
(
'请先选择分类后再上传文件'
)
category_db_id
=
self
.
_parse_int_id
(
category_id
,
'分类ID'
)
with
db_session
()
as
session
:
category
=
(
session
.
query
(
Category
)
.
filter
(
Category
.
id
==
category_db_id
,
Category
.
type
==
'data_file'
)
.
first
()
)
if
not
category
:
raise
ValueError
(
'上传分类不存在'
)
timestamp
=
datetime
.
now
()
.
strftime
(
'
%
Y
%
m
%
d
%
H
%
M
%
S
%
f'
)
stored_name
=
f
'{timestamp}_{Path(filename).name}'
safe_category_name
=
self
.
_sanitize_filename
(
category
.
name
)
safe_original_name
=
self
.
_sanitize_filename
(
Path
(
filename
)
.
stem
)
stored_name
=
f
'{safe_category_name}_{timestamp}_{safe_original_name}{suffix}'
target_path
=
self
.
_data_dir
/
stored_name
db_file_path
=
f
'/app/uploads/data/{stored_name}'
with
target_path
.
open
(
'wb'
)
as
file
:
file
.
write
(
content
)
_
,
total_count
=
self
.
_read_records
(
target_path
,
limit
=
None
)
try
:
with
db_session
()
as
session
:
if
category_db_id
is
not
None
:
category_exists
=
(
session
.
query
(
Category
)
.
filter
(
Category
.
id
==
category_db_id
,
Category
.
type
==
'data_file'
)
.
first
()
)
if
not
category_exists
:
raise
ValueError
(
'上传分类不存在'
)
_
,
total_count
=
self
.
_read_records
(
target_path
,
limit
=
None
)
file_meta
=
DataFile
(
filename
=
filename
,
stored_name
=
stored_name
,
file_path
=
db_file_path
,
category_id
=
category_db_id
,
data_count
=
total_count
,
)
...
...
@@ -198,6 +202,7 @@ class DataManagementService:
'id'
:
file_meta
.
id
,
'filename'
:
file_meta
.
filename
,
'stored_name'
:
file_meta
.
stored_name
,
'file_path'
:
file_meta
.
file_path
,
'category_id'
:
file_meta
.
category_id
,
'uploaded_at'
:
file_meta
.
uploaded_at
.
strftime
(
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
)
if
file_meta
.
uploaded_at
else
''
,
'data_count'
:
file_meta
.
data_count
,
...
...
@@ -215,7 +220,7 @@ class DataManagementService:
if
not
matched
:
raise
ValueError
(
'文件不存在'
)
target_path
=
self
.
_
data_dir
/
matched
.
stored_name
target_path
=
self
.
_
resolve_local_file_path
(
matched
.
file_path
,
matched
.
stored_name
)
if
target_path
.
exists
():
target_path
.
unlink
()
...
...
@@ -231,7 +236,7 @@ class DataManagementService:
if
not
file_meta
:
raise
ValueError
(
'文件不存在'
)
target_path
=
self
.
_
data_dir
/
file_meta
.
stored_name
target_path
=
self
.
_
resolve_local_file_path
(
file_meta
.
file_path
,
file_meta
.
stored_name
)
if
not
target_path
.
exists
():
raise
ValueError
(
'文件已丢失,请重新上传'
)
...
...
@@ -248,13 +253,16 @@ class DataManagementService:
return
[],
0
first_row
=
[
item
.
strip
()
for
item
in
rows
[
0
]]
has_header
=
any
(
self
.
_normalize_header
(
item
)
in
{
'time'
,
'current'
,
'voltage'
,
'temperature'
}
for
item
in
first_row
)
has_header
=
any
(
self
.
_normalize_header
(
item
)
in
{
'time'
,
'current'
,
'voltage'
,
'set_temperature'
,
'actual_temperature'
}
for
item
in
first_row
)
if
has_header
:
header
=
first_row
data_rows
=
rows
[
1
:]
else
:
header
=
[
'time'
,
'current'
,
'voltage'
,
'temperature'
]
header
=
[
'time'
,
'current'
,
'voltage'
,
'
set_temperature'
,
'actual_
temperature'
]
data_rows
=
rows
index_map
=
self
.
_build_index_map
(
header
)
...
...
@@ -272,7 +280,8 @@ class DataManagementService:
'time'
:
self
.
_read_value
(
row
,
index_map
[
'time'
]),
'current'
:
self
.
_to_float
(
self
.
_read_value
(
row
,
index_map
[
'current'
])),
'voltage'
:
self
.
_to_float
(
self
.
_read_value
(
row
,
index_map
[
'voltage'
])),
'temperature'
:
self
.
_to_float
(
self
.
_read_value
(
row
,
index_map
[
'temperature'
])),
'set_temperature'
:
self
.
_to_float
(
self
.
_read_value
(
row
,
index_map
[
'set_temperature'
])),
'actual_temperature'
:
self
.
_to_float
(
self
.
_read_value
(
row
,
index_map
[
'actual_temperature'
])),
}
)
...
...
@@ -283,7 +292,8 @@ class DataManagementService:
'time'
:
0
,
'current'
:
1
,
'voltage'
:
2
,
'temperature'
:
3
,
'set_temperature'
:
3
,
'actual_temperature'
:
4
,
}
for
index
,
name
in
enumerate
(
header
):
...
...
@@ -294,8 +304,10 @@ class DataManagementService:
index_map
[
'current'
]
=
index
elif
normalized
==
'voltage'
:
index_map
[
'voltage'
]
=
index
elif
normalized
==
'temperature'
:
index_map
[
'temperature'
]
=
index
elif
normalized
==
'set_temperature'
:
index_map
[
'set_temperature'
]
=
index
elif
normalized
==
'actual_temperature'
:
index_map
[
'actual_temperature'
]
=
index
return
index_map
...
...
@@ -307,10 +319,35 @@ class DataManagementService:
return
'current'
if
key
in
{
'voltage'
,
'电压'
}:
return
'voltage'
if
key
in
{
'temperature'
,
'温度'
}:
return
'temperature'
if
key
in
{
'set_temperature'
,
'设定温度'
,
'set temperature'
}:
return
'set_temperature'
if
key
in
{
'actual_temperature'
,
'实际温度'
,
'actual temperature'
,
'temperature'
,
'温度'
}:
return
'actual_temperature'
return
key
def
_sanitize_filename
(
self
,
value
:
str
)
->
str
:
cleaned
=
''
.
join
(
char
if
char
.
isalnum
()
or
char
in
{
'_'
,
'-'
,
'一'
,
'二'
,
'三'
,
'四'
,
'五'
,
'六'
,
'七'
,
'八'
,
'九'
,
'十'
,
'升'
,
'温'
,
'恒'
,
'降'
,
'实'
,
'验'
}
else
'_'
for
char
in
value
.
strip
())
compacted
=
'_'
.
join
(
part
for
part
in
cleaned
.
split
(
'_'
)
if
part
)
return
compacted
or
'file'
def
_resolve_local_file_path
(
self
,
db_file_path
:
str
,
stored_name
:
str
)
->
Path
:
def
_with_legacy_fallback
(
name
:
str
)
->
Path
:
current_path
=
self
.
_data_dir
/
name
if
current_path
.
exists
():
return
current_path
legacy_path
=
self
.
_legacy_data_dir
/
name
if
legacy_path
.
exists
():
return
legacy_path
return
current_path
if
db_file_path
:
normalized
=
db_file_path
.
replace
(
'
\\
'
,
'/'
)
suffix
=
normalized
.
split
(
'/app/uploads/data/'
,
1
)
if
len
(
suffix
)
==
2
and
suffix
[
1
]:
return
_with_legacy_fallback
(
Path
(
suffix
[
1
])
.
name
)
return
_with_legacy_fallback
(
Path
(
normalized
)
.
name
)
return
_with_legacy_fallback
(
stored_name
)
def
_read_value
(
self
,
row
:
list
[
str
],
index
:
int
)
->
str
:
if
index
<
len
(
row
):
return
row
[
index
]
.
strip
()
...
...
@@ -322,6 +359,8 @@ class DataManagementService:
return
self
.
_read_xlsx_rows
(
file_path
)
if
suffix
==
'.xls'
:
return
self
.
_read_xls_rows
(
file_path
)
if
suffix
==
'.csv'
:
return
self
.
_read_csv_rows
(
file_path
)
return
[]
def
_read_xlsx_rows
(
self
,
file_path
:
Path
)
->
list
[
list
[
str
]]:
...
...
@@ -347,6 +386,24 @@ class DataManagementService:
rows
.
append
(
cells
)
return
rows
def
_read_csv_rows
(
self
,
file_path
:
Path
)
->
list
[
list
[
str
]]:
encodings
=
[
'utf-8-sig'
,
'utf-8'
,
'gbk'
,
'gb2312'
]
for
encoding
in
encodings
:
try
:
with
file_path
.
open
(
'r'
,
encoding
=
encoding
,
newline
=
''
)
as
file
:
reader
=
csv
.
reader
(
file
)
rows
:
list
[
list
[
str
]]
=
[]
for
row
in
reader
:
cells
=
[
self
.
_cell_to_text
(
cell
)
for
cell
in
row
]
if
any
(
cell
!=
''
for
cell
in
cells
):
rows
.
append
(
cells
)
return
rows
except
UnicodeDecodeError
:
continue
raise
ValueError
(
'CSV 文件编码无法识别,请保存为 UTF-8 或 GBK 后重试'
)
def
_cell_to_text
(
self
,
value
:
Any
)
->
str
:
if
value
is
None
:
return
''
...
...
@@ -372,8 +429,8 @@ class DataManagementService:
workbook
=
Workbook
()
sheet
=
workbook
.
active
sheet
.
title
=
'数据模板'
sheet
.
append
([
'时间'
,
'电流'
,
'电压'
,
'温度'
])
sheet
.
append
([
'2026
-04-16 10:00:00'
,
1.2
,
220.5
,
36.8
])
sheet
.
append
([
'时间'
,
'电流'
,
'电压'
,
'
设定温度'
,
'实际
温度'
])
sheet
.
append
([
'2026
/1/22 16:38:01'
,
1.2
,
220.5
,
37.0
,
36.8
])
output
=
io
.
BytesIO
()
workbook
.
save
(
output
)
...
...
backend/uploads/data/温度2_20260416165018140191_19日15时38分33秒转换结果_回路76_-对应控点1.csv
0 → 100644
View file @
b599ac8f
This source diff could not be displayed because it is too large. You can
view the blob
instead.
frontend/package-lock.json
View file @
b599ac8f
...
...
@@ -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"
}
}
}
frontend/package.json
View file @
b599ac8f
...
...
@@ -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"
...
...
frontend/src/views/DataManagement/components/DataCurve.vue
View file @
b599ac8f
<
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
const
allValues
=
numericPoints
.
value
.
flatMap
((
item
)
=>
[
item
.
current
,
item
.
voltage
,
item
.
temperature
])
.
filter
((
value
)
=>
Number
.
isFinite
(
value
))
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
if
(
!
chartInstance
)
{
chartInstance
=
echarts
.
init
(
chartRef
.
value
)
}
const
x
=
padding
.
left
+
(
index
/
(
numericPoints
.
value
.
length
-
1
))
*
innerWidth
const
y
=
padding
.
top
+
((
maxValue
-
value
)
/
diff
)
*
innerHeight
const
hasData
=
chartData
.
value
.
length
>
0
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>
<canvas
ref=
"canvasRef"
class=
"curve-canvas"
/>
</div>
<div
ref=
"chartRef"
class=
"echarts-box"
/>
</div>
</
template
>
...
...
@@ -134,46 +232,38 @@ watch(
height
:
100%
;
display
:
flex
;
flex-direction
:
column
;
gap
:
10px
;
}
.legend-row
{
display
:
flex
;
gap
:
16px
;
color
:
#334155
;
font-size
:
13px
;
gap
:
14px
;
}
.
legend-item
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
6
px
;
.
stats-row
{
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
minmax
(
0
,
1fr
))
;
gap
:
12
px
;
}
.
dot
{
width
:
9
px
;
height
:
9
px
;
b
order-radius
:
50%
;
.
stat-card
{
padding
:
14px
16
px
;
border-radius
:
14
px
;
b
ackground
:
#f3f6fb
;
}
.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
:
28
0px
;
border-radius
:
1
2
px
;
border
:
1px
solid
rgba
(
15
,
23
,
42
,
0
.1
2
);
min-height
:
36
0px
;
border-radius
:
1
4
px
;
border
:
1px
solid
rgba
(
15
,
23
,
42
,
0
.1
);
background
:
linear-gradient
(
180deg
,
#ffffff
,
#f8fafc
);
}
</
style
>
frontend/src/views/DataManagement/index.vue
View file @
b599ac8f
<
script
setup
>
import
{
Delete
,
Edit
}
from
'@element-plus/icons-vue'
import
{
Delete
,
Edit
,
Folder
,
FolderOpened
,
Upload
}
from
'@element-plus/icons-vue'
import
{
computed
,
onBeforeUnmount
,
onMounted
,
reactive
,
ref
}
from
'vue'
import
{
ElMessage
,
ElMessageBox
}
from
'element-plus'
import
{
...
...
@@ -16,7 +16,7 @@ import {
import
DataCurve
from
'./components/DataCurve.vue'
const
categoryTree
=
ref
([])
const
selectedCategory
=
ref
(
''
)
const
selectedCategory
=
ref
(
'
all
'
)
const
searchForm
=
reactive
({
filename
:
''
,
...
...
@@ -37,10 +37,17 @@ const categoryForm = reactive({
name
:
''
,
})
const
uploadDialogVisible
=
ref
(
false
)
const
uploadForm
=
reactive
({
categoryId
:
''
,
})
const
layoutRef
=
ref
(
null
)
const
leftPaneWidth
=
ref
(
16
)
const
middlePaneWidth
=
ref
(
38
)
const
dragState
=
ref
(
null
)
const
pendingDragX
=
ref
(
null
)
let
dragFrameId
=
0
const
layoutStorageKey
=
'data-management-layout-v1'
const
MIN_LEFT
=
12
...
...
@@ -58,17 +65,33 @@ const categoryDialogTitle = computed(() => {
return
categoryDialogMode
.
value
===
'create'
?
'新增分类'
:
'编辑分类'
})
const
flatCategoryOptions
=
computed
(()
=>
{
const
root
=
categoryTree
.
value
.
find
((
item
)
=>
String
(
item
.
id
)
===
'all'
)
return
(
root
?.
children
||
[]).
map
((
item
)
=>
({
label
:
item
.
name
,
value
:
String
(
item
.
id
),
}))
})
const
normalizeCategoryTree
=
(
treeData
)
=>
{
const
source
=
Array
.
isArray
(
treeData
)
?
treeData
:
[]
const
oneLevel
=
source
.
length
===
1
&&
String
(
source
[
0
]?.
id
)
===
'all'
?
source
[
0
].
children
||
[]
:
source
const
oneLevel
=
source
.
filter
((
item
)
=>
String
(
item
?.
id
)
!==
'all'
)
return
oneLevel
.
map
((
item
)
=>
({
const
children
=
oneLevel
.
map
((
item
)
=>
({
id
:
item
.
id
,
name
:
item
.
name
,
parent_id
:
item
.
parent_id
,
children
:
[],
}))
return
[
{
id
:
'all'
,
name
:
'全部'
,
parent_id
:
null
,
children
,
},
]
}
const
setPaneWidths
=
(
nextLeft
,
nextMiddle
,
target
=
'left'
)
=>
{
...
...
@@ -121,11 +144,48 @@ const saveLayoutCache = () => {
)
}
const
applyDragPosition
=
(
clientX
)
=>
{
if
(
!
dragState
.
value
)
{
return
}
const
delta
=
((
clientX
-
dragState
.
value
.
startX
)
/
dragState
.
value
.
layoutWidth
)
*
100
if
(
dragState
.
value
.
target
===
'left'
)
{
setPaneWidths
(
dragState
.
value
.
startLeft
+
delta
,
dragState
.
value
.
startMiddle
,
'left'
)
}
else
{
setPaneWidths
(
dragState
.
value
.
startLeft
,
dragState
.
value
.
startMiddle
+
delta
,
'middle'
)
}
}
const
flushDragging
=
()
=>
{
dragFrameId
=
0
if
(
pendingDragX
.
value
===
null
)
{
return
}
applyDragPosition
(
pendingDragX
.
value
)
}
const
stopDragging
=
()
=>
{
document
.
removeEventListener
(
'mousemove'
,
handleDragging
)
document
.
removeEventListener
(
'mouseup'
,
stopDragging
)
dragState
.
value
=
null
window
.
removeEventListener
(
'mousemove'
,
handleDragging
)
window
.
removeEventListener
(
'mouseup'
,
stopDragging
)
window
.
removeEventListener
(
'mouseleave'
,
stopDragging
)
window
.
removeEventListener
(
'blur'
,
stopDragging
)
document
.
body
.
style
.
userSelect
=
''
document
.
body
.
style
.
cursor
=
''
if
(
dragFrameId
)
{
cancelAnimationFrame
(
dragFrameId
)
dragFrameId
=
0
}
pendingDragX
.
value
=
null
if
(
dragState
.
value
)
{
saveLayoutCache
()
}
dragState
.
value
=
null
}
const
handleDragging
=
(
event
)
=>
{
...
...
@@ -133,12 +193,10 @@ const handleDragging = (event) => {
return
}
const
delta
=
((
event
.
clientX
-
dragState
.
value
.
startX
)
/
dragState
.
value
.
layoutWidth
)
*
100
pendingDragX
.
value
=
event
.
clientX
if
(
dragState
.
value
.
target
===
'left'
)
{
setPaneWidths
(
dragState
.
value
.
startLeft
+
delta
,
dragState
.
value
.
startMiddle
,
'left'
)
}
else
{
setPaneWidths
(
dragState
.
value
.
startLeft
,
dragState
.
value
.
startMiddle
+
delta
,
'middle'
)
if
(
!
dragFrameId
)
{
dragFrameId
=
requestAnimationFrame
(
flushDragging
)
}
}
...
...
@@ -147,6 +205,9 @@ const startDragging = (target, event) => {
return
}
event
.
preventDefault
()
stopDragging
()
dragState
.
value
=
{
target
,
startX
:
event
.
clientX
,
...
...
@@ -155,16 +216,26 @@ const startDragging = (target, event) => {
layoutWidth
:
layoutRef
.
value
.
clientWidth
,
}
document
.
addEventListener
(
'mousemove'
,
handleDragging
)
document
.
addEventListener
(
'mouseup'
,
stopDragging
)
document
.
body
.
style
.
userSelect
=
'none'
document
.
body
.
style
.
cursor
=
'col-resize'
window
.
addEventListener
(
'mousemove'
,
handleDragging
)
window
.
addEventListener
(
'mouseup'
,
stopDragging
)
window
.
addEventListener
(
'mouseleave'
,
stopDragging
)
window
.
addEventListener
(
'blur'
,
stopDragging
)
}
const
loadCategoryTree
=
async
()
=>
{
const
treeData
=
await
getCategoryTree
()
categoryTree
.
value
=
normalizeCategoryTree
(
treeData
)
if
(
!
categoryTree
.
value
.
some
((
item
)
=>
item
.
id
===
selectedCategory
.
value
))
{
selectedCategory
.
value
=
''
const
allIds
=
[
...
categoryTree
.
value
.
map
((
item
)
=>
String
(
item
.
id
)),
...
categoryTree
.
value
.
flatMap
((
item
)
=>
(
item
.
children
||
[]).
map
((
child
)
=>
String
(
child
.
id
))),
]
if
(
!
allIds
.
includes
(
String
(
selectedCategory
.
value
)))
{
selectedCategory
.
value
=
'all'
}
}
...
...
@@ -172,7 +243,7 @@ const loadFileList = async () => {
loadingFiles
.
value
=
true
try
{
const
data
=
await
getDataFiles
({
category_id
:
selectedCategory
.
value
||
''
,
category_id
:
selectedCategory
.
value
&&
selectedCategory
.
value
!==
'all'
?
selectedCategory
.
value
:
''
,
filename
:
searchForm
.
filename
.
trim
(),
})
fileList
.
value
=
data
...
...
@@ -233,7 +304,7 @@ const handleDeleteCategory = async (node) => {
await
deleteCategory
(
node
.
id
)
ElMessage
.
success
(
'分类已删除'
)
if
(
selectedCategory
.
value
===
node
.
id
)
{
selectedCategory
.
value
=
''
selectedCategory
.
value
=
'
all
'
}
await
loadCategoryTree
()
await
loadFileList
()
...
...
@@ -251,22 +322,40 @@ const handleReset = async () => {
await
loadFileList
()
}
const
openUploadDialog
=
()
=>
{
uploadForm
.
categoryId
=
selectedCategory
.
value
&&
selectedCategory
.
value
!==
'all'
?
String
(
selectedCategory
.
value
)
:
''
uploadDialogVisible
.
value
=
true
}
const
beforeUploadExcel
=
(
file
)
=>
{
if
(
!
uploadForm
.
categoryId
)
{
ElMessage
.
warning
(
'请先在弹窗中选择分类'
)
return
false
}
const
lowerName
=
file
.
name
.
toLowerCase
()
const
valid
=
lowerName
.
endsWith
(
'.xlsx'
)
||
lowerName
.
endsWith
(
'.xls'
)
const
valid
=
lowerName
.
endsWith
(
'.xlsx'
)
||
lowerName
.
endsWith
(
'.xls'
)
||
lowerName
.
endsWith
(
'.csv'
)
if
(
!
valid
)
{
ElMessage
.
warning
(
'仅支持
Excel 文件(.xlsx/.xls
)'
)
ElMessage
.
warning
(
'仅支持
数据文件(.xlsx/.xls/.csv
)'
)
}
return
valid
}
const
handleUpload
=
async
(
uploadRequest
)
=>
{
if
(
!
uploadForm
.
categoryId
)
{
ElMessage
.
warning
(
'请先选择分类后再上传'
)
uploadRequest
.
onError
(
new
Error
(
'未选择分类'
))
return
}
const
formData
=
new
FormData
()
formData
.
append
(
'file'
,
uploadRequest
.
file
)
formData
.
append
(
'category_id'
,
selectedCategory
.
value
?
String
(
selectedCategory
.
value
)
:
''
)
formData
.
append
(
'category_id'
,
String
(
uploadForm
.
categoryId
)
)
try
{
await
uploadDataFile
(
formData
)
selectedCategory
.
value
=
String
(
uploadForm
.
categoryId
)
uploadDialogVisible
.
value
=
false
ElMessage
.
success
(
'上传成功'
)
await
loadFileList
()
uploadRequest
.
onSuccess
()
...
...
@@ -319,6 +408,7 @@ const handleDeleteFile = async (row) => {
onMounted
(
async
()
=>
{
loadLayoutCache
()
await
loadCategoryTree
()
selectedCategory
.
value
=
selectedCategory
.
value
||
'all'
await
loadFileList
()
})
...
...
@@ -328,7 +418,7 @@ onBeforeUnmount(() => {
</
script
>
<
template
>
<section
class=
"data-page"
>
<section
class=
"data-page"
:class=
"
{ dragging: !!dragState }"
>
<div
ref=
"layoutRef"
class=
"data-layout"
>
<el-card
class=
"pane-card"
shadow=
"hover"
:style=
"
{ width: `${leftPaneWidth}%` }">
<template
#
header
>
...
...
@@ -346,14 +436,20 @@ onBeforeUnmount(() => {
:data=
"categoryTree"
node-key=
"id"
highlight-current
default-expand-all
:current-node-key=
"selectedCategory || undefined"
:expand-on-click-node=
"false"
@
node-click=
"handleCategoryClick"
>
<
template
#
default=
"{ data }"
>
<div
class=
"tree-node"
>
<div
class=
"tree-main"
>
<el-icon
class=
"tree-icon"
>
<component
:is=
"data.id === 'all' ? FolderOpened : Folder"
/>
</el-icon>
<span
class=
"tree-name"
>
{{
data
.
name
}}
</span>
<span
class=
"tree-actions"
>
</div>
<span
v-if=
"data.id !== 'all'"
class=
"tree-actions"
>
<el-button
link
type=
"primary"
:icon=
"Edit"
@
click
.
stop=
"openEditCategoryDialog(data)"
/>
<el-button
link
type=
"danger"
:icon=
"Delete"
@
click
.
stop=
"handleDeleteCategory(data)"
/>
</span>
...
...
@@ -362,7 +458,7 @@ onBeforeUnmount(() => {
</el-tree>
</el-card>
<div
class=
"pane-divider"
@
mousedown=
"(event) => startDragging('left', event)"
/>
<div
class=
"pane-divider"
@
mousedown
.
prevent
=
"(event) => startDragging('left', event)"
/>
<el-card
class=
"pane-card"
shadow=
"hover"
:style=
"{ width: `${middlePaneWidth}%` }"
>
<
template
#
header
>
...
...
@@ -379,14 +475,7 @@ onBeforeUnmount(() => {
<el-button
type=
"primary"
@
click=
"handleSearch"
>
搜索
</el-button>
<el-button
@
click=
"handleReset"
>
重置
</el-button>
<el-button
type=
"info"
plain
@
click=
"handleDownloadTemplate"
>
下载模板
</el-button>
<el-upload
:show-file-list=
"false"
:http-request=
"handleUpload"
:before-upload=
"beforeUploadExcel"
accept=
".xlsx,.xls"
>
<el-button
type=
"success"
>
上传Excel
</el-button>
</el-upload>
<el-button
type=
"success"
:icon=
"Upload"
@
click=
"openUploadDialog"
>
上传文件
</el-button>
</el-form-item>
</el-form>
...
...
@@ -403,7 +492,7 @@ onBeforeUnmount(() => {
</el-table>
</el-card>
<div
class=
"pane-divider"
@
mousedown=
"(event) => startDragging('middle', event)"
/>
<div
class=
"pane-divider"
@
mousedown
.
prevent
=
"(event) => startDragging('middle', event)"
/>
<el-card
class=
"pane-card"
shadow=
"hover"
:style=
"{ width: `${rightPaneWidth}%` }"
>
<
template
#
header
>
...
...
@@ -432,7 +521,8 @@ onBeforeUnmount(() => {
<el-table-column
prop=
"time"
label=
"时间"
min-width=
"140"
/>
<el-table-column
prop=
"current"
label=
"电流"
min-width=
"100"
/>
<el-table-column
prop=
"voltage"
label=
"电压"
min-width=
"100"
/>
<el-table-column
prop=
"temperature"
label=
"温度"
min-width=
"100"
/>
<el-table-column
prop=
"set_temperature"
label=
"设定温度"
min-width=
"100"
/>
<el-table-column
prop=
"actual_temperature"
label=
"实际温度"
min-width=
"100"
/>
</el-table>
<DataCurve
v-else
:records=
"recordList"
/>
...
...
@@ -440,6 +530,42 @@ onBeforeUnmount(() => {
</el-card>
</div>
<el-dialog
v-model=
"uploadDialogVisible"
title=
"上传数据文件"
width=
"460px"
>
<el-form
label-position=
"top"
>
<el-form-item
label=
"选择分类"
required
>
<el-select
v-model=
"uploadForm.categoryId"
placeholder=
"请选择要上传到的分类"
style=
"width: 100%"
>
<el-option
v-for=
"item in flatCategoryOptions"
:key=
"item.value"
:label=
"item.label"
:value=
"item.value"
/>
</el-select>
</el-form-item>
<el-form-item
label=
"上传文件"
required
>
<el-upload
class=
"upload-panel"
drag
:show-file-list=
"false"
:http-request=
"handleUpload"
:before-upload=
"beforeUploadExcel"
accept=
".xlsx,.xls,.csv"
>
<el-icon
class=
"upload-panel__icon"
><Upload
/></el-icon>
<div
class=
"el-upload__text"
>
拖拽文件到此处,或点击选择 Excel / CSV 文件
</div>
<
template
#
tip
>
<div
class=
"el-upload__tip"
>
支持 .xlsx / .xls / .csv,上传前请先选择分类
</div>
</
template
>
</el-upload>
</el-form-item>
</el-form>
<
template
#
footer
>
<el-button
@
click=
"uploadDialogVisible = false"
>
关闭
</el-button>
</
template
>
</el-dialog>
<el-dialog
v-model=
"categoryDialogVisible"
:title=
"categoryDialogTitle"
width=
"420px"
>
<el-form
label-position=
"top"
>
<el-form-item
label=
"分类名称"
required
>
...
...
@@ -458,6 +584,11 @@ onBeforeUnmount(() => {
<
style
lang=
"scss"
scoped
>
.data-page
{
height
:
100%
;
&
.dragging
{
user-select
:
none
;
cursor
:
col-resize
;
}
}
.data-layout
{
...
...
@@ -468,19 +599,25 @@ onBeforeUnmount(() => {
}
.pane-divider
{
width
:
8
px
;
flex
:
0
0
8
px
;
width
:
12
px
;
flex
:
0
0
12
px
;
position
:
relative
;
cursor
:
col-resize
;
user-select
:
none
;
&
:
:
before
{
content
:
''
;
position
:
absolute
;
top
:
0
;
bottom
:
0
;
left
:
3
px
;
left
:
5
px
;
width
:
2px
;
background
:
rgba
(
148
,
163
,
184
,
0
.4
);
transition
:
background
0
.2s
ease
;
}
&
:hover::before
{
background
:
rgba
(
59
,
130
,
246
,
0
.7
);
}
}
...
...
@@ -519,12 +656,40 @@ onBeforeUnmount(() => {
margin-left
:
6px
;
}
:deep
(
.el-tree
)
{
--el-tree-node-hover-bg-color
:
#f8fbff
;
}
:deep
(
.el-tree-node__content
)
{
height
:
40px
;
border-radius
:
10px
;
margin-bottom
:
4px
;
}
:deep
(
.el-tree--highlight-current
.el-tree-node.is-current
>
.el-tree-node__content
)
{
background
:
linear-gradient
(
90deg
,
rgba
(
59
,
130
,
246
,
0
.14
)
,
rgba
(
14
,
165
,
233
,
0
.08
));
}
.tree-node
{
width
:
100%
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
10px
;
padding-right
:
4px
;
}
.tree-main
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
8px
;
min-width
:
0
;
}
.tree-icon
{
font-size
:
16px
;
color
:
#eab308
;
flex
:
0
0
auto
;
}
.tree-name
{
...
...
@@ -532,6 +697,21 @@ onBeforeUnmount(() => {
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
color
:
#1e293b
;
}
.tree-actions
{
opacity
:
0
.8
;
}
.upload-panel
{
width
:
100%
;
}
.upload-panel__icon
{
font-size
:
28px
;
color
:
#409eff
;
margin-bottom
:
8px
;
}
.search-form
{
...
...
sql/init_tables.sql
0 → 100644
View file @
b599ac8f
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
'文件路径'
;
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment