Files
geMoldInsight/frontend/src/modules/home/HomeView.vue
T
cjw e728dcd226 批次4后续专项完成:D11清偿 + OCC方案B实施 + 部署参数 + D2诚实标注 + CI门禁
① D11 HTML 报告 RustFS 单源化(TECH_DEBT P2 清偿):可视化产物写任务临时目录后
   裸传报告键 html/reports/{filename}(文件名寻址),/html StaticFiles 挂载删除,
   新增 html_report_router 根路径代理(报告键→遗留 JSON 包装→本地卷兜底→404,
   防穿越);URL 形状 /html/{filename} 不变,持久化引用零迁移;celery 摘除
   html_data 卷,镜像不再烤入陈旧报告;顺带删除 get_stp_file_with_data 死数据块
② OCC 方案 B(D10 清偿):run_occ(op_name, payload) 契约 + 常驻工作进程池
   (occ_process_pool + occ_worker 操作注册表),超时/崩溃 terminate 换新补位、
   任务级超时 recover 整体重建,残留线程泄漏根治;TopoDS 不跨进程(generate_cavity
   分模 + 方案 STEP 持久化全在子进程内,返回 export_manifest);删除内存形状缓存链、
   CADExporter.export_mold_results、shape_loader(→ stp_materializer)
③ OCC 方案 A 部署参数:CELERY_CONCURRENCY / CELERY_MAX_TASKS_PER_CHILD 进
   Dockerfile.celery + compose + .env.example
④ D2 诚实标注:铝价响应带 source: "simulated",前端按来源渲染标注(原硬编码
   "上海期货交易所"属虚假声明),死代码 getAluminumPrice 删除
⑤ CI 门禁:.gitea/workflows/ci.yml 三 job(pytest / 前端构建含 vue-tsc /
   openapi 漂移检测)

接口变更三件套随批完成(openapi 76→77 paths + gen:api + 前端构建通过;方案 B
接口面零变化)。测试基线 143 passed, 0 skipped(新增 16 项)。文档六处同步。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 17:22:01 +08:00

