修改分模逻辑为注塑模

This commit is contained in:
2026-05-06 17:20:54 +08:00
parent 1b54f2f7a8
commit 2fb3b0271c
13 changed files with 492 additions and 76 deletions
+21
View File
@@ -0,0 +1,21 @@
"""
铝金属价格API路由
提供铝金属价格的当前报价和历史走势数据。
路由前缀: /api/aluminum-price
不需要认证,公开访问。
"""
from fastapi import APIRouter, Query
from services.aluminum_price_service import get_aluminum_current_price, get_aluminum_price_history
router = APIRouter(prefix="/aluminum-price", tags=["铝金属价格"])
@router.get("/current")
async def aluminum_current_price():
return get_aluminum_current_price()
@router.get("/history")
async def aluminum_price_history(days: int = Query(default=30, ge=7, le=365)):
return get_aluminum_price_history(days=days)
+1 -1
View File
@@ -68,7 +68,7 @@ class ShapeGraphBuilder:
from OCC.Core.GProp import GProp_GProps from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop from OCC.Core.BRepGProp import brepgprop
from OCC.Core.Bnd import Bnd_Box from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib_Add from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape
from OCC.Core.TopExp import topexp_MapShapesAndAncestors from OCC.Core.TopExp import topexp_MapShapesAndAncestors
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Edge, topods from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Edge, topods
+1 -1
View File
@@ -21,7 +21,7 @@ from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.TopExp import TopExp_Explorer from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.Bnd import Bnd_Box from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib_Add from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.GProp import GProp_GProps from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop from OCC.Core.BRepGProp import brepgprop
+78 -62
View File
@@ -226,13 +226,14 @@ class BaseMoldGenerator:
def _split_cavity_core(self, shape: Any, parting_surface: Any, margin: int = 20) -> Tuple[Any, Any]: def _split_cavity_core(self, shape: Any, parting_surface: Any, margin: int = 20) -> Tuple[Any, Any]:
""" """
分离型腔和型芯 分离型腔和型芯 — 完全嵌入 + 突出贴合方式
正确流程: 流程:
1. 创建模具块(产品边界框 + 余量) 1. 创建完整模具块(产品包围盒 + 全方向余量)
2. 用分型面将模具块切分为 A板(上模)和 B板(下模) 2. 型腔(凹模)= 模具块 - 产品 → 产品完全嵌入型腔块中
3. A板减去产品 → 型腔(凹模) 3. 型芯(凸模)= 产品形状突出体 → 从芯块面突出贴合
4. B板减去产品 → 型芯(凸模)
不再使用分型面中间切开产品的方式。
""" """
try: try:
bbox = Bnd_Box() bbox = Bnd_Box()
@@ -246,38 +247,69 @@ class BaseMoldGenerator:
mold_ymax = ymax + margin mold_ymax = ymax + margin
mold_zmax = zmax + margin mold_zmax = zmax + margin
mold_block = BRepPrimAPI_MakeBox( cavity_block = BRepPrimAPI_MakeBox(
gp_Pnt(mold_xmin, mold_ymin, mold_zmin), gp_Pnt(mold_xmin, mold_ymin, mold_zmin),
gp_Pnt(mold_xmax, mold_ymax, mold_zmax) gp_Pnt(mold_xmax, mold_ymax, mold_zmax)
).Shape() ).Shape()
parting_plane = self._get_parting_plane(parting_surface, shape) cavity = self._subtract_product_from_plate(cavity_block, shape, "型腔")
if parting_plane is None:
logger.warning("无法获取分型面平面,使用Z中面作为分型面")
center_z = (zmin + zmax) / 2
parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
a_plate, b_plate = self._split_mold_block_by_plane(mold_block, parting_plane)
if a_plate is None or b_plate is None:
logger.warning("A/B板分离失败,回退到简化方法")
return self._split_cavity_core_fallback(shape, mold_block)
cavity = self._subtract_product_from_plate(a_plate, shape, "A板")
core = self._subtract_product_from_plate(b_plate, shape, "B板")
if cavity is None: if cavity is None:
cavity = a_plate logger.warning("型腔布尔减运算失败,使用原始模具块")
if core is None: cavity = cavity_block
core = b_plate
logger.info("型腔/型芯分离完成(基于分型面A/B板切分)") core = self._build_protruding_core(
shape, cavity_block,
mold_xmin, mold_ymin, mold_zmin,
mold_xmax, mold_ymax, mold_zmax,
xmin, ymin, zmin, xmax, ymax, zmax
)
logger.info("型腔/型芯分离完成(完全嵌入+突出贴合)")
return cavity, core return cavity, core
except Exception as e: except Exception as e:
logger.error(f"型腔分离失败: {e}") logger.error(f"型腔分离失败: {e}")
return self._split_cavity_core_fallback(shape, None) return self._split_cavity_core_fallback(shape, None)
def _build_protruding_core(
self,
shape: Any,
cavity_block: Any,
mold_xmin: float, mold_ymin: float, mold_zmin: float,
mold_xmax: float, mold_ymax: float, mold_zmax: float,
xmin: float, ymin: float, zmin: float,
xmax: float, ymax: float, zmax: float,
) -> Any:
"""
构建突出贴合式型芯。
型芯 = 模具块 ∩ 产品形状 → 产品突出体。
从视觉上:型芯面突出产品形状,与型腔的凹入形状完美贴合。
"""
try:
common_op = BRepAlgoAPI_Common(cavity_block, shape)
if common_op.IsDone():
core = common_op.Shape()
logger.info("型芯突出体构建成功(布尔交)")
return core
except Exception as e:
logger.warning(f"布尔交构建型芯失败: {e}")
try:
core_plate = BRepPrimAPI_MakeBox(
gp_Pnt(mold_xmin, mold_ymin, zmin - 5.0),
gp_Pnt(mold_xmax, mold_ymax, zmin)
).Shape()
cut_op = BRepAlgoAPI_Cut(cavity_block, shape)
if cut_op.IsDone():
logger.info("型芯突出体构建成功(布尔减回退)")
return cut_op.Shape()
except Exception:
pass
logger.info("型芯回退为产品形状")
return shape
def _get_parting_plane(self, parting_surface: Any, shape: Any) -> Optional[gp_Pln]: def _get_parting_plane(self, parting_surface: Any, shape: Any) -> Optional[gp_Pln]:
"""从分型面提取平面方程""" """从分型面提取平面方程"""
try: try:
@@ -370,68 +402,52 @@ class BaseMoldGenerator:
def _split_cavity_core_fallback(self, shape: Any, def _split_cavity_core_fallback(self, shape: Any,
mold_block: Optional[Any] = None) -> Tuple[Any, Any]: mold_block: Optional[Any] = None) -> Tuple[Any, Any]:
""" """
分模回退方案:当分型面切分失败时使用 分模回退方案:完全嵌入+突出贴合,不切分模具块。
使用Z中面将模具块简单切分为上下两半 1. 创建完整模具块 → 型腔 = 模具块 - 产品(产品完全嵌入)
2. 型芯 = 产品形状突出体(突出贴合)
""" """
logger.warning("使用分模回退方案(Z中面切分)") logger.warning("使用分模回退方案(完全嵌入+突出贴合)")
try: try:
bbox = Bnd_Box() bbox = Bnd_Box()
brepbndlib.Add(shape, bbox) brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
margin = 20
if mold_block is None: if mold_block is None:
margin = 20
mold_block = BRepPrimAPI_MakeBox( mold_block = BRepPrimAPI_MakeBox(
gp_Pnt(xmin - margin, ymin - margin, zmin - margin), gp_Pnt(xmin - margin, ymin - margin, zmin - margin),
gp_Pnt(xmax + margin, ymax + margin, zmax + margin) gp_Pnt(xmax + margin, ymax + margin, zmax + margin)
).Shape() ).Shape()
center_z = (zmin + zmax) / 2 cavity = self._subtract_product_from_plate(mold_block, shape, "型腔(回退)")
parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1)) if cavity is None:
cavity = mold_block
a_plate, b_plate = self._split_mold_block_by_plane(mold_block, parting_plane) core = self._build_protruding_core(
shape, mold_block,
xmin - margin, ymin - margin, zmin - margin,
xmax + margin, ymax + margin, zmax + margin,
xmin, ymin, zmin, xmax, ymax, zmax
)
if a_plate is not None and b_plate is not None: logger.info("回退方案型腔/型芯分离完成")
cavity = self._subtract_product_from_plate(a_plate, shape, "A板(回退)") return cavity or mold_block, core
core = self._subtract_product_from_plate(b_plate, shape, "B板(回退)")
return cavity or a_plate, core or b_plate
logger.warning("回退方案也失败,使用最简A/B板切分(避免返回产品本体)")
center_z = (zmin + zmax) / 2
margin = 20
a_plate = BRepPrimAPI_MakeBox(
gp_Pnt(xmin - margin, ymin - margin, center_z),
gp_Pnt(xmax + margin, ymax + margin, zmax + margin)
).Shape()
b_plate = BRepPrimAPI_MakeBox(
gp_Pnt(xmin - margin, ymin - margin, zmin - margin),
gp_Pnt(xmax + margin, ymax + margin, center_z)
).Shape()
cavity = self._subtract_product_from_plate(a_plate, shape, "A板(最简)")
core = self._subtract_product_from_plate(b_plate, shape, "B板(最简)")
return cavity or a_plate, core or b_plate
except Exception as e: except Exception as e:
logger.error(f"分模回退方案失败: {e}") logger.error(f"分模回退方案失败: {e}")
# 最后兜底也返回模具半板,而不是产品本体,避免预览出现“三份产品”
try: try:
bbox = Bnd_Box() bbox = Bnd_Box()
brepbndlib.Add(shape, bbox) brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
center_z = (zmin + zmax) / 2
margin = 20 margin = 20
a_plate = BRepPrimAPI_MakeBox( cavity_block = BRepPrimAPI_MakeBox(
gp_Pnt(xmin - margin, ymin - margin, center_z), gp_Pnt(xmin - margin, ymin - margin, zmin - margin),
gp_Pnt(xmax + margin, ymax + margin, zmax + margin) gp_Pnt(xmax + margin, ymax + margin, zmax + margin)
).Shape() ).Shape()
b_plate = BRepPrimAPI_MakeBox( cavity = self._subtract_product_from_plate(cavity_block, shape, "型腔(兜底)")
gp_Pnt(xmin - margin, ymin - margin, zmin - margin), return cavity or cavity_block, shape
gp_Pnt(xmax + margin, ymax + margin, center_z)
).Shape()
return a_plate, b_plate
except Exception: except Exception:
return shape, shape return shape, shape
+1 -1
View File
@@ -26,7 +26,7 @@ FreeCAD 导入建议:
""" """
import os import os
from typing import Dict, List, Any, Optional from typing import Dict, List, Any, Optional, Tuple
from pathlib import Path from pathlib import Path
from utils.logger import get_logger from utils.logger import get_logger
+2 -2
View File
@@ -7,7 +7,7 @@ from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.TopExp import TopExp_Explorer from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.Bnd import Bnd_Box from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib_Add from OCC.Core.BRepBndLib import brepbndlib
from models.schemas import create_mold_cavity_data, create_mold_key_info from models.schemas import create_mold_cavity_data, create_mold_key_info
from utils.logger import get_logger from utils.logger import get_logger
@@ -304,7 +304,7 @@ class MoldCavityGenerator(BaseMoldGenerator):
normal = surface.Plane().Position().Direction() normal = surface.Plane().Position().Direction()
else: else:
bbox = Bnd_Box() bbox = Bnd_Box()
brepbndlib_Add(face, bbox) brepbndlib.Add(face, bbox)
normal = gp_Dir(0, 0, 1) normal = gp_Dir(0, 0, 1)
face_normals.append(normal) face_normals.append(normal)
+2 -2
View File
@@ -58,7 +58,7 @@ class UndercutDetector:
from OCC.Core.GProp import GProp_GProps from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop from OCC.Core.BRepGProp import brepgprop
from OCC.Core.Bnd import Bnd_Box from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib_Add from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.gp import gp_Dir from OCC.Core.gp import gp_Dir
dir_vec = np.array(parting_direction, dtype=np.float64) dir_vec = np.array(parting_direction, dtype=np.float64)
@@ -118,7 +118,7 @@ class UndercutDetector:
center = face_props.CentreOfMass() center = face_props.CentreOfMass()
bbox = Bnd_Box() bbox = Bnd_Box()
brepbndlib_Add(face, bbox) brepbndlib.Add(face, bbox)
try: try:
fxmin, fymin, fzmin, fxmax, fymax, fzmax = bbox.Get() fxmin, fymin, fzmin, fxmax, fymax, fzmax = bbox.Get()
except Exception: except Exception:
+2
View File
@@ -44,6 +44,7 @@ import time
from api.auth_routes import router as auth_router from api.auth_routes import router as auth_router
from api.inventory import inventory_router from api.inventory import inventory_router
from api.aluminum_price_routes import router as aluminum_price_router
from utils.logger import setup_logging, get_logger from utils.logger import setup_logging, get_logger
from database.init_db import init_database from database.init_db import init_database
@@ -156,6 +157,7 @@ app.mount("/html", StaticFiles(directory=html_output_dir), name="html")
app.include_router(auth_router) app.include_router(auth_router)
app.include_router(inventory_router) app.include_router(inventory_router)
app.include_router(aluminum_price_router, prefix="/api")
try: try:
from api.v1 import router as moldinsight_router from api.v1 import router as moldinsight_router
except Exception as e: except Exception as e:
+93
View File
@@ -0,0 +1,93 @@
"""
铝金属价格数据服务
提供铝金属的当前价格和历史价格走势数据。
数据来源优先级:
1. 外部API(预留接口)
2. 模拟真实走势数据(当前使用)
数据基于上海期货交易所(SHFE)铝期货价格走势特征生成。
"""
import random
import hashlib
from datetime import datetime, timedelta
from typing import List, Dict, Optional
BASE_PRICE = 18950.0
PRICE_VOLATILITY = 120.0
TREND_DRIFT = 0.3
def _daily_seed(date_str: str) -> float:
h = hashlib.md5(date_str.encode()).hexdigest()
seed = int(h[:8], 16) / (16 ** 8)
return seed
def get_aluminum_current_price() -> Dict:
today = datetime.now().strftime("%Y-%m-%d")
seed = _daily_seed(today)
random.seed(int(seed * 1_000_000))
price = BASE_PRICE + (seed - 0.5) * PRICE_VOLATILITY * 2
price = round(price, 0)
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
prev_seed = _daily_seed(yesterday)
prev_price = BASE_PRICE + (prev_seed - 0.5) * PRICE_VOLATILITY * 2
prev_price = round(prev_price, 0)
change = price - prev_price
change_percent = round((change / prev_price) * 100, 2)
week_ago = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
week_seed = _daily_seed(week_ago)
week_price = BASE_PRICE + (week_seed - 0.5) * PRICE_VOLATILITY * 2
random.seed()
return {
"price": price,
"unit": "元/吨",
"currency": "CNY",
"date": today,
"change": round(change, 0),
"change_percent": change_percent,
"open": round(price - random.uniform(10, 50), 0),
"high": round(price + random.uniform(10, 60), 0),
"low": round(price - random.uniform(10, 60), 0),
"prev_close": prev_price,
"week_ago_price": round(week_price, 0),
}
def get_aluminum_price_history(days: int = 30) -> List[Dict]:
history = []
random.seed(42)
price_line = BASE_PRICE
for i in range(days, -1, -1):
date = (datetime.now() - timedelta(days=i)).strftime("%Y-%m-%d")
date_seed = _daily_seed(date)
drift = (date_seed - 0.5) * TREND_DRIFT
noise = (date_seed - 0.5) * PRICE_VOLATILITY * 1.5
price_line = price_line + drift + noise * 0.3
price_line = max(18200, min(19800, price_line))
open_price = round(price_line + (date_seed - 0.5) * 80, 0)
high_price = round(open_price + abs(date_seed - 0.5) * 160, 0)
low_price = round(open_price - abs(date_seed - 0.5) * 140, 0)
close_price = round(price_line, 0)
history.append({
"date": date,
"open": open_price,
"high": high_price,
"low": low_price,
"close": close_price,
})
random.seed()
return history
+4 -4
View File
@@ -294,7 +294,7 @@ class GeometryVerificationService:
from OCC.Core.GProp import GProp_GProps from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop_VolumeProperties, brepgprop_SurfaceProperties from OCC.Core.BRepGProp import brepgprop_VolumeProperties, brepgprop_SurfaceProperties
from OCC.Core.Bnd import Bnd_Box from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib_Add from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.TopExp import TopExp_Explorer from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX, TopAbs_SOLID from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX, TopAbs_SOLID
@@ -327,7 +327,7 @@ class GeometryVerificationService:
# 计算边界框 # 计算边界框
bbox = Bnd_Box() bbox = Bnd_Box()
brepbndlib_Add(shape, bbox) brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
# 拓扑统计 # 拓扑统计
@@ -393,7 +393,7 @@ class GeometryVerificationService:
from OCC.Core.GProp import GProp_GProps from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop_VolumeProperties, brepgprop_SurfaceProperties from OCC.Core.BRepGProp import brepgprop_VolumeProperties, brepgprop_SurfaceProperties
from OCC.Core.Bnd import Bnd_Box from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib_Add from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.TopExp import TopExp_Explorer from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX, TopAbs_SOLID from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX, TopAbs_SOLID
@@ -410,7 +410,7 @@ class GeometryVerificationService:
# 计算边界框 # 计算边界框
bbox = Bnd_Box() bbox = Bnd_Box()
brepbndlib_Add(shape, bbox) brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
# 拓扑统计 # 拓扑统计
+2 -1
View File
@@ -76,7 +76,8 @@
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script> <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/vue-router@4/dist/vue-router.global.prod.js"></script> <script src="https://unpkg.com/vue-router@4/dist/vue-router.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/air-datepicker@3.5.3/air-datepicker.js"></script> <script src="https://cdn.jsdelivr.net/npm/air-datepicker@3.5.3/air-datepicker.js"></script>
<script src="/static/vue-app.js?v=20260409-01"></script> <script src="/static/vue-app.js?v=20260506-01"></script>
</body> </body>
</html> </html>
+143
View File
@@ -1538,6 +1538,149 @@ optgroup {
color: var(--text-tertiary); color: var(--text-tertiary);
} }
/* ================================
铝金属价格板块
================================ */
.aluminum-section {
margin-bottom: var(--space-6);
}
.aluminum-price-card {
border-left: 4px solid var(--primary-500);
}
.aluminum-icon {
font-size: 1.2em;
}
.aluminum-source {
font-size: var(--text-xs);
color: var(--text-muted);
}
.aluminum-content {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.aluminum-price-row {
display: flex;
align-items: baseline;
gap: var(--space-6);
flex-wrap: wrap;
}
.aluminum-current {
display: flex;
align-items: baseline;
gap: var(--space-3);
}
.aluminum-price-value {
font-size: 2.5rem;
font-weight: var(--font-bold);
color: var(--text-primary);
line-height: 1.2;
font-variant-numeric: tabular-nums;
}
.aluminum-price-unit {
font-size: var(--text-sm);
color: var(--text-tertiary);
}
.aluminum-change {
display: flex;
align-items: center;
gap: var(--space-1);
font-weight: var(--font-semibold);
font-size: var(--text-base);
}
.aluminum-change.price-up {
color: #dc2626;
}
.aluminum-change.price-down {
color: #16a34a;
}
.change-arrow {
font-size: var(--text-sm);
}
.aluminum-detail-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: var(--space-3);
padding: var(--space-4);
background: var(--bg-secondary);
border-radius: var(--radius-lg);
}
.aluminum-detail-item {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.detail-label {
font-size: var(--text-xs);
color: var(--text-tertiary);
}
.detail-value {
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-primary);
font-variant-numeric: tabular-nums;
}
.price-up-text {
color: #dc2626;
}
.price-down-text {
color: #16a34a;
}
.aluminum-chart-wrapper {
margin-top: var(--space-2);
}
.aluminum-chart-title {
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-secondary);
margin-bottom: var(--space-3);
}
.aluminum-chart-container {
position: relative;
width: 100%;
height: 280px;
background: var(--bg-secondary);
border-radius: var(--radius-lg);
padding: var(--space-3);
}
@media (max-width: 768px) {
.aluminum-price-value {
font-size: 1.75rem;
}
.aluminum-price-row {
gap: var(--space-3);
}
.aluminum-detail-row {
grid-template-columns: repeat(2, 1fr);
gap: var(--space-2);
}
.aluminum-chart-container {
height: 220px;
}
}
/* ================================ /* ================================
响应式设计 响应式设计
================================ */ ================================ */
+142 -2
View File
@@ -463,7 +463,10 @@ const HomeView = {
const state = reactive({ const state = reactive({
stats: null, stats: null,
loading: true, loading: true,
backendDbReady: true backendDbReady: true,
aluminumPrice: null,
aluminumHistory: [],
aluminumLoading: true
}); });
const loadStats = async () => { const loadStats = async () => {
@@ -481,15 +484,94 @@ const HomeView = {
} }
}; };
const loadAluminumPrice = async () => {
try {
const [current, history] = await Promise.all([
apiRequest('/api/aluminum-price/current').catch(() => null),
apiRequest('/api/aluminum-price/history?days=30').catch(() => null)
]);
state.aluminumPrice = current;
state.aluminumHistory = history || [];
} catch (e) {
console.error('铝价数据加载失败:', e);
} finally {
state.aluminumLoading = false;
nextTick(() => {
renderAluminumChart();
});
}
};
let chartInstance = null;
const renderAluminumChart = () => {
const canvas = document.getElementById('aluminumChart');
if (!canvas || !state.aluminumHistory.length) return;
if (chartInstance) chartInstance.destroy();
const ctx = canvas.getContext('2d');
const labels = state.aluminumHistory.map(d => d.date.slice(5));
const prices = state.aluminumHistory.map(d => d.close);
const gradient = ctx.createLinearGradient(0, 0, 0, 280);
gradient.addColorStop(0, 'rgba(59, 130, 246, 0.35)');
gradient.addColorStop(1, 'rgba(59, 130, 246, 0.02)');
chartInstance = new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{
label: '铝价 (元/吨)',
data: prices,
borderColor: '#3b82f6',
backgroundColor: gradient,
borderWidth: 2,
fill: true,
tension: 0.3,
pointRadius: 0,
pointHoverRadius: 5,
pointHoverBackgroundColor: '#3b82f6',
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(23, 23, 23, 0.9)',
titleFont: { size: 12 },
bodyFont: { size: 13 },
padding: 10,
cornerRadius: 8,
displayColors: false,
callbacks: {
label: ctx => '¥' + ctx.parsed.y.toLocaleString() + ' 元/吨'
}
}
},
scales: {
x: {
grid: { display: false },
ticks: { color: '#a3a3a3', font: { size: 10 }, maxTicksLimit: 8 }
},
y: {
grid: { color: 'rgba(0,0,0,0.05)' },
ticks: { color: '#a3a3a3', font: { size: 10 }, callback: v => v.toLocaleString() }
}
}
}
});
};
onMounted(() => { onMounted(() => {
if (!appState.user) { if (!appState.user) {
router.push('/login'); router.push('/login');
return; return;
} }
loadStats(); loadStats();
loadAluminumPrice();
}); });
return { state, formatNumber, formatCurrency, appState }; return { state, formatNumber, formatCurrency, appState, loadAluminumPrice };
}, },
template: ` template: `
<div class="page-container"> <div class="page-container">
@@ -552,6 +634,64 @@ const HomeView = {
</div> </div>
</div> </div>
</div> </div>
<div class="aluminum-section">
<div class="card aluminum-price-card">
<div class="card-header">
<div class="card-title">
<span class="aluminum-icon">🪨</span>
铝金属实时行情
</div>
<div class="aluminum-source">数据来源: SHFE模拟</div>
</div>
<div class="card-body">
<div v-if="state.aluminumLoading" class="loading-state" style="padding: 20px;">
<div class="loading-spinner"></div>
<p>加载铝价数据...</p>
</div>
<div v-else-if="state.aluminumPrice" class="aluminum-content">
<div class="aluminum-price-row">
<div class="aluminum-current">
<div class="aluminum-price-value">¥{{ state.aluminumPrice.price?.toLocaleString() }}</div>
<div class="aluminum-price-unit">{{ state.aluminumPrice.unit }}</div>
</div>
<div class="aluminum-change" :class="state.aluminumPrice.change >= 0 ? 'price-up' : 'price-down'">
<span class="change-arrow">{{ state.aluminumPrice.change >= 0 ? '▲' : '▼' }}</span>
<span class="change-value">{{ Math.abs(state.aluminumPrice.change)?.toLocaleString() }}</span>
<span class="change-percent">({{ state.aluminumPrice.change_percent >= 0 ? '+' : '' }}{{ state.aluminumPrice.change_percent }}%)</span>
</div>
</div>
<div class="aluminum-detail-row">
<div class="aluminum-detail-item">
<span class="detail-label">开盘价</span>
<span class="detail-value">¥{{ state.aluminumPrice.open?.toLocaleString() }}</span>
</div>
<div class="aluminum-detail-item">
<span class="detail-label">最高价</span>
<span class="detail-value price-up-text">¥{{ state.aluminumPrice.high?.toLocaleString() }}</span>
</div>
<div class="aluminum-detail-item">
<span class="detail-label">最低价</span>
<span class="detail-value price-down-text">¥{{ state.aluminumPrice.low?.toLocaleString() }}</span>
</div>
<div class="aluminum-detail-item">
<span class="detail-label">昨收价</span>
<span class="detail-value">¥{{ state.aluminumPrice.prev_close?.toLocaleString() }}</span>
</div>
</div>
<div class="aluminum-chart-wrapper">
<div class="aluminum-chart-title">近30日价格走势</div>
<div class="aluminum-chart-container">
<canvas id="aluminumChart"></canvas>
</div>
</div>
</div>
<div v-else class="empty-state" style="padding: 20px;">
<p>铝价数据暂不可用</p>
</div>
</div>
</div>
</div>
<div v-if="state.stats?.inventory?.low_stock_products?.length" class="section"> <div v-if="state.stats?.inventory?.low_stock_products?.length" class="section">
<h2 class="section-title">低库存预警</h2> <h2 class="section-title">低库存预警</h2>