Commit 6ca52b5b authored by luwei's avatar luwei

Initial commit

parents
Pipeline #361 canceled with stages
# Node.js dependencies
node_modules/
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# OS generated files
.DS_Store
Thumbs.db
# Python dependencies
__pycache__/
*.py[cod]
*.pyo
*.pyd
venv/
# IDE settings
.vscode/
.idea/
*.sublime-workspace
# Environment variables
.env
# Build outputs
dist/
build/
*.pyc
# Database
*.sqlite3
*.db
# Miscellaneous
*.bak
*.swp
*.tmp
*.temp
from fastapi import APIRouter, File, Form, HTTPException, Query, Response, UploadFile
from pydantic import BaseModel, Field
from app.services.data_management_service import DataManagementService
from app.utils.response import success_response
router = APIRouter()
service = DataManagementService()
class CategoryCreateRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
parent_id: str | int | None = Field(default='all')
class CategoryUpdateRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
@router.get('/categories')
def get_categories():
return success_response(data=service.get_category_tree())
@router.post('/categories')
def create_category(request: CategoryCreateRequest):
try:
parent_id = 'all' if request.parent_id is None else str(request.parent_id)
category = service.create_category(name=request.name.strip(), parent_id=parent_id)
return success_response(data=category, message='分类创建成功')
except ValueError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
@router.put('/categories/{category_id}')
def update_category(category_id: str, request: CategoryUpdateRequest):
try:
category = service.update_category(category_id=category_id, name=request.name.strip())
return success_response(data=category, message='分类更新成功')
except ValueError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
@router.delete('/categories/{category_id}')
def delete_category(category_id: str):
try:
service.delete_category(category_id=category_id)
return success_response(data=True, message='分类删除成功')
except ValueError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
@router.get('/files')
def get_files(
category_id: str = Query(default=''),
filename: str = Query(default=''),
):
return success_response(data=service.list_files(category_id=category_id, filename=filename.strip()))
@router.post('/files/upload')
async def upload_file(
file: UploadFile = File(...),
category_id: str = Form(default=''),
):
try:
content = await file.read()
if not content:
raise ValueError('上传文件为空')
file_meta = service.upload_file(
filename=file.filename or 'unknown.xlsx',
content=content,
category_id=category_id,
)
return success_response(data=file_meta, message='文件上传成功')
except ValueError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
@router.get('/files/template')
def download_template():
content = service.get_excel_template_bytes()
return Response(
content=content,
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers={
'Content-Disposition': 'attachment; filename=data_template.xlsx',
},
)
@router.delete('/files/{file_id}')
def delete_file(file_id: str):
try:
service.delete_file(file_id=file_id)
return success_response(data=True, message='文件删除成功')
except ValueError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
@router.get('/files/{file_id}/records')
def get_file_records(file_id: str, limit: int = Query(default=500, ge=1, le=5000)):
try:
result = service.get_file_records(file_id=file_id, limit=limit)
return success_response(data=result)
except ValueError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
import os
from dataclasses import dataclass
def _read_env(name: str, default: str) -> str:
return os.getenv(name, default).strip()
@dataclass(frozen=True)
class Settings:
mysql_host: str = _read_env('MYSQL_HOST', '39.107.252.11')
mysql_port: int = int(_read_env('MYSQL_PORT', '3366'))
mysql_user: str = _read_env('MYSQL_USER', 'root')
mysql_password: str = _read_env('MYSQL_PASSWORD', 'RCX8WRu07M')
mysql_database: str = _read_env('MYSQL_DATABASE', 'thermal_control_system')
settings = Settings()
\ No newline at end of file
from contextlib import contextmanager
from typing import Generator
from sqlalchemy import create_engine, text
from sqlalchemy.engine import URL
from sqlalchemy.orm import Session, declarative_base, sessionmaker
from app.config import settings
Base = declarative_base()
def _build_mysql_url(database: str | None = None) -> URL:
return URL.create(
drivername='mysql+pymysql',
username=settings.mysql_user,
password=settings.mysql_password,
host=settings.mysql_host,
port=settings.mysql_port,
database=database,
)
engine = create_engine(
_build_mysql_url(settings.mysql_database),
pool_pre_ping=True,
pool_recycle=3600,
)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, class_=Session)
@contextmanager
def db_session() -> Generator[Session, None, None]:
session = SessionLocal()
try:
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
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
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
def create_app() -> FastAPI:
app = FastAPI(
title='Thermal Control System API',
version='0.1.0',
)
app.add_middleware(
CORSMiddleware,
allow_origins=['*'],
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
)
@app.exception_handler(HTTPException)
async def http_exception_handler(_, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content=error_response(message=str(exc.detail), code=exc.status_code),
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(_, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content=error_response(message='参数校验失败', code=422, data=exc.errors()),
)
@app.exception_handler(Exception)
async def unhandled_exception_handler(_, exc: Exception):
return JSONResponse(
status_code=500,
content=error_response(message=f'服务器异常: {exc}', code=500),
)
@app.get('/api/health')
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
app = create_app()
\ No newline at end of file
from app.models.data_management import Category, DataFile
__all__ = ['Category', 'DataFile']
from sqlalchemy import BIGINT, TIMESTAMP, Enum, Index, Integer, String, Text, text
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class Category(Base):
__tablename__ = 'categories'
__table_args__ = (
Index('idx_type', 'type'),
Index('idx_parent', 'parent_id'),
{'mysql_comment': '数据分类表'},
)
id: Mapped[int] = mapped_column(BIGINT, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(100), nullable=False, comment='分类名称')
type: Mapped[str] = mapped_column(
Enum('data_file', 'data_package', name='category_type_enum'),
nullable=False,
comment='分类类型',
)
parent_id: Mapped[int | None] = mapped_column(BIGINT, nullable=True, comment='父分类ID')
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text('0'), comment='排序')
created_at: Mapped[str] = mapped_column(TIMESTAMP, nullable=False, server_default=text('CURRENT_TIMESTAMP'))
updated_at: Mapped[str] = mapped_column(
TIMESTAMP,
nullable=False,
server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
)
class DataFile(Base):
__tablename__ = 'data_files'
__table_args__ = (
Index('idx_file_category', 'category_id'),
{'mysql_comment': '数据文件表'},
)
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='存储文件名')
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'))
remark: Mapped[str | None] = mapped_column(Text, nullable=True)
This diff is collapsed.
from typing import Any
def success_response(data: Any = None, message: str = 'success', code: int = 0) -> dict[str, Any]:
return {
'code': code,
'message': message,
'data': data,
}
def error_response(message: str = 'error', code: int = 1, data: Any = None) -> dict[str, Any]:
return {
'code': code,
'message': message,
'data': data,
}
fastapi==0.116.1
uvicorn[standard]==0.35.0
python-multipart==0.0.20
pydantic==2.11.7
SQLAlchemy==2.0.43
PyMySQL==1.1.2
openpyxl==3.1.5
xlrd==2.0.1
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/
# Vite
*.timestamp-*-*.mjs
# frontend
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<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>
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "frontend",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"pinia": "^3.0.4",
"vue": "^3.5.31",
"vue-router": "^5.0.4"
},
"devDependencies": {
"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"
},
"engines": {
"sass": "^1.93.2",
"node": "^20.19.0 || >=22.12.0"
}
}
<script setup>
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const tabs = [
{ label: '数据管理', path: '/data-management' },
{ label: '实时监控', path: '/realtime-monitor' },
{ label: '历史数据', path: '/history-data' },
{ label: '模型训练', path: '/model-training' },
{ label: '模型列表', path: '/model-list' },
{ label: '模型评估', path: '/model-evaluation' },
{ label: '数据包管理', path: '/package-management' },
]
const activeTab = computed(() => {
const matched = tabs.find((item) => route.path.startsWith(item.path))
return matched?.path ?? '/data-management'
})
const handleTabChange = (path) => {
router.push(path)
}
</script>
<template>
<div class="app-shell">
<header class="top-header">
<div class="project-title">热实验温度控制系统</div>
<el-tabs class="top-tabs" :model-value="activeTab" @tab-change="handleTabChange">
<el-tab-pane
v-for="tab in tabs"
:key="tab.path"
:label="tab.label"
:name="tab.path"
/>
</el-tabs>
</header>
<main class="app-main">
<router-view />
</main>
</div>
</template>
<style lang="scss" scoped>
.app-shell {
min-height: 100vh;
background:
radial-gradient(circle at 8% 12%, rgba(14, 165, 233, 0.22), transparent 38%),
radial-gradient(circle at 96% 8%, rgba(34, 197, 94, 0.2), transparent 36%),
#f2f8fb;
}
.top-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
height: 72px;
padding: 0 20px;
backdrop-filter: blur(8px);
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
background: rgba(255, 255, 255, 0.84);
position: sticky;
top: 0;
z-index: 20;
}
.project-title {
flex: 0 0 auto;
font-size: 22px;
letter-spacing: 1px;
font-weight: 700;
color: #0f172a;
white-space: nowrap;
}
.top-tabs {
flex: 1;
min-width: 0;
:deep(.el-tabs__header) {
margin-bottom: 0;
}
:deep(.el-tabs__nav-wrap::after) {
display: none;
}
:deep(.el-tabs__item) {
color: #334155;
font-weight: 500;
}
:deep(.el-tabs__item.is-active) {
color: #0f766e;
}
:deep(.el-tabs__active-bar) {
background-color: #0f766e;
height: 3px;
border-radius: 999px;
}
}
.app-main {
height: calc(100vh - 72px);
padding: 16px 20px 20px;
}
@media (max-width: 980px) {
.top-header {
flex-direction: column;
align-items: flex-start;
height: auto;
padding: 12px;
}
.project-title {
font-size: 18px;
}
.top-tabs {
width: 100%;
}
.app-main {
height: auto;
min-height: calc(100vh - 120px);
padding: 12px;
}
}
</style>
import request from '@/utils/request'
export function getCategoryTree() {
return request.get('/data/categories')
}
export function createCategory(payload) {
return request.post('/data/categories', payload)
}
export function updateCategory(categoryId, payload) {
return request.put(`/data/categories/${categoryId}`, payload)
}
export function deleteCategory(categoryId) {
return request.delete(`/data/categories/${categoryId}`)
}
export function getDataFiles(params) {
return request.get('/data/files', { params })
}
export function uploadDataFile(formData) {
return request.post('/data/files/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
}
export function deleteDataFile(fileId) {
return request.delete(`/data/files/${fileId}`)
}
export function getFileRecords(fileId, params) {
return request.get(`/data/files/${fileId}/records`, { params })
}
export function downloadDataTemplate() {
return request.get('/data/files/template', {
responseType: 'blob',
})
}
<script setup>
defineProps({
title: {
type: String,
default: '页面建设中',
},
})
</script>
<template>
<section class="uc-page">
<div class="uc-card">
<h2>{{ title }}</h2>
<p>该模块正在建设中,可先使用“数据管理”完成数据上传与查看。</p>
</div>
</section>
</template>
<style lang="scss" scoped>
.uc-page {
height: 100%;
display: grid;
place-items: center;
}
.uc-card {
width: min(640px, 100%);
border-radius: 18px;
padding: 38px 30px;
background: linear-gradient(135deg, #ffffff, #f2fbf8);
border: 1px solid rgba(15, 118, 110, 0.2);
box-shadow: 0 12px 30px rgba(2, 8, 23, 0.1);
h2 {
margin: 0 0 10px;
font-size: 28px;
color: #0f172a;
}
p {
margin: 0;
color: #334155;
font-size: 15px;
}
}
</style>
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import './styles/global.scss'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
redirect: '/data-management',
},
{
path: '/data-management',
name: 'data-management',
component: () => import('@/views/DataManagement/index.vue'),
},
{
path: '/realtime-monitor',
name: 'realtime-monitor',
component: () => import('@/components/common/UnderConstruction.vue'),
props: { title: '实时监控' },
},
{
path: '/history-data',
name: 'history-data',
component: () => import('@/components/common/UnderConstruction.vue'),
props: { title: '历史数据' },
},
{
path: '/model-training',
name: 'model-training',
component: () => import('@/components/common/UnderConstruction.vue'),
props: { title: '模型训练' },
},
{
path: '/model-list',
name: 'model-list',
component: () => import('@/components/common/UnderConstruction.vue'),
props: { title: '模型列表' },
},
{
path: '/model-evaluation',
name: 'model-evaluation',
component: () => import('@/components/common/UnderConstruction.vue'),
props: { title: '模型评估' },
},
{
path: '/package-management',
name: 'package-management',
component: () => import('@/components/common/UnderConstruction.vue'),
props: { title: '数据包管理' },
},
],
})
export default router
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@500;600;700&family=Noto+Sans+SC:wght@400;500;700&display=swap');
:root {
--page-text: #0f172a;
--page-muted: #64748b;
--brand: #0f766e;
--brand-soft: #14b8a6;
}
* {
box-sizing: border-box;
}
html,
body,
#app {
margin: 0;
padding: 0;
width: 100%;
min-height: 100%;
}
body {
font-family: 'Poppins', 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
color: var(--page-text);
}
import axios from 'axios'
import { ElMessage } from 'element-plus'
const request = axios.create({
baseURL: '/api',
timeout: 20000,
})
request.interceptors.response.use(
(response) => {
if (response.config.responseType === 'blob') {
return response.data
}
const payload = response.data
if (typeof payload?.code !== 'undefined') {
if (payload.code === 0) {
return payload.data
}
ElMessage.error(payload.message || '请求失败')
return Promise.reject(new Error(payload.message || '请求失败'))
}
return payload
},
(error) => {
const message = error?.response?.data?.message || error.message || '网络异常'
ElMessage.error(message)
return Promise.reject(error)
},
)
export default request
\ No newline at end of file
<script setup>
import { computed, nextTick, onMounted, ref, watch } from 'vue'
const props = defineProps({
records: {
type: Array,
default: () => [],
},
})
const canvasRef = ref(null)
const numericPoints = computed(() => {
return props.records
.map((item, idx) => ({
x: idx,
current: Number(item.current),
voltage: Number(item.voltage),
temperature: Number(item.temperature),
}))
.filter((item) => {
return (
Number.isFinite(item.current) ||
Number.isFinite(item.voltage) ||
Number.isFinite(item.temperature)
)
})
})
const draw = () => {
const canvas = canvasRef.value
if (!canvas) {
return
}
const dpr = window.devicePixelRatio || 1
const width = canvas.clientWidth
const height = canvas.clientHeight
canvas.width = width * dpr
canvas.height = height * dpr
const ctx = canvas.getContext('2d')
ctx.scale(dpr, dpr)
ctx.clearRect(0, 0, width, height)
if (numericPoints.value.length < 2) {
ctx.fillStyle = '#64748b'
ctx.font = '14px Poppins, Noto Sans SC, sans-serif'
ctx.fillText('暂无足够数据生成曲线', 20, height / 2)
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
}
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)
}
})
if (started) {
ctx.strokeStyle = color
ctx.lineWidth = 2
ctx.stroke()
}
}
drawLine('current', '#0ea5e9')
drawLine('voltage', '#f97316')
drawLine('temperature', '#ef4444')
}
onMounted(async () => {
await nextTick()
draw()
})
watch(
() => props.records,
async () => {
await nextTick()
draw()
},
{ deep: true },
)
</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>
<canvas ref="canvasRef" class="curve-canvas" />
</div>
</template>
<style lang="scss" scoped>
.curve-box {
height: 100%;
display: flex;
flex-direction: column;
gap: 10px;
}
.legend-row {
display: flex;
gap: 16px;
color: #334155;
font-size: 13px;
}
.legend-item {
display: inline-flex;
align-items: center;
gap: 6px;
}
.dot {
width: 9px;
height: 9px;
border-radius: 50%;
}
.dot-current {
background: #0ea5e9;
}
.dot-voltage {
background: #f97316;
}
.dot-temperature {
background: #ef4444;
}
.curve-canvas {
width: 100%;
flex: 1;
min-height: 280px;
border-radius: 12px;
border: 1px solid rgba(15, 23, 42, 0.12);
background: linear-gradient(180deg, #ffffff, #f8fafc);
}
</style>
This diff is collapsed.
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
},
},
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})
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