235 lines
8.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<div class="hero-section">
<h1 class="hero-title">Gemold 模具制造管理系统</h1>
<p class="hero-subtitle">智能模具设计与制造一体化平台,从零件分析到模具生成,全流程数字化解决方案</p>
</div>
<div class="aluminum-section" v-if="aluminum.price">
<div class="card aluminum-price-card">
<div class="card-header">
<div class="card-title">
<span class="aluminum-icon">🪙</span> 铝金属价格
</div>
<span class="aluminum-source">{{ aluminumSourceLabel }}</span>
</div>
<div class="aluminum-content">
<div class="aluminum-price-row">
<div class="aluminum-current">
<span class="aluminum-price-value">{{ formatAluminumPrice(aluminum.price) }}</span>
<span class="aluminum-price-unit">元/吨</span>
</div>
<div :class="['aluminum-change', aluminum.change >= 0 ? 'price-up' : 'price-down']">
<span class="change-arrow">{{ aluminum.change >= 0 ? '▲' : '▼' }}</span>
<span>{{ formatAluminumPrice(Math.abs(aluminum.change)) }}</span>
<span>({{ aluminum.change_percent >= 0 ? '+' : '' }}{{ aluminum.change_percent }}%)</span>
</div>
</div>
<div class="aluminum-detail-row">
<div class="aluminum-detail-item">
<span class="detail-label">开盘价</span>
<span class="detail-value">{{ formatAluminumPrice(aluminum.open) }}</span>
</div>
<div class="aluminum-detail-item">
<span class="detail-label">最高价</span>
<span class="detail-value">{{ formatAluminumPrice(aluminum.high) }}</span>
</div>
<div class="aluminum-detail-item">
<span class="detail-label">最低价</span>
<span class="detail-value">{{ formatAluminumPrice(aluminum.low) }}</span>
</div>
<div class="aluminum-detail-item">
<span class="detail-label">昨收价</span>
<span class="detail-value">{{ formatAluminumPrice(aluminum.prev_close) }}</span>
</div>
<div class="aluminum-detail-item">
<span class="detail-label">7日前</span>
<span class="detail-value">{{ formatAluminumPrice(aluminum.week_ago_price) }}</span>
</div>
</div>
<div class="aluminum-chart-wrapper" v-if="aluminum.history.length > 0">
<div class="aluminum-chart-title">近30日价格走势</div>
<div class="aluminum-chart-container">
<canvas ref="chartCanvas"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="features-grid">
<div class="feature-card" @click="router.push('/moldinsight')" style="cursor: pointer;">
<div class="feature-icon">◈</div>
<h3 class="feature-title">MoldInsight</h3>
<p class="feature-desc">智能模具分析引擎,支持 STP/STEP 文件解析、分模设计、CAM 刀路规划</p>
</div>
<div class="feature-card" @click="router.push('/inventory')" style="cursor: pointer;">
<div class="feature-icon">⊞</div>
<h3 class="feature-title">进销存管理</h3>
<p class="feature-desc">产品管理、采购销售、库存管理、财务管理,一站式 ERP 解决方案</p>
</div>
<div class="feature-card">
<div class="feature-icon">⚙</div>
<h3 class="feature-title">AI 驱动</h3>
<p class="feature-desc">AI 分型面检测、模具方案评分、智能设计建议,提升设计效率</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, onMounted, nextTick, ref } from 'vue'
import { useRouter } from 'vue-router'
import { apiRequest } from '@/shared/api'
import { Chart, registerables } from 'chart.js'
const router = useRouter()
const chartCanvas = ref<HTMLCanvasElement | null>(null)
let chartInstance: Chart | null = null
interface AluminumPrice {
price: number
unit: string
date: string
change: number
change_percent: number
open: number
high: number
low: number
prev_close: number
week_ago_price: number
// D2:数据来源声明——simulated 为模拟走势(参考数据,非实时行情)
source?: string
}
interface HistoryItem {
date: string
close: number
}
const aluminum = reactive({
price: null as number | null,
unit: '',
date: '',
change: 0,
change_percent: 0,
open: 0,
source: '',
high: 0,
low: 0,
prev_close: 0,
week_ago_price: 0,
history: [] as HistoryItem[],
})
const formatAluminumPrice = (val: number | null) => {
if (val === null || val === undefined) return '--'
return Math.round(val).toLocaleString('zh-CN')
}
// D2:来源标注以接口 source 字段为准,不再硬编码交易所名称——
// 此前显示"数据来源: 上海期货交易所"而数据实为模拟走势,属虚假来源声明
const aluminumSourceLabel = computed(() => {
if (aluminum.source === 'simulated') return '模拟数据 · 参考走势,非实时行情'
if (aluminum.source) return `数据来源: ${aluminum.source}`
return `更新于 ${aluminum.date}`
})
const loadAluminumPrice = async () => {
try {
const data = await apiRequest<AluminumPrice>('/api/aluminum-price/current')
aluminum.price = data.price
aluminum.unit = data.unit
aluminum.date = data.date
aluminum.source = data.source ?? ''
aluminum.change = data.change
aluminum.change_percent = data.change_percent
aluminum.open = data.open
aluminum.high = data.high
aluminum.low = data.low
aluminum.prev_close = data.prev_close
aluminum.week_ago_price = data.week_ago_price
} catch (e) {
console.error('加载铝价失败:', e)
}
try {
const history = await apiRequest<HistoryItem[]>('/api/aluminum-price/history?days=30')
aluminum.history = history
await nextTick()
renderChart()
} catch (e) {
console.error('加载铝价历史失败:', e)
}
}
const renderChart = () => {
if (!chartCanvas.value || aluminum.history.length === 0) return
if (chartInstance) chartInstance.destroy()
const isUp = aluminum.change >= 0
const lineColor = isUp ? '#dc2626' : '#16a34a'
const fillColor = isUp ? 'rgba(220, 38, 38, 0.08)' : 'rgba(22, 163, 74, 0.08)'
chartInstance = new Chart(chartCanvas.value, {
type: 'line',
data: {
labels: aluminum.history.map(h => h.date.slice(5)),
datasets: [{
label: '收盘价 (元/吨)',
data: aluminum.history.map(h => h.close),
borderColor: lineColor,
backgroundColor: fillColor,
fill: true,
tension: 0.3,
pointRadius: 0,
borderWidth: 2,
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: (ctx) => `${Math.round(ctx.parsed.y ?? 0).toLocaleString('zh-CN')} 元/吨`,
},
},
},
scales: {
x: {
grid: { display: false },
ticks: { maxTicksLimit: 10, font: { size: 10 }, color: 'var(--text-tertiary)' },
},
y: {
grid: { color: 'var(--border-light)' },
ticks: {
font: { size: 10 },
color: 'var(--text-tertiary)',
callback: (v) => (v as number).toLocaleString('zh-CN'),
},
},
},
},
})
}
onMounted(() => {
Chart.register(...registerables)
loadAluminumPrice()
})
</script>
<style scoped>
.hero-section { text-align: center; padding: var(--space-16) 0; }
.hero-title { font-size: var(--text-4xl); font-weight: var(--font-bold); color: var(--text-primary); margin-bottom: var(--space-4); }
.hero-subtitle { font-size: var(--text-lg); color: var(--text-tertiary); max-width: 600px; margin: 0 auto var(--space-8); }
.features-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--space-6); margin-top: var(--space-12); }
.feature-card { background: var(--bg-primary); border: 1px solid var(--border-light); border-radius: var(--radius-xl); padding: var(--space-6); text-align: left; }
.feature-card[style*="cursor"]:hover { border-color: var(--primary-200); box-shadow: var(--shadow-md); transform: translateY(-2px); transition: all 0.2s ease; }
.feature-icon { width: 40px; height: 40px; background: var(--primary-50); border-radius: var(--radius-lg); display: flex; align-items: center; justify-content: center; color: var(--primary-500); font-size: var(--text-lg); margin-bottom: var(--space-4); }
.feature-title { font-size: var(--text-base); font-weight: var(--font-semibold); color: var(--text-primary); margin-bottom: var(--space-2); }
.feature-desc { font-size: var(--text-sm); color: var(--text-tertiary); line-height: 1.6; }
</style>