修改分模逻辑为注塑模
This commit is contained in:
@@ -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)
|
||||
@@ -68,7 +68,7 @@ class ShapeGraphBuilder:
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
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.TopExp import topexp_MapShapesAndAncestors
|
||||
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Edge, topods
|
||||
|
||||
@@ -21,7 +21,7 @@ from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
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.BRepGProp import brepgprop
|
||||
|
||||
|
||||
@@ -226,13 +226,14 @@ class BaseMoldGenerator:
|
||||
|
||||
def _split_cavity_core(self, shape: Any, parting_surface: Any, margin: int = 20) -> Tuple[Any, Any]:
|
||||
"""
|
||||
分离型腔和型芯
|
||||
分离型腔和型芯 — 完全嵌入 + 突出贴合方式
|
||||
|
||||
正确流程:
|
||||
1. 创建模具块(产品边界框 + 余量)
|
||||
2. 用分型面将模具块切分为 A板(上模)和 B板(下模)
|
||||
3. A板减去产品 → 型腔(凹模)
|
||||
4. B板减去产品 → 型芯(凸模)
|
||||
流程:
|
||||
1. 创建完整模具块(产品包围盒 + 全方向余量)
|
||||
2. 型腔(凹模)= 模具块 - 产品 → 产品完全嵌入型腔块中
|
||||
3. 型芯(凸模)= 产品形状突出体 → 从芯块面突出贴合
|
||||
|
||||
不再使用分型面中间切开产品的方式。
|
||||
"""
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
@@ -246,38 +247,69 @@ class BaseMoldGenerator:
|
||||
mold_ymax = ymax + margin
|
||||
mold_zmax = zmax + margin
|
||||
|
||||
mold_block = BRepPrimAPI_MakeBox(
|
||||
cavity_block = BRepPrimAPI_MakeBox(
|
||||
gp_Pnt(mold_xmin, mold_ymin, mold_zmin),
|
||||
gp_Pnt(mold_xmax, mold_ymax, mold_zmax)
|
||||
).Shape()
|
||||
|
||||
parting_plane = self._get_parting_plane(parting_surface, 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板")
|
||||
|
||||
cavity = self._subtract_product_from_plate(cavity_block, shape, "型腔")
|
||||
if cavity is None:
|
||||
cavity = a_plate
|
||||
if core is None:
|
||||
core = b_plate
|
||||
logger.warning("型腔布尔减运算失败,使用原始模具块")
|
||||
cavity = cavity_block
|
||||
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"型腔分离失败: {e}")
|
||||
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]:
|
||||
"""从分型面提取平面方程"""
|
||||
try:
|
||||
@@ -370,68 +402,52 @@ class BaseMoldGenerator:
|
||||
def _split_cavity_core_fallback(self, shape: Any,
|
||||
mold_block: Optional[Any] = None) -> Tuple[Any, Any]:
|
||||
"""
|
||||
分模回退方案:当分型面切分失败时使用
|
||||
分模回退方案:完全嵌入+突出贴合,不切分模具块。
|
||||
|
||||
使用Z中面将模具块简单切分为上下两半
|
||||
1. 创建完整模具块 → 型腔 = 模具块 - 产品(产品完全嵌入)
|
||||
2. 型芯 = 产品形状突出体(突出贴合)
|
||||
"""
|
||||
logger.warning("使用分模回退方案(Z中面切分)")
|
||||
logger.warning("使用分模回退方案(完全嵌入+突出贴合)")
|
||||
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
if mold_block is None:
|
||||
margin = 20
|
||||
if mold_block is None:
|
||||
mold_block = BRepPrimAPI_MakeBox(
|
||||
gp_Pnt(xmin - margin, ymin - margin, zmin - margin),
|
||||
gp_Pnt(xmax + margin, ymax + margin, zmax + margin)
|
||||
).Shape()
|
||||
|
||||
center_z = (zmin + zmax) / 2
|
||||
parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
|
||||
cavity = self._subtract_product_from_plate(mold_block, shape, "型腔(回退)")
|
||||
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:
|
||||
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
|
||||
|
||||
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
|
||||
logger.info("回退方案型腔/型芯分离完成")
|
||||
return cavity or mold_block, core
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分模回退方案失败: {e}")
|
||||
# 最后兜底也返回模具半板,而不是产品本体,避免预览出现“三份产品”
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
center_z = (zmin + zmax) / 2
|
||||
margin = 20
|
||||
a_plate = BRepPrimAPI_MakeBox(
|
||||
gp_Pnt(xmin - margin, ymin - margin, center_z),
|
||||
cavity_block = BRepPrimAPI_MakeBox(
|
||||
gp_Pnt(xmin - margin, ymin - margin, zmin - margin),
|
||||
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()
|
||||
return a_plate, b_plate
|
||||
cavity = self._subtract_product_from_plate(cavity_block, shape, "型腔(兜底)")
|
||||
return cavity or cavity_block, shape
|
||||
except Exception:
|
||||
return shape, shape
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ FreeCAD 导入建议:
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Dict, List, Any, Optional
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from pathlib import Path
|
||||
from utils.logger import get_logger
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
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 utils.logger import get_logger
|
||||
@@ -304,7 +304,7 @@ class MoldCavityGenerator(BaseMoldGenerator):
|
||||
normal = surface.Plane().Position().Direction()
|
||||
else:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib_Add(face, bbox)
|
||||
brepbndlib.Add(face, bbox)
|
||||
normal = gp_Dir(0, 0, 1)
|
||||
|
||||
face_normals.append(normal)
|
||||
|
||||
@@ -58,7 +58,7 @@ class UndercutDetector:
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
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
|
||||
|
||||
dir_vec = np.array(parting_direction, dtype=np.float64)
|
||||
@@ -118,7 +118,7 @@ class UndercutDetector:
|
||||
center = face_props.CentreOfMass()
|
||||
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib_Add(face, bbox)
|
||||
brepbndlib.Add(face, bbox)
|
||||
try:
|
||||
fxmin, fymin, fzmin, fxmax, fymax, fzmax = bbox.Get()
|
||||
except Exception:
|
||||
|
||||
@@ -44,6 +44,7 @@ import time
|
||||
|
||||
from api.auth_routes import router as auth_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 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(inventory_router)
|
||||
app.include_router(aluminum_price_router, prefix="/api")
|
||||
try:
|
||||
from api.v1 import router as moldinsight_router
|
||||
except Exception as e:
|
||||
|
||||
@@ -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
|
||||
@@ -294,7 +294,7 @@ class GeometryVerificationService:
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop_VolumeProperties, brepgprop_SurfaceProperties
|
||||
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.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX, TopAbs_SOLID
|
||||
|
||||
@@ -327,7 +327,7 @@ class GeometryVerificationService:
|
||||
|
||||
# 计算边界框
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib_Add(shape, bbox)
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
# 拓扑统计
|
||||
@@ -393,7 +393,7 @@ class GeometryVerificationService:
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop_VolumeProperties, brepgprop_SurfaceProperties
|
||||
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.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX, TopAbs_SOLID
|
||||
|
||||
@@ -410,7 +410,7 @@ class GeometryVerificationService:
|
||||
|
||||
# 计算边界框
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib_Add(shape, bbox)
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
# 拓扑统计
|
||||
|
||||
+2
-1
@@ -76,7 +76,8 @@
|
||||
|
||||
<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://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="/static/vue-app.js?v=20260409-01"></script>
|
||||
<script src="/static/vue-app.js?v=20260506-01"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1538,6 +1538,149 @@ optgroup {
|
||||
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
@@ -463,7 +463,10 @@ const HomeView = {
|
||||
const state = reactive({
|
||||
stats: null,
|
||||
loading: true,
|
||||
backendDbReady: true
|
||||
backendDbReady: true,
|
||||
aluminumPrice: null,
|
||||
aluminumHistory: [],
|
||||
aluminumLoading: true
|
||||
});
|
||||
|
||||
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(() => {
|
||||
if (!appState.user) {
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
loadStats();
|
||||
loadAluminumPrice();
|
||||
});
|
||||
|
||||
return { state, formatNumber, formatCurrency, appState };
|
||||
return { state, formatNumber, formatCurrency, appState, loadAluminumPrice };
|
||||
},
|
||||
template: `
|
||||
<div class="page-container">
|
||||
@@ -553,6 +635,64 @@ const HomeView = {
|
||||
</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">
|
||||
<h2 class="section-title">低库存预警</h2>
|
||||
<div class="table-container">
|
||||
|
||||
Reference in New Issue
Block a user