diff --git a/bj_power_dashboard/src/App.tsx b/bj_power_dashboard/src/App.tsx index d026b57..d971834 100644 --- a/bj_power_dashboard/src/App.tsx +++ b/bj_power_dashboard/src/App.tsx @@ -1,31 +1,23 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect } from 'react'; import { useStore } from './store/useStore'; import { color } from './theme/tokens'; import KPICards from './components/KPICards'; import EquipmentGrid from './components/EquipmentGrid'; import ProductionChart from './components/ProductionChart'; -import CncStatusChart from './components/CncStatusChart'; import AlarmTable from './components/AlarmTable'; import TrendChart from './components/TrendChart'; import YieldRateChart from './components/YieldRateChart'; -import SamplingTrendChart from './components/SamplingTrendChart'; -import InspectionTrendChart from './components/InspectionTrendChart'; -import MachineMonitor from './components/MachineMonitor'; import { AppHeader } from './components/AppHeader'; import { Panel } from './components/ui/Panel'; -type Page = 'overview' | 'monitor'; - export default function App() { - const [page, setPage] = useState('overview'); const fetchAll = useStore((s) => s.fetchAll); const refresh = useStore((s) => s.refresh); - const intervalRef = useRef>(); useEffect(() => { fetchAll(); - intervalRef.current = setInterval(refresh, 10000); - return () => clearInterval(intervalRef.current); + const t = setInterval(refresh, 10000); + return () => clearInterval(t); }, [fetchAll, refresh]); // 订阅 MES SSE:看板相关事件(dashboard_update)实时触发刷新 @@ -37,15 +29,7 @@ export default function App() { return () => es.close(); }, [refresh]); - // 产线概览 / 设备监控 每 30s 自动轮播切换 - // 依赖 page:用户手动切页后计时从 0 重新开始,避免刚切完立刻被自动切走 - useEffect(() => { - const t = setInterval(() => { - setPage((p) => (p === 'overview' ? 'monitor' : 'overview')); - }, 30000); - return () => clearInterval(t); - }, [page]); - + // 装配线看板:仅产线概览(扫码枪+拧紧枪工位,无 CNC/清洗机) return (
- + - {page === 'overview' ? ( -
+ {/* KPI Row */} +
+ +
+ + {/* Middle Row: Equipment | Production | Alarms */} +
- {/* KPI Row */} -
- -
+ + + + + + + + + +
- {/* Middle Row: Equipment | Production | CNC Status | Alarms */} -
- - - - - - - - - - - - -
- - {/* Bottom Row */} -
- - - - - - - - - - - - -
-
- ) : ( -
- -
- )} + + + + + + +
+ ); } diff --git a/bj_power_dashboard/src/api/client.ts b/bj_power_dashboard/src/api/client.ts index 7f3893c..509e85c 100644 --- a/bj_power_dashboard/src/api/client.ts +++ b/bj_power_dashboard/src/api/client.ts @@ -2,7 +2,7 @@ import axios from 'axios'; // MES(B) 内部 API:X-API-TOKEN 写死 token(值与 MES etc 配置 Internal.Token 一致) export const MES_TOKEN = - (import.meta as any).env?.VITE_MES_TOKEN || 'bj-power-internal-2026'; + (import.meta as any).env?.VITE_MES_TOKEN || 'Hardman_2026'; const api = axios.create({ baseURL: '/api/v1/dashboard', diff --git a/bj_power_dashboard/src/components/AppHeader.tsx b/bj_power_dashboard/src/components/AppHeader.tsx index a9cdbe1..27cf4ef 100644 --- a/bj_power_dashboard/src/components/AppHeader.tsx +++ b/bj_power_dashboard/src/components/AppHeader.tsx @@ -4,19 +4,7 @@ import { color, font, radius } from '../theme/tokens'; import { StatusDot } from './ui/StatusDot'; import { useStore } from '../store/useStore'; -type Page = 'overview' | 'monitor'; - -interface AppHeaderProps { - page: Page; - onChangePage: (p: Page) => void; -} - -const TABS: { key: Page; label: string; en: string }[] = [ - { key: 'overview', label: '产线概览', en: 'OVERVIEW' }, - { key: 'monitor', label: '设备监控', en: 'MONITOR' }, -]; - -export function AppHeader({ page, onChangePage }: AppHeaderProps) { +export function AppHeader() { const [now, setNow] = useState(() => dayjs()); const overview = useStore((s) => s.overview); @@ -52,7 +40,7 @@ export function AppHeader({ page, onChangePage }: AppHeaderProps) { lineHeight: 1.1, margin: 0, }}> - 液压马达后盖 CNC 产线 + 液压马达后盖装配线
- HYDRAULIC MOTOR REAR COVER · CNC PRODUCTION COMMAND CENTER + HYDRAULIC MOTOR REAR COVER · ASSEMBLY LINE COMMAND CENTER
- {/* Center: Tabs */} - - {/* Right: Status + Time */}
= { - RUN: '运行', - STOP: '停止', - ALM: '报警', - HOLD: '暂停', - MSTR: '手动', - MEM: '记忆', - AUTO: '自动', - 待机: '待机', - 离线: '离线', -}; - -// 展示顺序:运行最优先,报警最显眼,其余跟随 -const STATUS_ORDER = ['RUN', 'ALM', '待机', 'HOLD', 'STOP', 'MSTR', 'MEM', '离线'] as const; - -function statusColor(s: string): string { - if (s === 'RUN') return color.status.success; - if (s === 'ALM') return color.status.danger; - if (s === 'HOLD' || s === 'STOP') return color.status.warning; - if (s === '离线') return color.status.neutral; - if (s === '待机') return color.status.info; - return color.status.info; -} - -/** - * CNC 状态分布环形图 - * - * 数据源:cncLatest(后端 /cnc-latest,以 dc_device_status 中的 CNC 设备为基准)。 - * 按实际机器运行状态(RUN/STOP/ALM/HOLD 等)聚合;无 okuma 快照时按连接状态 - * 兜底为"待机/离线",与 MachineMonitor 卡片口径一致。 - */ -export default function CncStatusChart() { - const cncLatest = useStore((s) => s.cncLatest); - - const { buckets, total } = useMemo(() => { - const counts = new Map(); - for (const cnc of cncLatest) { - // 与 MachineMonitor 兜底逻辑保持一致 - const hasSnapshot = !!cnc.status; - const status = hasSnapshot ? cnc.status : (cnc.is_connected ? '待机' : '离线'); - counts.set(status, (counts.get(status) ?? 0) + 1); - } - const ordered: { status: string; count: number }[] = []; - for (const s of STATUS_ORDER) { - if (counts.has(s)) ordered.push({ status: s, count: counts.get(s)! }); - } - // 兜底:未在预定义顺序的状态拼到末尾 - for (const [status, count] of counts) { - if (!(STATUS_ORDER as readonly string[]).includes(status)) { - ordered.push({ status, count }); - } - } - return { buckets: ordered, total: cncLatest.length }; - }, [cncLatest]); - - const option = useMemo(() => { - if (total === 0) { - return { - backgroundColor: 'transparent', - title: { - text: '暂无 CNC 数据', - left: 'center', top: 'middle', - textStyle: { color: color.text.tertiary, fontSize: 12, fontWeight: 400 }, - }, - }; - } - - return { - backgroundColor: 'transparent', - tooltip: { - trigger: 'item', - backgroundColor: 'rgba(8,15,32,0.95)', - borderColor: 'rgba(34,211,238,0.4)', - borderWidth: 1, - textStyle: { color: '#e6f1ff', fontSize: 12, fontFamily: font.body }, - extraCssText: 'box-shadow:0 8px 24px rgba(0,0,0,0.6); backdrop-filter: blur(8px);', - formatter: (p: any) => { - const pct = total > 0 ? ((p.value / total) * 100).toFixed(0) : '0'; - return ` -
- - ${p.name} - ${p.value} 台 - ${pct}% -
`; - }, - }, - // 中心总台数 + 副标题 - graphic: [ - { - type: 'text', - left: 'center', - top: '38%', - style: { - text: String(total), - fill: color.text.primary, - fontFamily: font.mono, - fontSize: 28, - fontWeight: 700, - textShadowColor: 'rgba(34,211,238,0.5)', - textShadowBlur: 12, - }, - }, - { - type: 'text', - left: 'center', - top: '58%', - style: { - text: 'CNC 台数', - fill: color.text.tertiary, - fontFamily: font.display, - fontSize: 10, - }, - }, - ], - series: [ - { - name: 'CNC 状态', - type: 'pie', - radius: ['58%', '78%'], - center: ['50%', '50%'], - avoidLabelOverlap: false, - label: { show: false }, - labelLine: { show: false }, - itemStyle: { - borderColor: 'rgba(8,15,32,0.95)', - borderWidth: 2, - shadowBlur: 8, - shadowColor: 'rgba(34,211,238,0.25)', - }, - emphasis: { - scale: true, - scaleSize: 4, - itemStyle: { shadowBlur: 16 }, - }, - data: buckets.map(({ status, count }) => ({ - name: CNC_STATUS_LABEL[status] || status, - value: count, - itemStyle: { color: statusColor(status) }, - })), - }, - ], - animationDuration: 700, - animationEasing: 'cubicOut', - }; - }, [buckets, total]); - - const ref = useEChart(option); - - return ( -
-
-
-
- {/* 图例:状态计数(空态不渲染,避免空行) */} - {total > 0 && ( -
- {buckets.map(({ status, count }) => { - const c = statusColor(status); - return ( -
- - - {CNC_STATUS_LABEL[status] || status} - - - {count} - -
- ); - })} -
- )} -
- ); -} diff --git a/bj_power_dashboard/src/components/InspectionTrendChart.tsx b/bj_power_dashboard/src/components/InspectionTrendChart.tsx deleted file mode 100644 index dcbd67b..0000000 --- a/bj_power_dashboard/src/components/InspectionTrendChart.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useMemo } from 'react'; -import * as echarts from 'echarts'; -import type { EChartsOption } from 'echarts'; -import { useStore } from '../store/useStore'; -import { color, font } from '../theme/tokens'; -import { useEChart } from '../hooks/useEChart'; - -// 三种 category 的展示顺序与配色(与 tokens.category 对齐,缺省回退中性色) -const CATEGORY_ORDER = ['A6VM107', 'A6VM160', 'A6VM200']; -const catColor = (cat: string) => - (color.category as Record)[cat] || color.status.neutral; - -// 内窥镜检测趋势:与 SamplingTrendChart 同构,数据源 inspectionTrends -export default function InspectionTrendChart() { - const trends = useStore((s) => s.inspectionTrends); - - const option = useMemo(() => { - if (!trends.length) return null; - - const dates = Array.from(new Set(trends.map((t) => t.date))); - const cats = Array.from(new Set(trends.map((t) => t.category))) - .sort((a, b) => CATEGORY_ORDER.indexOf(a) - CATEGORY_ORDER.indexOf(b)); - - const cell = new Map(); - for (const t of trends) { - cell.set(`${t.date}|${t.category}`, { passed: t.passed, failed: t.failed }); - } - - // 每个 category 两个 series:合格(category 色) + 不合格(红色),堆叠到同一 category - const series: any[] = []; - for (const cat of cats) { - const base = catColor(cat); - series.push({ - name: `${cat} 合格`, - type: 'bar', - stack: cat, - barMaxWidth: 22, - itemStyle: { - color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ - { offset: 0, color: base }, - { offset: 1, color: `${base}66` }, - ]), - borderRadius: [2, 2, 0, 0], - shadowBlur: 6, - shadowColor: `${base}55`, - }, - emphasis: { focus: 'series' as const }, - data: dates.map((d) => cell.get(`${d}|${cat}`)?.passed ?? 0), - }); - series.push({ - name: `${cat} 不合格`, - type: 'bar', - stack: cat, - barMaxWidth: 22, - itemStyle: { - color: color.status.danger, - borderRadius: [2, 2, 0, 0], - }, - emphasis: { focus: 'series' as const }, - data: dates.map((d) => cell.get(`${d}|${cat}`)?.failed ?? 0), - }); - } - - return { - backgroundColor: 'transparent', - tooltip: { - trigger: 'axis', - backgroundColor: 'rgba(8,15,32,0.95)', - borderColor: 'rgba(34,211,238,0.4)', - borderWidth: 1, - textStyle: { color: '#e6f1ff', fontSize: 12, fontFamily: font.body }, - extraCssText: 'box-shadow:0 8px 24px rgba(0,0,0,0.6); backdrop-filter: blur(8px);', - axisPointer: { - type: 'shadow', - shadowStyle: { color: 'rgba(34,211,238,0.06)' }, - }, - formatter: (params: any) => { - const date = params[0]?.axisValue ?? ''; - const byCat = new Map(); - for (const p of params) { - const m = /^(\S+)\s(合格|不合格)$/.exec(p.seriesName); - if (!m) continue; - const [, cat, kind] = m; - const cur = byCat.get(cat) ?? { passed: 0, failed: 0 }; - if (kind === '合格') cur.passed = p.value ?? 0; - else cur.failed = p.value ?? 0; - byCat.set(cat, cur); - } - const rows = Array.from(byCat.entries()) - .map(([cat, v]) => { - const total = v.passed + v.failed; - const yld = total > 0 ? ((v.passed / total) * 100).toFixed(1) : '--'; - const c = catColor(cat); - return ` -
- - ${cat} - 合 ${v.passed} - 废 ${v.failed} - ${yld}% -
`; - }) - .join(''); - const dayTotal = Array.from(byCat.values()).reduce((s, v) => s + v.passed + v.failed, 0); - return ` -
- ${date} 检测合计 ${dayTotal} 件 -
${rows}`; - }, - }, - legend: { - data: series.map((s) => s.name), - textStyle: { color: color.text.secondary, fontSize: 10, fontFamily: font.body }, - right: 8, top: 4, - itemWidth: 8, itemHeight: 8, itemGap: 8, - icon: 'circle', - }, - grid: { top: 36, right: 12, bottom: 24, left: 36, containLabel: true }, - xAxis: { - type: 'category', - data: dates, - axisLabel: { - color: color.text.tertiary, fontSize: 10, fontFamily: font.mono, - }, - axisLine: { lineStyle: { color: color.border.base } }, - axisTick: { show: false }, - }, - yAxis: { - type: 'value', - minInterval: 1, - axisLabel: { - color: color.text.tertiary, fontSize: 10, fontFamily: font.mono, - }, - axisLine: { show: false }, - axisTick: { show: false }, - splitLine: { lineStyle: { color: color.border.grid, type: 'dashed' } }, - }, - series, - animationDuration: 700, - animationEasing: 'cubicOut', - }; - }, [trends]); - - const ref = useEChart(option); - return
; -} diff --git a/bj_power_dashboard/src/components/KPICards.tsx b/bj_power_dashboard/src/components/KPICards.tsx index 26210ce..60e3692 100644 --- a/bj_power_dashboard/src/components/KPICards.tsx +++ b/bj_power_dashboard/src/components/KPICards.tsx @@ -144,35 +144,19 @@ const IconAlert = () => ( ); -const IconSampling = () => ( - - - - - -); -const IconInspection = () => ( - - - - - -); export default function KPICards() { const overview = useStore((s) => s.overview); - const samplingOverview = useStore((s) => s.samplingOverview); - const inspectionOverview = useStore((s) => s.inspectionOverview); if (!overview) { return (
- {[0, 1, 2, 3, 4, 5, 6].map(i => ( + {[0, 1, 2, 3, 4].map(i => (
@@ -207,8 +191,8 @@ export default function KPICards() { icon={} /> } /> - } - /> - } - /> - - {name} - - {type} - -
- ); -} - -// ===== OEE Ring ===== -function OeeRing({ value, label, size = 52, color: ringColor }: { - value: number; label: string; size?: number; color?: string; -}) { - const r = size / 2; - const stroke = 4; - const inner = r - stroke; - const circ = 2 * Math.PI * inner; - const v = Math.max(0, Math.min(value, 100)); - const offset = circ * (1 - v / 100); - - const c = ringColor ?? (v >= 85 ? color.status.success : v >= 60 ? color.status.warning : color.status.danger); - - return ( -
- - - - - - - - - - = 56 ? 14 : 12} - fontWeight={700} - fontFamily={font.mono}> - {v.toFixed(0)} - - -
- {label} -
-
- ); -} - -function RuntimeRow({ label, value, c }: { label: string; value: string; c: string }) { - return ( -
- - {label} - - - {value} - -
- ); -} - -// ===== 通用小格子:label + 数值 + 可选 SIM 角标 ===== -function Metric({ label, val, fmt: fmtFn, color: c, unit }: { - label: string; - val: FieldVal; - fmt?: (v: number | string | null) => string; - color?: string; - unit?: string; -}) { - const text = val.value == null ? '--' - : fmtFn ? fmtFn(val.value) - : String(val.value); - return ( -
- - {label}{val.source === 'sim' && } - - - {text}{unit && text !== '--' ? {unit} : null} - -
- ); -} - -// ===== 轴单格(比 Metric 更紧凑,用于轴行内联) ===== -function AxisCell({ label, val, fmt: fmtFn, color: c, unit }: { - label: string; - val: FieldVal; - fmt?: (v: number | null) => string; - color?: string; - unit?: string; -}) { - const text = val.value == null ? '--' : fmtFn ? fmtFn(val.value) : String(val.value); - return ( -
- - {label}{val.source === 'sim' && } - - - {text}{unit && text !== '--' ? {unit} : null} - -
- ); -} - -// ===== 区块标题 ===== -function SectionLabel({ children, accent }: { children: string; accent?: string }) { - return ( -
- - {children} -
- ); -} - -// ===== Status Badge for CNC ===== -function statusTone(status: string): 'success' | 'danger' | 'warning' | 'info' | 'neutral' { - if (status.includes('ALM') || status === 'FAULT') return 'danger'; - if (status === 'RUN') return 'success'; - if (status === 'HOLD' || status === 'STOP' || status === '待机') return 'warning'; - if (status === '离线' || status === 'OFFLINE') return 'neutral'; - if (status === 'AUTO' || status === 'MSTR' || status === 'MEM') return 'info'; - return 'neutral'; -} - -// ===== CNC Card ===== -function CncCard({ cnc, oee }: { cnc: CncDisplay; oee?: CncOeeItem }) { - const axes = cnc.axes ?? []; - const hasSnapshot = !!cnc.status || axes.length > 0 || !!cnc.program; - - // 喂真实状态给模拟器(每次渲染都同步) - const simStatus: SimStatus = !hasSnapshot && !cnc.is_connected - ? 'OFFLINE' - : (cnc.status || 'IDLE') as SimStatus; - simulator.setState(cnc.machine, simStatus, cnc.mode || 'AUTO', cnc.program || '', !!cnc.is_connected); - - // 取合并后的完整字段集(轴名用真实轴名) - const axisNames = axes.length > 0 ? axes.map((a) => a.name || '') : ['X', 'Y', 'Z']; - const sim = simulator.getState(cnc.machine, axisNames); - const f = mergeCnc(cnc, sim); - - const displayStatus = hasSnapshot - ? cnc.status - : (cnc.is_connected ? '待机' : '离线'); - - const isAlarm = (cnc.status || '').includes('ALM'); - const isRunning = cnc.status === 'RUN'; - const isOffline = !hasSnapshot && !cnc.is_connected; - const accentColor = isAlarm - ? color.status.danger - : isRunning - ? color.status.success - : isOffline - ? color.status.neutral - : color.status.warning; - - return ( -
-
- - {/* Header */} -
-
- - - {cnc.machine} - -
-
- {displayStatus || '未知'} - - {f.mode.value}{f.mode.source === 'sim' && } - - - 急停{f.eStop.value ? 'ON' : 'OFF'}{f.eStop.source === 'sim' && } - -
-
- - {/* OEE rings + runtime */} -
- {oee ? ( - <> - -
- - - -
-
- - - -
- - ) : ( -
- 等待 OEE 数据... -
- )} -
- - {/* 报警条:仅报警时有内容,单行紧凑 */} -
- 报警 - - {f.alarmNo.value || '--'}{f.alarmNo.source === 'sim' && } - - - {f.alarmType.value || '--'}{f.alarmType.source === 'sim' && } - - - {f.alarmText.value || '无报警'}{f.alarmText.source === 'sim' && !f.alarmText.value ? : null} - - - {fmt.time(f.alarmStart.value as string)} → {fmt.time(f.alarmEnd.value as string)}{f.alarmStart.source === 'sim' && } - -
- - {/* 进给轴:每轴一行,坐标4系+速度3项 横排紧凑 */} - 进给轴 · AXES -
- {f.axes.map((ax, i) => ( -
- {ax.name} - (v as number).toFixed(3)} /> - (v as number).toFixed(3)} /> - (v as number).toFixed(3)} /> - (v as number).toFixed(3)} /> - (v as number).toFixed(0)} unit="m/m" color={color.status.info} /> - (v as number).toFixed(0)} unit="m/m" color={color.status.success} /> - (v as number).toFixed(0)} unit="%" color={color.status.warning} /> -
- ))} -
- - {/* 工件轴 + 进给 合并一行 8 格 */} - 主轴 / 进给 · SPINDLE / FEED -
- (v as number).toFixed(0)} unit="rpm" color={color.status.info} /> - (v as number).toFixed(0)} unit="rpm" color={color.status.success} /> - `${(v as number).toFixed(0)}`} unit="%" color={color.status.accent} /> - (v as number).toFixed(0)} unit="%" color={color.status.warning} /> - (v as number).toFixed(0)} unit="m/m" color={color.status.info} /> - (v as number).toFixed(0)} unit="m/m" color={color.status.success} /> - `${(v as number).toFixed(0)}`} unit="%" color={color.status.accent} /> - (v as number).toFixed(0)} unit="%" color={color.status.warning} /> -
- - {/* 程序区:名称/行号/行程序 + 时间类 + 能耗,一个紧凑块 */} - 程序 · PROGRAM -
-
- PGM - - {f.program.value || '--'}{f.program.source === 'sim' && } - - N - - {f.lineNo.value}{f.lineNo.source === 'sim' && } - - 循环 - - {fmt.duration(f.cycleTotal.value as number)}{f.cycleTotal.source === 'sim' && } - - - - {fmt.duration(f.cycleRemain.value as number)}{f.cycleRemain.source === 'sim' && } - - 能耗 - - {(f.energy.value as number).toFixed(3)}kWh{f.energy.source === 'sim' && } - -
-
- 加工 - {fmt.time(f.machStart.value as string)} → {fmt.time(f.machEnd.value as string)}{f.machStart.source === 'sim' && } -
-
-
- ); -} - -// ===== Washer Card ===== -function WasherCard({ points, oee }: { points: WasherPoint[]; oee?: WasherOeeItem }) { - const connected = oee?.is_connected ?? false; - const accent = connected ? color.status.info : color.status.neutral; - - // 喂真实状态给模拟器:清洗机状态点 data_name="状态" - const statusPoint = points.find((p) => p.data_name === '状态'); - const rawStatus = statusPoint?.value || ''; - const simStatus: SimStatus = connected ? (rawStatus || 'IDLE') as SimStatus : 'OFFLINE'; - simulator.setState('washer-hp', simStatus, 'AUTO', '', connected); - - const sim = simulator.getState('washer-hp', ['X', 'Y', 'Z']); - const f = mergeWasher(points, sim); - - return ( -
-
- - {/* Header */} -
-
- - - 高压清洗机 - -
-
- {connected ? '在线' : '离线'} - - {f.mode.value}{f.mode.source === 'sim' && } - - - 急停{f.eStop.value ? 'ON' : 'OFF'}{f.eStop.source === 'sim' && } - -
-
- - {/* OEE compact */} - {oee ? ( -
- -
- - - -
-
- - -
-
- ) : ( -
- 等待 OEE 数据... -
- )} - - {/* 报警区 */} -
- - - - fmt.time(v as string)} color={color.status.danger} /> - fmt.time(v as string)} color={color.status.danger} /> -
- - {/* 三种坐标:机械 / 绝对 / 相对,各 X/Y/Z */} - 坐标 · COORDINATE -
- {([ - { label: '机械', getVal: (i: number) => f.axes[i].mac, c: color.primary[400] }, - { label: '绝对', getVal: (i: number) => f.axes[i].abs, c: color.status.success }, - { label: '相对', getVal: (i: number) => f.axes[i].rel, c: color.status.accent }, - { label: '剩余', getVal: (i: number) => f.axes[i].remain, c: color.status.warning }, - ] as const).map(({ label, getVal, c }) => ( -
- - {label} - - {['X', 'Y', 'Z'].map((ax, i) => { - const v = getVal(i); - return ( -
- {ax} - - {typeof v.value === 'number' ? v.value.toFixed(3) : '--'}{v.source === 'sim' && } - -
- ); - })} -
- ))} -
- - {/* 工件轴/主轴 速度倍率负载 */} - 主轴 · SPINDLE -
- (v as number).toFixed(0)} unit="rpm" color={color.status.info} /> - (v as number).toFixed(0)} unit="rpm" color={color.status.success} /> - `${(v as number).toFixed(0)}`} unit="%" color={color.status.accent} /> - (v as number).toFixed(1)} unit="%" color={color.status.warning} /> -
- - {/* 进给 速度倍率负载 */} - 进给 · FEED -
- (v as number).toFixed(0)} unit="mm/min" color={color.status.info} /> - (v as number).toFixed(0)} unit="mm/min" color={color.status.success} /> - `${(v as number).toFixed(0)}`} unit="%" color={color.status.accent} /> - (v as number).toFixed(1)} unit="%" color={color.status.warning} /> -
- - {/* 程序信息 */} - 程序 · PROGRAM -
-
- 程序号 - {f.program.value || '--'}{f.program.source === 'sim' && } -
-
-
- ); -} - -// ===== Main ===== -export default function MachineMonitor() { - const devStatus = useStore((s) => s.devStatus); - const cncLatest = useStore((s) => s.cncLatest); - const washerLatest = useStore((s) => s.washerLatest); - const cncOee = useStore((s) => s.cncOee); - const washerOee = useStore((s) => s.washerOee); - const monitorLoading = useStore((s) => s.monitorLoading); - const fetchMonitor = useStore((s) => s.fetchMonitor); - const refreshMonitor = useStore((s) => s.refreshMonitor); - - // 强制重渲染驱动器(配合 1s tick) - const [, forceUpdate] = useReducer((x) => x + 1, 0); - - useEffect(() => { - fetchMonitor(); - const t = setInterval(refreshMonitor, 5000); - return () => clearInterval(t); - }, [fetchMonitor, refreshMonitor]); - - // 模拟引擎 1s tick,驱动仿真动画(坐标缓动/能耗递增/剩余时间倒数/行号递增) - useEffect(() => { - const unsub = simulator.subscribe(forceUpdate); - const t = setInterval(() => simulator.tick(), 1000); - return () => { unsub(); clearInterval(t); }; - }, []); - - const oeeByMachine = new Map(cncOee.map((o) => [o.machine, o])); - const washerOeeItem = washerOee[0]; - - return ( -
- {/* 4 设备卡片: 3 CNC + 1 清洗机, 2×2 网格统一展示 */} -
- {cncLatest.length === 0 && !monitorLoading ? ( -
- 暂无 CNC 采集数据 -
- ) : ( - cncLatest.map((cnc) => ( - - )) - )} - -
-
- ); -} diff --git a/bj_power_dashboard/src/components/SamplingTrendChart.tsx b/bj_power_dashboard/src/components/SamplingTrendChart.tsx deleted file mode 100644 index 19725b7..0000000 --- a/bj_power_dashboard/src/components/SamplingTrendChart.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { useMemo } from 'react'; -import * as echarts from 'echarts'; -import type { EChartsOption } from 'echarts'; -import { useStore } from '../store/useStore'; -import { color, font } from '../theme/tokens'; -import { useEChart } from '../hooks/useEChart'; - -// 三种 category 的展示顺序与配色(与 tokens.category 对齐,缺省回退中性色) -const CATEGORY_ORDER = ['A6VM107', 'A6VM160', 'A6VM200']; -const catColor = (cat: string) => - (color.category as Record)[cat] || color.status.neutral; - -export default function SamplingTrendChart() { - const trends = useStore((s) => s.samplingTrends); - - const option = useMemo(() => { - if (!trends.length) return null; - - // 聚合:trends 为 30 行(10 日 × 3 category),按日期/category 重组 - const dates = Array.from(new Set(trends.map((t) => t.date))); - const cats = Array.from(new Set(trends.map((t) => t.category))) - .sort((a, b) => CATEGORY_ORDER.indexOf(a) - CATEGORY_ORDER.indexOf(b)); - - // 按 date -> category 建索引,便于取值 - const cell = new Map(); - for (const t of trends) { - cell.set(`${t.date}|${t.category}`, { passed: t.passed, failed: t.failed }); - } - - // 每个 category 两个 series:合格(category 色) + 不合格(红色),堆叠到同一 category - const series: any[] = []; - for (const cat of cats) { - const base = catColor(cat); - series.push({ - name: `${cat} 合格`, - type: 'bar', - stack: cat, - barMaxWidth: 22, - itemStyle: { - color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ - { offset: 0, color: base }, - { offset: 1, color: `${base}66` }, - ]), - borderRadius: [2, 2, 0, 0], - shadowBlur: 6, - shadowColor: `${base}55`, - }, - emphasis: { focus: 'series' as const }, - data: dates.map((d) => cell.get(`${d}|${cat}`)?.passed ?? 0), - }); - series.push({ - name: `${cat} 不合格`, - type: 'bar', - stack: cat, - barMaxWidth: 22, - itemStyle: { - color: color.status.danger, - borderRadius: [2, 2, 0, 0], - }, - emphasis: { focus: 'series' as const }, - data: dates.map((d) => cell.get(`${d}|${cat}`)?.failed ?? 0), - }); - } - - return { - backgroundColor: 'transparent', - tooltip: { - trigger: 'axis', - backgroundColor: 'rgba(8,15,32,0.95)', - borderColor: 'rgba(34,211,238,0.4)', - borderWidth: 1, - textStyle: { color: '#e6f1ff', fontSize: 12, fontFamily: font.body }, - extraCssText: 'box-shadow:0 8px 24px rgba(0,0,0,0.6); backdrop-filter: blur(8px);', - axisPointer: { - type: 'shadow', - shadowStyle: { color: 'rgba(34,211,238,0.06)' }, - }, - formatter: (params: any) => { - const date = params[0]?.axisValue ?? ''; - // 按 category 归并(同一 category 的 合格/不合格 两条) - const byCat = new Map(); - for (const p of params) { - const m = /^(\S+)\s(合格|不合格)$/.exec(p.seriesName); - if (!m) continue; - const [, cat, kind] = m; - const cur = byCat.get(cat) ?? { passed: 0, failed: 0 }; - if (kind === '合格') cur.passed = p.value ?? 0; - else cur.failed = p.value ?? 0; - byCat.set(cat, cur); - } - const rows = Array.from(byCat.entries()) - .map(([cat, v]) => { - const total = v.passed + v.failed; - const yld = total > 0 ? ((v.passed / total) * 100).toFixed(1) : '--'; - const c = catColor(cat); - return ` -
- - ${cat} - 合 ${v.passed} - 废 ${v.failed} - ${yld}% -
`; - }) - .join(''); - const dayTotal = Array.from(byCat.values()).reduce((s, v) => s + v.passed + v.failed, 0); - return ` -
- ${date} 抽检合计 ${dayTotal} 件 -
${rows}`; - }, - }, - legend: { - data: series.map((s) => s.name), - textStyle: { color: color.text.secondary, fontSize: 10, fontFamily: font.body }, - right: 8, top: 4, - itemWidth: 8, itemHeight: 8, itemGap: 8, - icon: 'circle', - }, - grid: { top: 36, right: 12, bottom: 24, left: 36, containLabel: true }, - xAxis: { - type: 'category', - data: dates, - axisLabel: { - color: color.text.tertiary, fontSize: 10, fontFamily: font.mono, - }, - axisLine: { lineStyle: { color: color.border.base } }, - axisTick: { show: false }, - }, - yAxis: { - type: 'value', - minInterval: 1, - axisLabel: { - color: color.text.tertiary, fontSize: 10, fontFamily: font.mono, - }, - axisLine: { show: false }, - axisTick: { show: false }, - splitLine: { lineStyle: { color: color.border.grid, type: 'dashed' } }, - }, - series, - animationDuration: 700, - animationEasing: 'cubicOut', - }; - }, [trends]); - - const ref = useEChart(option); - return
; -} diff --git a/bj_power_dashboard/src/store/useStore.ts b/bj_power_dashboard/src/store/useStore.ts index 12a4cf6..c40fdb2 100644 --- a/bj_power_dashboard/src/store/useStore.ts +++ b/bj_power_dashboard/src/store/useStore.ts @@ -2,10 +2,7 @@ import { create } from 'zustand'; import api, { mesApi } from '../api/client'; import type { OverviewData, EquipmentItem, ProgressItem, - AlarmItem, TrendItem, SamplingOverview, SamplingTrendItem, - InspectionOverview, InspectionTrendItem, - DevStatus, CncDisplay, WasherPoint, CncHistoryPoint, - CncOeeItem, WasherOeeItem, + AlarmItem, TrendItem, } from '../types'; // MES /api/internal/dashboard/overview 响应结构 @@ -58,72 +55,37 @@ async function fromMes(path: string, fallbackPath?: string): Promise Promise; refresh: () => Promise; - fetchMonitor: () => Promise; - refreshMonitor: () => Promise; } export const useStore = create((set) => ({ - // Page 1 state overview: null, equipment: [], progress: [], alarms: [], trends: [], - samplingOverview: null, - samplingTrends: [], - inspectionOverview: null, - inspectionTrends: [], loading: false, - // Page 2 state - devStatus: [], - cncLatest: [], - washerLatest: [], - cncHistory: [], - cncOee: [], - washerOee: [], - monitorLoading: false, - fetchAll: async () => { set({ loading: true }); try { - const [overview, equipment, progress, alarms, trends, cncLatest, samplingOverview, samplingTrends, inspectionOverview, inspectionTrends] = + const [overview, equipment, progress, alarms, trends] = await Promise.all([ fetchOverview(), fromMes('equipment', '/equipment-status').then(v => v ?? []), fromMes('progress', '/production-progress').then(v => v ?? []), fromMes('alarms', '/alarms').then(v => v ?? []), fromMes('trends', '/trends').then(v => v ?? []), - api.get('/cnc-latest').then(r => r.data).catch(() => []), - api.get('/sampling-overview').then(r => r.data).catch(() => null), - api.get('/sampling-trends').then(r => r.data).catch(() => []), - api.get('/inspection-overview').then(r => r.data).catch(() => null), - api.get('/inspection-trends').then(r => r.data).catch(() => []), ]); - set({ overview, equipment, progress, alarms, trends, cncLatest, samplingOverview, samplingTrends, inspectionOverview, inspectionTrends, loading: false }); + set({ overview, equipment, progress, alarms, trends, loading: false }); } catch { set({ loading: false }); } @@ -131,53 +93,18 @@ export const useStore = create((set) => ({ refresh: async () => { try { - const [overview, equipment, alarms, trends, cncLatest, samplingOverview, inspectionOverview] = await Promise.all([ + const [overview, equipment, alarms, trends] = await Promise.all([ fetchOverview(), fromMes('equipment', '/equipment-status'), fromMes('alarms', '/alarms'), fromMes('trends', '/trends'), - api.get('/cnc-latest').then(r => r.data).catch(() => undefined), - api.get('/sampling-overview').then(r => r.data).catch(() => undefined), - api.get('/inspection-overview').then(r => r.data).catch(() => undefined), ]); set((prev) => ({ overview, ...(equipment !== null ? { equipment } : {}), ...(alarms !== null ? { alarms } : {}), ...(trends !== null ? { trends } : {}), - ...(cncLatest !== undefined ? { cncLatest } : {}), - ...(samplingOverview !== undefined ? { samplingOverview } : {}), - ...(inspectionOverview !== undefined ? { inspectionOverview } : {}), })); } catch { /* ignore */ } }, - - fetchMonitor: async () => { - set({ monitorLoading: true }); - try { - const [devStatus, cncLatest, washerLatest, cncOee, washerOee] = await Promise.all([ - api.get('/device-status').then(r => r.data), - api.get('/cnc-latest').then(r => r.data), - api.get('/washer-latest').then(r => r.data), - api.get('/cnc-oee').then(r => r.data), - api.get('/washer-oee').then(r => r.data), - ]); - set({ devStatus, cncLatest, washerLatest, cncOee, washerOee, monitorLoading: false }); - } catch { - set({ monitorLoading: false }); - } - }, - - refreshMonitor: async () => { - try { - const [devStatus, cncLatest, washerLatest, cncOee, washerOee] = await Promise.all([ - api.get('/device-status').then(r => r.data), - api.get('/cnc-latest').then(r => r.data), - api.get('/washer-latest').then(r => r.data), - api.get('/cnc-oee').then(r => r.data), - api.get('/washer-oee').then(r => r.data), - ]); - set({ devStatus, cncLatest, washerLatest, cncOee, washerOee }); - } catch { /* ignore */ } - }, })); diff --git a/bj_power_dashboard/src/utils/machineFields.ts b/bj_power_dashboard/src/utils/machineFields.ts deleted file mode 100644 index 051e874..0000000 --- a/bj_power_dashboard/src/utils/machineFields.ts +++ /dev/null @@ -1,329 +0,0 @@ -/** - * 字段模板与真值合并 - * - * 看板需求清单里每个字段都有"应有"定义。后端 CncDisplay / WasherPoint 只下发了其中一部分, - * 前端按模板逐项核对:后端给了的标 'real',没给的从 simulator 取模拟值标 'sim'。 - * 卡片据此决定是否显示 SIM 角标。 - */ - -import type { CncDisplay, WasherPoint } from '../types'; -import type { SimState } from './simulator'; - -export type Source = 'real' | 'sim'; - -export interface FieldVal { - value: T; - source: Source; -} - -export interface AxisFieldSet { - name: string; - abs: FieldVal; - rel: FieldVal; - mac: FieldVal; - remain: FieldVal; - feedSet: FieldVal; - feedAct: FieldVal; - feedOvr: FieldVal; - feedLoad: FieldVal; -} - -/** CNC 卡片完整字段集(覆盖需求清单全部项) */ -export interface CncFieldSet { - status: FieldVal; - mode: FieldVal; - eStop: FieldVal; - alarmType: FieldVal; - alarmNo: FieldVal; - alarmText: FieldVal; - alarmStart: FieldVal; - alarmEnd: FieldVal; - axes: AxisFieldSet[]; - workSet: FieldVal; - workAct: FieldVal; - workOvr: FieldVal; - workLoad: FieldVal; - feedSet: FieldVal; - feedAct: FieldVal; - feedOvr: FieldVal; - feedLoad: FieldVal; - program: FieldVal; - lineNo: FieldVal; - curLine: FieldVal; - nextLine: FieldVal; - cycleTotal: FieldVal; - cycleRemain: FieldVal; - machStart: FieldVal; - machEnd: FieldVal; - energy: FieldVal; -} - -/** 清洗机卡片完整字段集 */ -export interface WasherFieldSet { - status: FieldVal; - mode: FieldVal; - eStop: FieldVal; - alarmType: FieldVal; - alarmNo: FieldVal; - alarmText: FieldVal; - alarmStart: FieldVal; - alarmEnd: FieldVal; - // 坐标(清洗机真值,按 data_name 取) - axes: AxisFieldSet[]; - workSet: FieldVal; - workAct: FieldVal; - workOvr: FieldVal; - workLoad: FieldVal; - feedSet: FieldVal; - feedAct: FieldVal; - feedOvr: FieldVal; - feedLoad: FieldVal; - program: FieldVal; - lineNo: FieldVal; - curLine: FieldVal; - nextLine: FieldVal; - cycleTotal: FieldVal; - cycleRemain: FieldVal; - machStart: FieldVal; - machEnd: FieldVal; - energy: FieldVal; -} - -// ============ 辅助 ============ - -function mkSim(value: T): FieldVal { - return { value, source: 'sim' }; -} - -function mkReal(value: T): FieldVal { - return { value, source: 'real' }; -} - -function fmtTime(iso: string | null): string { - if (!iso) return '--'; - const d = new Date(iso); - if (isNaN(d.getTime())) return '--'; - const pad = (n: number) => String(n).padStart(2, '0'); - return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; -} - -function fmtDuration(sec: number): string { - if (sec == null || isNaN(sec)) return '--'; - const m = Math.floor(sec / 60); - const s = Math.floor(sec % 60); - return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; -} - -/** - * 解析清洗机报警信息字符串。 - * 采集器(back_cover_dc collector.go)把 Focas ReadSystemAlarm() 拼成 - * "AlarmId:Type:Axis:Message;AlarmId:Type:Axis:Message;..." - * 取第一条拆出 编号/类型/文本。Type→中文映射按 Focas 常规约定。 - */ -interface ParsedAlarm { - no: string; // 报警编号(AlarmId) - type: string; // 报警类型(中文) - text: string; // 报警文本(Message) - hasAlarm: boolean; -} - -const FOCAS_ALARM_TYPE: Record = { - 0: '报警信息', - 1: '系统报警', - 2: '操作信息', - 3: '提示信息', -}; - -function parseWasherAlarm(raw: string | null): ParsedAlarm { - const empty: ParsedAlarm = { no: '', type: '', text: '', hasAlarm: false }; - if (!raw || !raw.trim()) return empty; - // 取第一条(分号分隔) - const first = raw.split(';')[0]; - // 格式 id:type:axis:message,message 可能含冒号,所以只按前3个冒号切 - const parts = first.split(':'); - if (parts.length < 4) { - // 格式不符,当作纯文本 - return { no: '', type: '', text: first, hasAlarm: true }; - } - const id = parts[0]; - const typeNum = parseInt(parts[1], 10); - // message 是第4段之后所有内容(去掉 axis=parts[2]) - const message = parts.slice(3).join(':'); - return { - no: id || '', - type: isNaN(typeNum) ? parts[1] : (FOCAS_ALARM_TYPE[typeNum] ?? `类型${typeNum}`), - text: message || '', - hasAlarm: true, - }; -} - -/** CNC 报警消息是纯文本(OSP alarm_msg),无编号/类型,只有文本 */ -function parseCncAlarm(raw: string | null): ParsedAlarm { - if (!raw || !raw.trim()) return { no: '', type: '', text: '', hasAlarm: false }; - return { no: '', type: '', text: raw, hasAlarm: true }; -} - -// ============ CNC 合并 ============ - -/** - * CNC 真值来源(CncDisplay): - * status / mode / program / alarm / axes[{name,position}](仅工作坐标,即"绝对"语义) / - * runtimes / counters / is_connected - * 缺失项(全部 sim): 急停/报警编号/报警类型/报警开始结束时间/相对坐标/机械坐标/剩余移动量/ - * 工件轴与进给的设定实际倍率负载/行号/当前行程序/下一行程序/循环剩余时间/加工开始结束/能耗 - */ -export function mergeCnc(display: CncDisplay, sim: SimState): CncFieldSet { - // 真实轴名(后端 axes[].name),缺失时用模拟轴名 - const realAxes = display.axes ?? []; - const axisNames = realAxes.length > 0 - ? realAxes.map((a) => a.name || '') - : sim.axes.map((a) => a.name); - - const axes: AxisFieldSet[] = axisNames.map((name, i) => { - const simAxis = sim.axes[i] ?? sim.axes[0]; - const realPos = realAxes[i]?.position; - // 后端"工作坐标当前位置"作为绝对坐标真值(若存在) - const absReal = realPos != null && realPos !== '' ? parseFloat(realPos) : null; - return { - name: name || simAxis.name, - abs: absReal != null && !isNaN(absReal) ? mkReal(absReal) : mkSim(simAxis.abs), - rel: mkSim(simAxis.rel), - mac: mkSim(simAxis.mac), - remain: mkSim(simAxis.remain), - feedSet: mkSim(simAxis.feedSet), - feedAct: mkSim(simAxis.feedAct), - feedOvr: mkSim(simAxis.feedOvr), - feedLoad: mkSim(simAxis.feedLoad), - }; - }); - - return { - status: display.status ? mkReal(display.status) : mkSim(sim.status), - mode: display.mode ? mkReal(display.mode) : mkSim(sim.mode), - eStop: mkSim(sim.eStop), - alarmType: mkSim(sim.alarmType), - alarmNo: mkSim(sim.alarmNo), - alarmText: display.alarm ? mkReal(display.alarm) : mkSim(''), - alarmStart: mkSim(sim.alarmStart), - alarmEnd: mkSim(sim.alarmEnd), - axes, - workSet: mkSim(sim.workSet), - workAct: mkSim(sim.workAct), - workOvr: mkSim(sim.workOvr), - workLoad: mkSim(sim.workLoad), - feedSet: mkSim(sim.axes[0]?.feedSet ?? 0), - feedAct: mkSim(sim.axes[0]?.feedAct ?? 0), - feedOvr: mkSim(sim.axes[0]?.feedOvr ?? 0), - feedLoad: mkSim(sim.axes[0]?.feedLoad ?? 0), - program: display.program ? mkReal(display.program) : mkSim(''), - lineNo: mkSim(sim.lineNo), - curLine: mkSim(sim.curLine), - nextLine: mkSim(sim.nextLine), - cycleTotal: mkSim(sim.cycleTotal), - cycleRemain: mkSim(sim.cycleRemain), - machStart: mkSim(sim.machStart), - machEnd: mkSim(sim.machEnd), - energy: mkSim(sim.energy), - }; -} - -// ============ 清洗机合并 ============ - -const AXIS_LABELS = ['X', 'Y', 'Z']; - -/** 清洗机真值来源(WasherPoint[],按中文 data_name 查找): - * 状态/报警信息(解析出编号/类型/文本)/主轴转速/主轴倍率/进给率/进给倍率/主轴负载/轴负载/ - * 绝对坐标/机械坐标/相对坐标/剩余距离/程序号/当前刀号/运行时间/实际生产数量/预计生产数量/工件尺寸 - * 缺失项(全部 sim): 工作模式/急停/报警开始结束时间/行号/当前行程序/下一行程序/ - * 循环剩余时间/加工开始结束/能耗/各轴设定实际速度倍率负载(轴负载有真值,速度类无) - */ -export function mergeWasher(points: WasherPoint[], sim: SimState): WasherFieldSet { - const byName = new Map(points.map((p) => [p.data_name, p])); - const getNum = (name: string): number | null => { - const p = byName.get(name); - if (!p) return null; - if (p.float_value != null) return p.float_value; - if (p.int_value != null) return p.int_value; - const v = parseFloat(p.value); - return isNaN(v) ? null : v; - }; - const getArr = (name: string): number[] | null => { - const p = byName.get(name); - if (!p) return null; - if (p.array_value && p.array_value.length > 0) return p.array_value; - return null; - }; - const getStr = (name: string): string | null => { - const p = byName.get(name); - if (!p) return null; - return p.value || null; - }; - - // 坐标三系(真值,按 data_name 取数组) - const absArr = getArr('绝对坐标'); - const relArr = getArr('相对坐标'); - const macArr = getArr('机械坐标'); - const remArr = getArr('剩余距离'); - const axisLoads = getArr('轴负载'); - - const axes: AxisFieldSet[] = AXIS_LABELS.map((label, i) => { - const simAxis = sim.axes[i] ?? sim.axes[0]; - return { - name: label, - abs: absArr != null && absArr[i] != null ? mkReal(absArr[i]) : mkSim(simAxis.abs), - rel: relArr != null && relArr[i] != null ? mkReal(relArr[i]) : mkSim(simAxis.rel), - mac: macArr != null && macArr[i] != null ? mkReal(macArr[i]) : mkSim(simAxis.mac), - remain: remArr != null && remArr[i] != null ? mkReal(remArr[i]) : mkSim(simAxis.remain), - feedSet: mkSim(simAxis.feedSet), - feedAct: mkSim(simAxis.feedAct), - feedOvr: mkSim(simAxis.feedOvr), - feedLoad: axisLoads != null && axisLoads[i] != null ? mkReal(axisLoads[i]) : mkSim(simAxis.feedLoad), - }; - }); - - // 速度/倍率/负载:主轴转速/主轴倍率/主轴负载/进给率/进给倍率 为真值 - const spindleSpeed = getNum('主轴转速'); - const spindleOvr = getNum('主轴倍率'); - const spindleLoad = getNum('主轴负载'); - const feedRate = getNum('进给率'); - const feedOvr = getNum('进给倍率'); - const programNo = getStr('程序号'); - - // 报警:清洗机"报警信息"是 "id:type:axis:msg;..." 结构化串,解析取真值 - const alarmRaw = getStr('报警信息'); - const parsedAlarm = parseWasherAlarm(alarmRaw); - - return { - status: getStr('状态') ? mkReal(getStr('状态')!) : mkSim(sim.status), - mode: mkSim(sim.mode), - eStop: mkSim(sim.eStop), - alarmType: parsedAlarm.hasAlarm && parsedAlarm.type ? mkReal(parsedAlarm.type) : mkSim(sim.alarmType), - alarmNo: parsedAlarm.hasAlarm && parsedAlarm.no ? mkReal(parsedAlarm.no) : mkSim(sim.alarmNo), - alarmText: parsedAlarm.hasAlarm ? mkReal(parsedAlarm.text) : mkSim(''), - alarmStart: mkSim(sim.alarmStart), - alarmEnd: mkSim(sim.alarmEnd), - axes, - workSet: spindleSpeed != null ? mkReal(spindleSpeed) : mkSim(sim.workSet), - workAct: spindleSpeed != null ? mkReal(spindleSpeed) : mkSim(sim.workAct), - workOvr: spindleOvr != null ? mkReal(spindleOvr) : mkSim(sim.workOvr), - workLoad: spindleLoad != null ? mkReal(spindleLoad) : mkSim(sim.workLoad), - feedSet: feedRate != null ? mkReal(feedRate) : mkSim(sim.axes[0]?.feedSet ?? 0), - feedAct: feedRate != null ? mkReal(feedRate) : mkSim(sim.axes[0]?.feedAct ?? 0), - feedOvr: feedOvr != null ? mkReal(feedOvr) : mkSim(sim.axes[0]?.feedOvr ?? 0), - feedLoad: mkSim(sim.axes[0]?.feedLoad ?? 0), - program: programNo ? mkReal(programNo) : mkSim(''), - lineNo: mkSim(sim.lineNo), - curLine: mkSim(sim.curLine), - nextLine: mkSim(sim.nextLine), - cycleTotal: mkSim(sim.cycleTotal), - cycleRemain: mkSim(sim.cycleRemain), - machStart: mkSim(sim.machStart), - machEnd: mkSim(sim.machEnd), - energy: mkSim(sim.energy), - }; -} - -export const fmt = { - time: fmtTime, - duration: fmtDuration, -}; diff --git a/bj_power_dashboard/src/utils/simulator.ts b/bj_power_dashboard/src/utils/simulator.ts deleted file mode 100644 index 02c5802..0000000 --- a/bj_power_dashboard/src/utils/simulator.ts +++ /dev/null @@ -1,338 +0,0 @@ -/** - * 模拟数据引擎 Simulator - * - * 看板需求清单里大量字段后端 CncDisplay / WasherPoint 不下发(绝对/相对/机械坐标、 - * 速度倍率负载、行号行程序、循环剩余时间、加工开始结束、急停、能耗、报警编号/类型/ - * 开始结束时间等)。本引擎在前端内部按 ~1s tick 推进仿真,与真实状态联动: - * - RUN: 坐标缓动、进给速度非零、循环剩余时间倒数、行号递增、能耗累加 - * - IDLE/HOLD: 速度归零、坐标静止、循环时间不变 - * - ALM: 报警字段填充、alarmStart 置位、alarmEnd 清空,坐标静止 - * - OFFLINE/未连接: 所有动态冻结、能耗停止累加 - * - * 真实状态由 MachineMonitor 在每次后端刷新(5s)后通过 setState 写入; - * tick 由顶层组件用 setInterval(1000) 驱动并强制重渲染。 - */ - -export type SimStatus = 'RUN' | 'IDLE' | 'ALM' | 'HOLD' | 'OFFLINE' | string; - -export interface SimAxis { - name: string; - abs: number; - rel: number; - mac: number; - remain: number; - feedSet: number; // 进给设定速度 mm/min - feedAct: number; // 进给实际速度 - feedOvr: number; // 进给倍率 % - feedLoad: number; // 进给负载 % -} - -export interface SimState { - axes: SimAxis[]; - workSet: number; // 工件轴(主轴)设定转速 rpm - workAct: number; // 工件轴实际转速 - workOvr: number; // 工件轴倍率 % - workLoad: number; // 工件轴负载 % - mode: string; // 工作模式 - eStop: boolean; - // 程序 - lineNo: number; - curLine: string; - nextLine: string; - cycleTotal: number; // 程序循环时间 秒 - cycleRemain: number; // 程序剩余时间 秒 - machStart: string | null; // 加工开始时间 ISO - machEnd: string | null; // 加工结束时间 ISO - // 报警 - alarmNo: string; - alarmType: string; - alarmStart: string | null; - alarmEnd: string | null; - // 能耗 kWh - energy: number; - // 当前仿真状态 - status: SimStatus; - isConn: boolean; -} - -// NC 行模板池(模拟当前行/下一行程序) -const NC_LINES = [ - 'G01 X100.000 Y50.000 F800', - 'G02 X120.000 Y60.000 R20.000', - 'G03 X80.000 Y40.000 I-10.000 J5.000', - 'G01 Z-5.000 F200', - 'G00 Z20.000', - 'M03 S3000', - 'M08', - 'G01 X150.000 Y80.000 F600', - 'G02 X100.000 Y100.000 R30.000', - 'G00 Z50.000 M09', -]; - -// 报警池(模拟报警编号/类型) -const ALARM_POOL = [ - { no: 'PS0010', type: '程序报警' }, - { no: 'OT0500', type: '超程报警' }, - { no: 'SV0300', type: '伺服报警' }, - { no: 'IO1000', type: 'IO报警' }, - { no: 'EX1001', type: '外部报警' }, -]; - -interface DeviceSim { - status: SimStatus; - mode: string; - program: string; - isConn: boolean; - axes: SimAxis[]; - workSet: number; - workOvr: number; - workLoad: number; - modeReady: string; - lineNo: number; - lineIdx: number; - cycleTotal: number; - cycleRemain: number; - machStart: string | null; - machEnd: string | null; - alarmNo: string; - alarmType: string; - alarmStart: string | null; - alarmEnd: string | null; - energy: number; - eStop: boolean; - prevStatus: SimStatus; - initialized: boolean; -} - -const DEFAULT_AXIS_NAMES = ['X', 'Y', 'Z']; - -/** - * 状态归一化 - 把 CNC(OSP) 与清洗机(Focas) 以及中文状态统一成内部集合。 - * OSP: RUN/ALM/HLD/IDLE ; Focas: RUN/IDLE/HOLD/ALARM/EMERGENCY ; 中文: 运行/报警/暂停/空闲 - * 统一映射到 RUN/IDLE/ALM/HOLD/OFFLINE,供 tickDevice 分支判断。 - */ -function normalizeStatus(raw: SimStatus): SimStatus { - const s = String(raw || '').toUpperCase().trim(); - if (!s) return 'IDLE'; - if (s === 'OFFLINE' || s === '离线' || s === '关机') return 'OFFLINE'; - if (s === 'RUN' || s === '运行' || s === '运转' || s.includes('RUN')) return 'RUN'; - if (s === 'ALM' || s === 'ALARM' || s === 'EMERGENCY' || s === '报警' || s === '故障') return 'ALM'; - if (s === 'HLD' || s === 'HOLD' || s === 'STOP' || s === '暂停' || s === '停止') return 'HOLD'; - if (s === 'IDLE' || s === '空闲' || s === '待机' || s === 'STANDBY') return 'IDLE'; - // 含 RUN 位(如 OSP 的 RUN|TUR)也视为运行 - if (s.includes('RUN')) return 'RUN'; - return 'IDLE'; -} - -function makeAxis(name: string, idx: number): SimAxis { - const base = 50 + idx * 80; - return { - name, - abs: base, - rel: base * 0.4, - mac: base + 200, - remain: 10 + idx * 5, - feedSet: 800, - feedAct: 0, - feedOvr: 100, - feedLoad: 0, - }; -} - -class Simulator { - private devices = new Map(); - private listeners = new Set<() => void>(); - - subscribe(fn: () => void): () => void { - this.listeners.add(fn); - return () => this.listeners.delete(fn); - } - - private notify() { - this.listeners.forEach((fn) => fn()); - } - - /** 顶层组件每秒调用一次,推进仿真并通知订阅者重渲染 */ - tick(): void { - for (const d of this.devices.values()) { - this.tickDevice(d); - } - this.notify(); - } - - /** 写入某设备的真实状态(由后端刷新驱动) */ - setState(machine: string, status: SimStatus, mode: string, program: string, isConn: boolean): void { - let d = this.devices.get(machine); - if (!d) { - d = this.createDevice(machine); - this.devices.set(machine, d); - } - d.prevStatus = d.status; - d.status = isConn ? normalizeStatus(status) : 'OFFLINE'; - d.isConn = isConn; - if (mode) d.modeReady = mode; - if (program) { - // 程序切换时重置行号/循环时间 - if (d.program !== program) { - d.program = program; - d.lineNo = 10 + Math.floor(Math.random() * 100); - d.lineIdx = Math.floor(Math.random() * NC_LINES.length); - d.cycleTotal = 120 + Math.floor(Math.random() * 180); - d.cycleRemain = d.cycleTotal; - d.machStart = new Date().toISOString(); - d.machEnd = null; - } - } - // 状态切换处理 - if (d.status === 'ALM' && d.prevStatus !== 'ALM') { - const a = ALARM_POOL[Math.floor(Math.random() * ALARM_POOL.length)]; - d.alarmNo = a.no; - d.alarmType = a.type; - d.alarmStart = new Date().toISOString(); - d.alarmEnd = null; - } else if (d.status !== 'ALM' && d.prevStatus === 'ALM') { - d.alarmEnd = new Date().toISOString(); - } - } - - /** 读取某设备的完整仿真状态(对外展示用) */ - getState(machine: string, axisNames: string[]): SimState { - let d = this.devices.get(machine); - if (!d) { - d = this.createDevice(machine); - this.devices.set(machine, d); - } - // 对齐轴数与名称 - const names = axisNames.length > 0 ? axisNames : DEFAULT_AXIS_NAMES; - while (d.axes.length < names.length) { - d.axes.push(makeAxis(names[d.axes.length] ?? `#${d.axes.length}`, d.axes.length)); - } - d.axes.forEach((a, i) => { - if (names[i]) a.name = names[i]; - }); - - const workAct = d.status === 'RUN' ? d.workSet * (d.workOvr / 100) : 0; - - return { - axes: d.axes.map((a) => ({ - ...a, - feedAct: d.status === 'RUN' ? a.feedSet * (a.feedOvr / 100) : 0, - feedLoad: d.status === 'RUN' ? a.feedLoad : 0, - })), - workSet: d.workSet, - workAct, - workOvr: d.workOvr, - workLoad: d.status === 'RUN' ? d.workLoad : 0, - mode: d.modeReady || 'AUTO', - eStop: d.eStop, - lineNo: d.lineNo, - curLine: NC_LINES[d.lineIdx % NC_LINES.length], - nextLine: NC_LINES[(d.lineIdx + 1) % NC_LINES.length], - cycleTotal: d.cycleTotal, - cycleRemain: d.cycleRemain, - machStart: d.machStart, - machEnd: d.machEnd, - alarmNo: d.alarmNo, - alarmType: d.alarmType, - alarmStart: d.alarmStart, - alarmEnd: d.alarmEnd, - energy: d.energy, - status: d.status, - isConn: d.isConn, - }; - } - - private createDevice(machine: string): DeviceSim { - const axes = DEFAULT_AXIS_NAMES.map((n, i) => makeAxis(n, i)); - return { - status: 'IDLE', - mode: 'AUTO', - modeReady: 'AUTO', - program: '', - isConn: false, - axes, - workSet: 3000, - workOvr: 100, - workLoad: 35, - lineNo: 0, - lineIdx: 0, - cycleTotal: 180, - cycleRemain: 180, - machStart: null, - machEnd: null, - alarmNo: '', - alarmType: '', - alarmStart: null, - alarmEnd: null, - energy: 0, - eStop: false, - prevStatus: 'IDLE', - initialized: true, - }; - } - - private tickDevice(d: DeviceSim): void { - if (d.status === 'OFFLINE' || !d.isConn) { - // 离线:冻结,负载归零 - d.axes.forEach((a) => { a.feedAct = 0; a.feedLoad = 0; }); - return; - } - - if (d.status === 'RUN') { - // 坐标缓动:每 tick 推进一个小步长,到阈值折返 - d.axes.forEach((a, i) => { - const step = 0.15 + i * 0.05; - a.abs += step; - a.rel += step; - a.mac += step; - if (a.abs > 200 + i * 60) { - a.abs = 50 + i * 80; - a.rel = 20 + i * 30; - } - a.remain = Math.max(0, a.remain - step * 0.5); - if (a.remain <= 0) a.remain = 10 + i * 5; - a.feedAct = a.feedSet * (a.feedOvr / 100); - // 负载在 25-55 间轻微抖动 - a.feedLoad = 30 + Math.sin(Date.now() / 1000 + i) * 8 + (Math.random() - 0.5) * 3; - }); - - // 循环剩余时间倒数 - if (d.cycleRemain > 0) { - d.cycleRemain -= 1; - if (d.cycleRemain <= 0) { - d.cycleRemain = 0; - d.machEnd = new Date().toISOString(); - d.lineNo += 5; - d.lineIdx = (d.lineIdx + 1) % NC_LINES.length; - } - } else { - // 一段停顿后重新开始下一循环 - if (Math.random() < 0.1) { - d.cycleTotal = 120 + Math.floor(Math.random() * 180); - d.cycleRemain = d.cycleTotal; - d.machStart = new Date().toISOString(); - d.machEnd = null; - d.lineNo += 10; - } - } - - // 能耗累加:功率 ~ 5.5kW - d.energy += 5.5 / 3600; - d.workLoad = 30 + Math.sin(Date.now() / 1000) * 8 + (Math.random() - 0.5) * 3; - d.eStop = false; - } else if (d.status === 'ALM') { - // 报警:坐标静止,负载归零 - d.axes.forEach((a) => { a.feedAct = 0; a.feedLoad = 0; }); - d.workLoad = 0; - // 报警状态下能耗小幅累加(待机功耗) - d.energy += 0.8 / 3600; - } else { - // IDLE/HOLD:静止,待机功耗 - d.axes.forEach((a) => { a.feedAct = 0; a.feedLoad = 0; }); - d.workLoad = 0; - d.energy += 0.5 / 3600; - } - } -} - -export const simulator = new Simulator(); diff --git a/bj_power_mes/common/logx/config.go b/bj_power_mes/common/logx/config.go index 9df0e34..b96b87b 100644 --- a/bj_power_mes/common/logx/config.go +++ b/bj_power_mes/common/logx/config.go @@ -5,6 +5,7 @@ type LogConf struct { Format string `json:",default=text"` // 日志格式: text, json TimeFormat string `json:",default=2006-01-02 15:04:05"` // 时间格式 Filename string `json:",default=spcm.log"` // 日志文件名 + Daily bool `json:",default=true"` // 按天存储: 文件名追加日期,跨天自动切换 MaxSize int `json:",default=100"` // 单个日志文件最大大小(MB) MaxBackups int `json:",default=10"` // 保留的旧日志文件数量 MaxAge int `json:",default=30"` // 日志文件保留天数 diff --git a/bj_power_mes/common/logx/daily.go b/bj_power_mes/common/logx/daily.go new file mode 100644 index 0000000..71dc6f2 --- /dev/null +++ b/bj_power_mes/common/logx/daily.go @@ -0,0 +1,97 @@ +package log + +import ( + "io" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "gopkg.in/natefinch/lumberjack.v2" +) + +// dailyWriter 按天滚动的日志写入器: +// 文件名格式 base-2006-01-02.log,跨天自动切换新文件; +// 单日内超 MaxSize 仍由 lumberjack 备份(base-2006-01-02-.log); +// 超过 MaxAge 天的日志文件在跨天时清理。 +type dailyWriter struct { + mu sync.Mutex + conf LogConf + date string + lj *lumberjack.Logger +} + +func newDailyWriter(conf LogConf) *dailyWriter { + return &dailyWriter{conf: conf} +} + +func (w *dailyWriter) filename(date string) string { + ext := filepath.Ext(w.conf.Filename) + base := strings.TrimSuffix(w.conf.Filename, ext) + return base + "-" + date + ext +} + +func (w *dailyWriter) Write(p []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + + today := time.Now().Format("2006-01-02") + if w.lj == nil || today != w.date { + if w.lj != nil { + _ = w.lj.Close() + } + w.date = today + w.lj = &lumberjack.Logger{ + Filename: w.filename(today), + MaxSize: w.conf.MaxSize, + MaxBackups: w.conf.MaxBackups, + MaxAge: w.conf.MaxAge, + Compress: w.conf.Compress, + } + w.cleanup() + } + return w.lj.Write(p) +} + +func (w *dailyWriter) Close() error { + w.mu.Lock() + defer w.mu.Unlock() + if w.lj != nil { + return w.lj.Close() + } + return nil +} + +// cleanup 清理超过 MaxAge 天的按天日志文件 +func (w *dailyWriter) cleanup() { + if w.conf.MaxAge <= 0 { + return + } + dir := filepath.Dir(w.conf.Filename) + ext := filepath.Ext(w.conf.Filename) + base := strings.TrimSuffix(filepath.Base(w.conf.Filename), ext) + entries, err := os.ReadDir(dir) + if err != nil { + return + } + cutoff := time.Now().AddDate(0, 0, -w.conf.MaxAge) + for _, e := range entries { + if e.IsDir() || !strings.HasPrefix(e.Name(), base+"-") || !strings.HasSuffix(e.Name(), ext) { + continue + } + datePart := strings.TrimSuffix(strings.TrimPrefix(e.Name(), base+"-"), ext) + t, err := time.ParseInLocation("2006-01-02", datePart, time.Local) + if err != nil { + continue // 非按天命名的历史备份,交给 lumberjack 策略 + } + if t.Before(cutoff) { + _ = os.Remove(filepath.Join(dir, e.Name())) + } + } +} + +var ( + _ io.WriteCloser = (*dailyWriter)(nil) + _ io.Writer = (*dailyWriter)(nil) +) diff --git a/bj_power_mes/common/logx/log.go b/bj_power_mes/common/logx/log.go index 58aa4b3..785425a 100644 --- a/bj_power_mes/common/logx/log.go +++ b/bj_power_mes/common/logx/log.go @@ -46,16 +46,21 @@ func NewLogger(conf LogConf) *Logger { conf.Filename = filepath.Join(workdir, conf.Filename) } - // 创建lumberjack日志滚动器 - lumberjackLogger := &lumberjack.Logger{ - Filename: conf.Filename, - MaxSize: conf.MaxSize, - MaxBackups: conf.MaxBackups, - MaxAge: conf.MaxAge, - Compress: conf.Compress, - } // 创建写入器 + // 创建日志写入器:默认按天存储;Daily=false 时退回单文件按大小滚动 + var fileWriter io.Writer + if conf.Daily { + fileWriter = newDailyWriter(conf) + } else { + fileWriter = &lumberjack.Logger{ + Filename: conf.Filename, + MaxSize: conf.MaxSize, + MaxBackups: conf.MaxBackups, + MaxAge: conf.MaxAge, + Compress: conf.Compress, + } + } - var writer io.Writer = lumberjackLogger + var writer io.Writer = fileWriter if conf.Console { writer = io.MultiWriter(writer, os.Stdout) } diff --git a/bj_power_mes/etc/bj_power_mes-api.yaml b/bj_power_mes/etc/bj_power_mes-api.yaml index e82b338..8e73459 100644 --- a/bj_power_mes/etc/bj_power_mes-api.yaml +++ b/bj_power_mes/etc/bj_power_mes-api.yaml @@ -20,10 +20,18 @@ Logger: AddSource: true # 是否添加调用源信息 Auth: - AccessSecret: hardman_2024 - AccessExpire: 72000 + AccessSecret: Hardman_2026 + AccessExpire: 86400 RefreshExpire: 144000 +# 项目间内部 API 写死 token +Internal: + Token: Hardman_2026 + +# WMS 内部 API token +Wms: + Token: Hardman_2026 + Database: Host: 127.0.0.1 Port: 5432 diff --git a/bj_power_mes/frontend/src/api/work-order/index.ts b/bj_power_mes/frontend/src/api/work-order/index.ts index e20ee3c..75849b5 100644 --- a/bj_power_mes/frontend/src/api/work-order/index.ts +++ b/bj_power_mes/frontend/src/api/work-order/index.ts @@ -127,6 +127,8 @@ export interface EventLog { jobId?: number workOrderId?: number workpieceNo?: string + workOrderNo?: string + operator?: string equipmentId?: number equipmentName?: string tempSlotNo?: number @@ -326,6 +328,8 @@ export function useWorkOrderApi() { startTime?: string endTime?: string workpieceNo?: string + workOrderNo?: string + operator?: string jobId?: number }): Promise> => { return get({url: `/api/v1/event-logs`, params}) diff --git a/bj_power_mes/frontend/src/constants/event.ts b/bj_power_mes/frontend/src/constants/event.ts index e6236af..e54bf53 100644 --- a/bj_power_mes/frontend/src/constants/event.ts +++ b/bj_power_mes/frontend/src/constants/event.ts @@ -8,4 +8,20 @@ export const EVENT_TYPE_LABELS: Record = { ORDER_CANCELLED: '取消', ORDER_COMPLETED: '工单完工', ALARM_RAISED: '报警', ALARM_ACKED: '确认', MANUAL_ACTION_CREATED: '人工处理', MANUAL_ACTION_RESOLVED: '已解决', -} \ No newline at end of file + TEMP_SLOT_CHANGED: '暂存台', + EQUIPMENT_AVAILABILITY_CHANGED: '设备启停', + // 装配线操作日志 + BOM_SAVED: 'BOM维护', + MATERIAL_REQUESTED: '备料', + PLC_PROCESS_SENT: 'PLC下发', + TORQUE_REPORTED: '拧紧', + SCAN_REPORTED: '扫码报工', + SEMI_FLOW: '半成品', + AGV_TASK_CREATED: 'AGV任务', +} + +// 装配线操作日志事件(工序/备料/拧紧/扫码/流转/搬运) +export const ASSEMBLY_EVENT_TYPES = [ + 'BOM_SAVED', 'MATERIAL_REQUESTED', 'PLC_PROCESS_SENT', + 'TORQUE_REPORTED', 'SCAN_REPORTED', 'SEMI_FLOW', 'AGV_TASK_CREATED', +] diff --git a/bj_power_mes/frontend/src/views/event-log/index.tsx b/bj_power_mes/frontend/src/views/event-log/index.tsx index 6e140d0..a0ad43c 100644 --- a/bj_power_mes/frontend/src/views/event-log/index.tsx +++ b/bj_power_mes/frontend/src/views/event-log/index.tsx @@ -8,7 +8,7 @@ import {TagColor} from '@douyinfe/semi-ui/lib/es/tag' import {EVENT_TYPE_LABELS} from '@/constants/event' // 事件类别分组 -type EventCategory = 'material' | 'process' | 'lifecycle' | 'alarm' | 'manual' +type EventCategory = 'material' | 'process' | 'lifecycle' | 'alarm' | 'manual' | 'assembly' const EVENT_CATEGORY: Record = { LOAD: 'material', UNLOAD: 'material', EXCHANGE: 'material', REPLENISH: 'material', @@ -19,6 +19,9 @@ const EVENT_CATEGORY: Record = { ORDER_CANCELLED: 'lifecycle', ORDER_COMPLETED: 'lifecycle', ALARM_RAISED: 'alarm', ALARM_ACKED: 'alarm', MANUAL_ACTION_CREATED: 'manual', MANUAL_ACTION_RESOLVED: 'manual', + BOM_SAVED: 'assembly', MATERIAL_REQUESTED: 'assembly', PLC_PROCESS_SENT: 'assembly', + TORQUE_REPORTED: 'assembly', SCAN_REPORTED: 'assembly', SEMI_FLOW: 'assembly', + AGV_TASK_CREATED: 'assembly', } const CATEGORY_COLOR: Record = { @@ -27,6 +30,7 @@ const CATEGORY_COLOR: Record = { lifecycle: 'grey', alarm: 'red', manual: 'orange', + assembly: 'cyan', } const EVENT_TYPE_OPTIONS = [ @@ -35,6 +39,10 @@ const EVENT_TYPE_OPTIONS = [ {label: '加工', value: 'MACHINE_START'}, {label: '完工', value: 'MACHINE_DONE'}, {label: '完成', value: 'JOB_COMPLETED'}, {label: '报废', value: 'JOB_SCRAPPED'}, {label: '下线', value: 'JOB_DISCHARGED'}, {label: '报警', value: 'ALARM_RAISED'}, {label: '确认', value: 'ALARM_ACKED'}, + {label: 'BOM维护', value: 'BOM_SAVED'}, {label: '备料', value: 'MATERIAL_REQUESTED'}, + {label: 'PLC下发', value: 'PLC_PROCESS_SENT'}, {label: '拧紧上报', value: 'TORQUE_REPORTED'}, + {label: '扫码报工', value: 'SCAN_REPORTED'}, {label: '半成品流转', value: 'SEMI_FLOW'}, + {label: 'AGV任务', value: 'AGV_TASK_CREATED'}, ] const ENTITY_TYPE_OPTIONS = [ @@ -54,6 +62,8 @@ export default function WorkOrderLogPage() { const [entityType, setEntityType] = useState() const [dateRange, setDateRange] = useState<[Date, Date]>() const [workpieceNo, setWorkpieceNo] = useState() + const [workOrderNo, setWorkOrderNo] = useState() + const [operator, setOperator] = useState() const [panelVisible, setPanelVisible] = useState(false) const [selectedOrderId, setSelectedOrderId] = useState() const [workpiecePanelVisible, setWorkpiecePanelVisible] = useState(false) @@ -65,16 +75,18 @@ export default function WorkOrderLogPage() { setPanelVisible(true) }, []) - const fetchLogs = useCallback(async (page: number, filters?: {eventType?: string; entityType?: string; dateRange?: [Date, Date]; workpieceNo?: string}) => { + const fetchLogs = useCallback(async (page: number, filters?: {eventType?: string; entityType?: string; dateRange?: [Date, Date]; workpieceNo?: string; workOrderNo?: string; operator?: string}) => { setLoading(true) try { - const f = filters ?? {eventType, entityType, dateRange, workpieceNo} + const f = filters ?? {eventType, entityType, dateRange, workpieceNo, workOrderNo, operator} const params: Record = {page, limit: PAGE_SIZE} if (f.eventType) params.eventType = f.eventType if (f.entityType) params.entityType = f.entityType if (f.dateRange?.[0]) params.startTime = moment(f.dateRange[0]).format('YYYY-MM-DD') if (f.dateRange?.[1]) params.endTime = moment(f.dateRange[1]).format('YYYY-MM-DD') if (f.workpieceNo) params.workpieceNo = f.workpieceNo + if (f.workOrderNo) params.workOrderNo = f.workOrderNo + if (f.operator) params.operator = f.operator const result = await orderApi.queryEventLogs(params as Parameters[0]) setLogs(result.data) @@ -87,7 +99,7 @@ export default function WorkOrderLogPage() { } finally { setLoading(false) } - }, [orderApi]) + }, [orderApi, eventType, entityType, dateRange, workpieceNo, workOrderNo, operator]) useEffect(() => { fetchLogs(1) @@ -130,6 +142,18 @@ export default function WorkOrderLogPage() { } }, }, + { + title: '工单号', + dataIndex: 'workOrderNo', + width: 130, + render: (text: string) => text || '-', + }, + { + title: '操作人', + dataIndex: 'operator', + width: 100, + render: (text: string) => text || '-', + }, { title: '设备', dataIndex: 'equipmentName', @@ -182,7 +206,23 @@ export default function WorkOrderLogPage() { showClear onClear={() => setWorkpieceNo(undefined)} /> - + setWorkOrderNo(undefined)} + /> + setOperator(undefined)} + /> +
fetchLogs(page, {eventType, entityType, dateRange, workpieceNo}), + onPageChange: (page: number) => fetchLogs(page, {eventType, entityType, dateRange, workpieceNo, workOrderNo, operator}), }} /> diff --git a/bj_power_mes/internal/config/config.go b/bj_power_mes/internal/config/config.go index 029b64d..96504b3 100644 --- a/bj_power_mes/internal/config/config.go +++ b/bj_power_mes/internal/config/config.go @@ -54,7 +54,7 @@ type Config struct { // WMS 服务(库存系统 C) Wms struct { BaseURL string `json:",default=http://127.0.0.1:8890"` - Token string `json:",default=bj-power-internal-2026"` // X-API-TOKEN 写死内部 token + Token string `json:",default=Hardman_2026"` // X-API-TOKEN 写死内部 token } // 海康 RCS-2000 AGV 调度 @@ -68,7 +68,7 @@ type Config struct { // 项目间内部 API(工位终端 D 等调用)写死 token Internal struct { - Token string `json:",default=bj-power-internal-2026"` + Token string `json:",default=Hardman_2026"` } Mom MomConfig `json:"MOM"` diff --git a/bj_power_mes/internal/eventbus/bus.go b/bj_power_mes/internal/eventbus/bus.go index 5599b13..2b3b2ad 100644 --- a/bj_power_mes/internal/eventbus/bus.go +++ b/bj_power_mes/internal/eventbus/bus.go @@ -54,6 +54,15 @@ const ( // 连接状态事件 EventConnectionStatusUpdated EventType = "CONNECTION_STATUS_UPDATED" + // 装配线操作日志事件(工单/工件/操作步骤/人员可追溯) + EventBomSaved EventType = "BOM_SAVED" + EventMaterialRequested EventType = "MATERIAL_REQUESTED" + EventPlcProcessSent EventType = "PLC_PROCESS_SENT" + EventTorqueReported EventType = "TORQUE_REPORTED" + EventScanReported EventType = "SCAN_REPORTED" + EventSemiFlow EventType = "SEMI_FLOW" + EventAgvTaskCreated EventType = "AGV_TASK_CREATED" + EventAll EventType = "*" ) diff --git a/bj_power_mes/internal/eventbus/events.go b/bj_power_mes/internal/eventbus/events.go index c77ce8a..5c7399d 100644 --- a/bj_power_mes/internal/eventbus/events.go +++ b/bj_power_mes/internal/eventbus/events.go @@ -3,11 +3,28 @@ package eventbus import ( "fmt" "strconv" + "strings" "time" ) // 事件构造辅助函数 +// NewProductionEvent 构建装配线操作日志事件(payload 内可含 +// workOrderNo/orderNo、workpieceNo/sn、operator、stepCode 等键,写入事件日志表) +func NewProductionEvent(eventType EventType, description string, payload map[string]any) Event { + if payload == nil { + payload = map[string]any{} + } + payload["description"] = description + return Event{ + ID: fmt.Sprintf("%s-%d", strings.ToLower(string(eventType)), time.Now().UnixNano()), + Type: eventType, + Source: "production", + Timestamp: time.Now(), + Payload: payload, + } +} + // NewTempSlotChangedEvent 构建暂存台槽位变更事件 func NewTempSlotChangedEvent(slotNo, jobID int, action string) Event { return Event{ diff --git a/bj_power_mes/internal/eventlog/writer.go b/bj_power_mes/internal/eventlog/writer.go index e01f719..d4af521 100644 --- a/bj_power_mes/internal/eventlog/writer.go +++ b/bj_power_mes/internal/eventlog/writer.go @@ -157,6 +157,17 @@ func (w *EventLogWriter) flush(events []eventbus.Event) { if v, ok := e.Payload["workpieceNo"].(string); ok && v != "" { b.SetWorkpieceNo(v) } + if v, ok := e.Payload["sn"].(string); ok && v != "" && e.Payload["workpieceNo"] == nil { + b.SetWorkpieceNo(v) // 工位终端上报以 sn 为工件号 + } + if v, ok := e.Payload["workOrderNo"].(string); ok && v != "" { + b.SetWorkOrderNo(v) + } else if v, ok := e.Payload["orderNo"].(string); ok && v != "" { + b.SetWorkOrderNo(v) + } + if v, ok := e.Payload["operator"].(string); ok && v != "" { + b.SetOperator(v) + } if v, ok := getPayloadInt(e.Payload, "equipmentId"); ok { b.SetEquipmentId(v) } diff --git a/bj_power_mes/internal/logic/production/agvtask.go b/bj_power_mes/internal/logic/production/agvtask.go index c378765..2ed736a 100644 --- a/bj_power_mes/internal/logic/production/agvtask.go +++ b/bj_power_mes/internal/logic/production/agvtask.go @@ -7,6 +7,7 @@ import ( "time" "bj_power_mes/ent/agvtask" + "bj_power_mes/internal/eventbus" "bj_power_mes/internal/sse" "bj_power_mes/internal/svc" "bj_power_mes/internal/types" @@ -81,6 +82,16 @@ func (l *AgvTaskLogic) AgvTaskCreate(req *types.AgvTaskCreateReq) (map[string]an return nil, err } + // 操作日志(AGV 搬运可追溯) + l.svcCtx.EventBus.Publish(l.ctx, eventbus.NewProductionEvent( + eventbus.EventAgvTaskCreated, + fmt.Sprintf("AGV 任务 %s %s→%s [%s]", taskNo, req.DockFrom, req.DockTo, status), + map[string]any{ + "orderNo": req.OrderNo, "taskNo": taskNo, "status": status, + "dockFrom": req.DockFrom, "dockTo": req.DockTo, + "operator": CurrentUserName(l.ctx, l.svcCtx), + })) + if status == "执行中" && sim { go func(id int, orderNo string) { time.Sleep(10 * time.Second) diff --git a/bj_power_mes/internal/logic/production/bom.go b/bj_power_mes/internal/logic/production/bom.go index 6d4cb50..ce35233 100644 --- a/bj_power_mes/internal/logic/production/bom.go +++ b/bj_power_mes/internal/logic/production/bom.go @@ -2,9 +2,11 @@ package production import ( "context" + "fmt" "bj_power_mes/ent" "bj_power_mes/ent/bomitem" + "bj_power_mes/internal/eventbus" "bj_power_mes/internal/svc" "bj_power_mes/internal/types" @@ -81,5 +83,16 @@ func (l *BomLogic) BomSave(req *types.BomSaveReq) error { return err } } - return tx.Commit() + if err := tx.Commit(); err != nil { + return err + } + // 操作日志(BOM 维护可追溯) + l.svcCtx.EventBus.Publish(l.ctx, eventbus.NewProductionEvent( + eventbus.EventBomSaved, + fmt.Sprintf("保存产品 %s 的 BOM(%d 项物料)", req.ProductCode, len(req.Items)), + map[string]any{ + "productCode": req.ProductCode, "itemCount": len(req.Items), + "operator": CurrentUserName(l.ctx, l.svcCtx), + })) + return nil } diff --git a/bj_power_mes/internal/logic/production/material.go b/bj_power_mes/internal/logic/production/material.go index 50b09a5..456a277 100644 --- a/bj_power_mes/internal/logic/production/material.go +++ b/bj_power_mes/internal/logic/production/material.go @@ -7,6 +7,7 @@ import ( "bj_power_mes/ent/materialrequest" "bj_power_mes/ent/workorder" + "bj_power_mes/internal/eventbus" "bj_power_mes/internal/svc" "bj_power_mes/internal/types" wmsstock "bj_power_mes/internal/wmsstock" @@ -128,6 +129,20 @@ func (l *MaterialRequestLogic) MaterialRequestCreate(req *types.MaterialRequestC if err != nil { return nil, err } + + // 操作日志(备料可追溯) + operator := req.Operator + if operator == "" { + operator = CurrentUserName(l.ctx, l.svcCtx) + } + l.svcCtx.EventBus.Publish(l.ctx, eventbus.NewProductionEvent( + eventbus.EventMaterialRequested, + fmt.Sprintf("备料 %s(%s),物料 %d 项", req.OrderNo, status, len(result.Items)), + map[string]any{ + "orderNo": req.OrderNo, "status": status, "allOk": result.AllOk, + "operator": operator, + })) + out := map[string]any{"record": rec} if respText != "" { out["warn"] = respText diff --git a/bj_power_mes/internal/logic/production/plc_send.go b/bj_power_mes/internal/logic/production/plc_send.go index d94fd64..9f36c35 100644 --- a/bj_power_mes/internal/logic/production/plc_send.go +++ b/bj_power_mes/internal/logic/production/plc_send.go @@ -2,9 +2,11 @@ package production import ( "context" + "fmt" "bj_power_mes/ent" "bj_power_mes/ent/plcsendlog" + "bj_power_mes/internal/eventbus" "bj_power_mes/internal/plcproc" "bj_power_mes/internal/sse" "bj_power_mes/internal/svc" @@ -29,6 +31,14 @@ func (l *PlcLogic) PlcSendProcess(req *types.PlcSendProcessReq) (*ent.PlcSendLog if err != nil { return nil, err } + // 操作日志 + l.svcCtx.EventBus.Publish(l.ctx, eventbus.NewProductionEvent( + eventbus.EventPlcProcessSent, + fmt.Sprintf("PLC 下发工序号 %s → %s", req.CodeStr, req.OrderNo), + map[string]any{ + "orderNo": req.OrderNo, "codeStr": req.CodeStr, + "operator": CurrentUserName(l.ctx, l.svcCtx), + })) l.svcCtx.SSEHandler.Emit(sse.EventDashboardUpdate, `{"type":"plc_sent","orderNo":"`+req.OrderNo+`","code":"`+req.CodeStr+`"}`) return rec, nil } diff --git a/bj_power_mes/internal/logic/production/scan.go b/bj_power_mes/internal/logic/production/scan.go index 4a62dd8..2bbbf6f 100644 --- a/bj_power_mes/internal/logic/production/scan.go +++ b/bj_power_mes/internal/logic/production/scan.go @@ -2,10 +2,12 @@ package production import ( "context" + "fmt" "strconv" "bj_power_mes/ent/processdict" "bj_power_mes/ent/scanrecord" + "bj_power_mes/internal/eventbus" "bj_power_mes/internal/sse" "bj_power_mes/internal/svc" "bj_power_mes/internal/types" @@ -58,6 +60,19 @@ func (l *ScanLogic) ScanReport(req *types.ScanReportReq) (map[string]any, error) return nil, err } + operator := req.Operator + if operator == "" { + operator = CurrentUserName(l.ctx, l.svcCtx) + } + // 操作日志(工单/工件/步骤/人员可追溯) + l.svcCtx.EventBus.Publish(l.ctx, eventbus.NewProductionEvent( + eventbus.EventScanReported, + fmt.Sprintf("扫码报工[%s] 工序%s 工件%s", action, req.StepCode, req.Sn), + map[string]any{ + "orderNo": req.OrderNo, "sn": req.Sn, "stepCode": req.StepCode, + "action": action, "dockCode": req.DockCode, "operator": operator, + })) + l.svcCtx.SSEHandler.Emit(sse.EventDashboardUpdate, map[string]any{ "type": "scan_reported", "orderNo": req.OrderNo, "sn": req.Sn, diff --git a/bj_power_mes/internal/logic/production/semiflow.go b/bj_power_mes/internal/logic/production/semiflow.go index 8813581..205a613 100644 --- a/bj_power_mes/internal/logic/production/semiflow.go +++ b/bj_power_mes/internal/logic/production/semiflow.go @@ -6,6 +6,7 @@ import ( "strings" "bj_power_mes/ent/semiflow" + "bj_power_mes/internal/eventbus" "bj_power_mes/internal/sse" "bj_power_mes/internal/svc" "bj_power_mes/internal/types" @@ -85,6 +86,14 @@ func (l *SemiFlowLogic) record(action string, req *types.SemiFlowReq, zone strin if err != nil { return nil, err } + // 操作日志(半成品流转可追溯) + l.svcCtx.EventBus.Publish(l.ctx, eventbus.NewProductionEvent( + eventbus.EventSemiFlow, + fmt.Sprintf("半成品%s 工件%s → %s", map[string]string{"inbound": "入库", "outbound": "出库"}[action], req.Sn, zone), + map[string]any{ + "sn": req.Sn, "orderNo": req.OrderNo, "action": action, + "zoneCode": zone, "completedProcess": req.CompletedProcess, "operator": req.Operator, + })) l.svcCtx.SSEHandler.Emit(sse.EventDashboardUpdate, map[string]any{"type": "semi_flow", "action": action, "sn": req.Sn, "orderNo": req.OrderNo}) return map[string]any{"record": rec}, nil diff --git a/bj_power_mes/internal/logic/production/torque.go b/bj_power_mes/internal/logic/production/torque.go index c8c510e..e8a0451 100644 --- a/bj_power_mes/internal/logic/production/torque.go +++ b/bj_power_mes/internal/logic/production/torque.go @@ -2,8 +2,10 @@ package production import ( "context" + "fmt" "bj_power_mes/ent/torquerecord" + "bj_power_mes/internal/eventbus" "bj_power_mes/internal/sse" "bj_power_mes/internal/svc" "bj_power_mes/internal/types" @@ -60,6 +62,15 @@ func (l *TorqueLogic) TorqueReport(req *types.TorqueReportReq) (map[string]any, return nil, err } saved++ + + // 操作日志(拧紧结果可追溯) + l.svcCtx.EventBus.Publish(l.ctx, eventbus.NewProductionEvent( + eventbus.EventTorqueReported, + fmt.Sprintf("拧紧上报 %s 螺栓%s %s 扭矩%.1f°角度%.1f", rec.Sn, rec.ScrewNo, result, rec.Torque, rec.Angle), + map[string]any{ + "workOrderNo": rec.WorkOrderNo, "sn": rec.Sn, "dockCode": rec.DockCode, + "screwNo": rec.ScrewNo, "result": result, "operator": rec.Operator, + })) } l.svcCtx.SSEHandler.Emit(sse.EventDashboardUpdate, diff --git a/bj_power_mes/internal/logic/reports/query_event_logs_logic.go b/bj_power_mes/internal/logic/reports/query_event_logs_logic.go index c03ec6c..d374b21 100644 --- a/bj_power_mes/internal/logic/reports/query_event_logs_logic.go +++ b/bj_power_mes/internal/logic/reports/query_event_logs_logic.go @@ -46,6 +46,12 @@ func (l *QueryEventLogsLogic) QueryEventLogs(req *types.QueryEventLogsReq) (resp if req.WorkpieceNo != "" { conditions = append(conditions, enteventlog.WorkpieceNoContains(req.WorkpieceNo)) } + if req.WorkOrderNo != "" { + conditions = append(conditions, enteventlog.WorkOrderNoContains(req.WorkOrderNo)) + } + if req.Operator != "" { + conditions = append(conditions, enteventlog.OperatorContains(req.Operator)) + } if req.JobId > 0 { conditions = append(conditions, enteventlog.JobId(req.JobId)) } @@ -87,6 +93,8 @@ func (l *QueryEventLogsLogic) QueryEventLogs(req *types.QueryEventLogsReq) (resp CreatedAt: lg.CreatedAt.Format(time.RFC3339), JobId: int(lg.JobId), WorkpieceNo: lg.WorkpieceNo, + WorkOrderNo: lg.WorkOrderNo, + Operator: lg.Operator, EquipmentId: int(lg.EquipmentId), EquipmentName: lg.EquipmentName, TempSlotNo: int(lg.TempSlotNo), diff --git a/bj_power_mes/internal/types/types.go b/bj_power_mes/internal/types/types.go index e373d73..7933070 100644 --- a/bj_power_mes/internal/types/types.go +++ b/bj_power_mes/internal/types/types.go @@ -1010,6 +1010,8 @@ type EventLog struct { CreatedAt string `json:"createdAt"` JobId int `json:"jobId,optional"` WorkpieceNo string `json:"workpieceNo,optional"` + WorkOrderNo string `json:"workOrderNo,optional"` + Operator string `json:"operator,optional"` EquipmentId int `json:"equipmentId,optional"` EquipmentName string `json:"equipmentName,optional"` TempSlotNo int `json:"tempSlotNo,optional"` @@ -1027,6 +1029,8 @@ type QueryEventLogsReq struct { StartTime string `form:"startTime,optional"` EndTime string `form:"endTime,optional"` WorkpieceNo string `form:"workpieceNo,optional"` + WorkOrderNo string `form:"workOrderNo,optional"` + Operator string `form:"operator,optional"` JobId int `form:"jobId,optional"` } diff --git a/bj_power_mes/schema/event_log.go b/bj_power_mes/schema/event_log.go index f8deedd..c73786a 100644 --- a/bj_power_mes/schema/event_log.go +++ b/bj_power_mes/schema/event_log.go @@ -32,6 +32,8 @@ func (EventLog) Fields() []ent.Field { field.Int64("entityVersion").Default(0).Comment("实体版本号"), field.Int("jobId").Optional().Comment("关联工件ID"), field.String("workpieceNo").MaxLen(64).Default("").Comment("工件编号"), + field.String("workOrderNo").MaxLen(64).Default("").Comment("关联工单号"), + field.String("operator").MaxLen(64).Default("").Comment("操作人"), field.Int("equipmentId").Optional().Comment("关联设备ID"), field.String("equipmentName").MaxLen(64).Default("").Comment("设备名称"), field.Int("tempSlotNo").Optional().Comment("暂存台槽位号"), @@ -51,6 +53,8 @@ func (EventLog) Indexes() []ent.Index { index.Fields("createdAt"), index.Fields("entityType", "entityId"), index.Fields("jobId"), + index.Fields("workOrderNo"), + index.Fields("operator"), index.Fields("equipmentId"), } } diff --git a/bj_power_mes/schema/tools/migrate.go b/bj_power_mes/schema/tools/migrate.go index ef15864..25f3b07 100644 --- a/bj_power_mes/schema/tools/migrate.go +++ b/bj_power_mes/schema/tools/migrate.go @@ -5,31 +5,42 @@ package main import ( "context" "database/sql" + "flag" + "fmt" "log" "bj_power_mes/ent" + "bj_power_mes/internal/db" "entgo.io/ent/dialect" entsql "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/schema" _ "github.com/jackc/pgx/v5/stdlib" + "github.com/zeromicro/go-zero/core/conf" ) +// 建表工具:DSN 一律从 etc 配置读取,不再硬编码 +// 用法(在 bj_power_mes 目录下):go run schema/tools/migrate.go + var ( client *ent.Client - ctx = context.Background() + ctx = context.Background() + configFile = flag.String("config", "etc/bj_power_mes-api.yaml", "配置文件路径") ) func setUp() { - db, err := sql.Open("pgx", "postgresql://postgres:postgres@127.0.0.1:5432/back_cover") - //db, err := sql.Open("pgx", "postgresql://postgres:hardman@192.168.1.125:5432/back_cover") + var c db.DatabaseConf + conf.MustLoad(*configFile, &c) + + dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", + c.Host, c.Port, c.User, c.Password, c.Dbname) + db, err := sql.Open("pgx", dsn) if err != nil { log.Fatal(err) } - _, err = db.Query("select 1") - if err != nil { + if _, err = db.Query("select 1"); err != nil { log.Fatal(err) } @@ -45,6 +56,7 @@ func tearDown() { } func main() { + flag.Parse() setUp() defer tearDown() diff --git a/bj_power_wms/bj_power_wms.go b/bj_power_wms/bj_power_wms.go index e64552d..a27171f 100644 --- a/bj_power_wms/bj_power_wms.go +++ b/bj_power_wms/bj_power_wms.go @@ -6,6 +6,7 @@ import ( "bj_power_wms/internal/config" "bj_power_wms/internal/handler" + "bj_power_wms/internal/logdaily" "bj_power_wms/internal/svc" "github.com/zeromicro/go-zero/core/conf" @@ -21,6 +22,14 @@ func main() { var c config.Config conf.MustLoad(*configFile, &c) + // 日志按天存储:logs/bj_power_wms-2026-01-02.log,保留 30 天 + if c.Log.Mode == "file" { + logx.SetUp(c.Log) + w := logdaily.New(c.Log.Path, c.Name, 30) + defer w.Close() + logx.SetWriter(logx.NewWriter(w)) + } + server := rest.MustNewServer(c.RestConf) defer server.Stop() diff --git a/bj_power_wms/etc/bj_power_wms-api.yaml b/bj_power_wms/etc/bj_power_wms-api.yaml index 57a422f..5cf9b06 100644 --- a/bj_power_wms/etc/bj_power_wms-api.yaml +++ b/bj_power_wms/etc/bj_power_wms-api.yaml @@ -14,12 +14,12 @@ Postgres: # JWT(短有效期 + 滑动续签) Auth: - AccessSecret: bj-power-wms-secret-2026 + AccessSecret: Hardman_2026 AccessExpire: 1800 # 项目间内部 API 写死 token Internal: - Token: bj-power-internal-2026 + Token: Hardman_2026 Log: Mode: file diff --git a/bj_power_wms/internal/logdaily/daily.go b/bj_power_wms/internal/logdaily/daily.go new file mode 100644 index 0000000..dddc008 --- /dev/null +++ b/bj_power_wms/internal/logdaily/daily.go @@ -0,0 +1,91 @@ +// Package logdaily 提供按天滚动的日志写入器(供 go-zero logx.SetWriter 使用) +package logdaily + +import ( + "io" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +type dailyWriter struct { + mu sync.Mutex + dir string + base string + keepDays int + date string + f *os.File +} + +// New 创建按天滚动的写入器:文件名 dir/base-2006-01-02.log,跨天自动切换, +// 超过 keepDays 天的旧日志在跨天时清理(keepDays<=0 表示永久保留)。 +func New(dir, base string, keepDays int) io.WriteCloser { + return &dailyWriter{dir: dir, base: base, keepDays: keepDays} +} + +func (w *dailyWriter) filename(date string) string { + return filepath.Join(w.dir, w.base+"-"+date+".log") +} + +func (w *dailyWriter) Write(p []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + + today := time.Now().Format("2006-01-02") + if w.f == nil || today != w.date { + if w.f != nil { + _ = w.f.Close() + } + if err := os.MkdirAll(w.dir, 0o755); err != nil { + return 0, err + } + f, err := os.OpenFile(w.filename(today), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) + if err != nil { + return 0, err + } + w.f = f + w.date = today + w.cleanup() + } + return w.f.Write(p) +} + +func (w *dailyWriter) Close() error { + w.mu.Lock() + defer w.mu.Unlock() + if w.f != nil { + return w.f.Close() + } + return nil +} + +func (w *dailyWriter) cleanup() { + if w.keepDays <= 0 { + return + } + entries, err := os.ReadDir(w.dir) + if err != nil { + return + } + cutoff := time.Now().AddDate(0, 0, -w.keepDays) + for _, e := range entries { + if e.IsDir() || !strings.HasPrefix(e.Name(), w.base+"-") || !strings.HasSuffix(e.Name(), ".log") { + continue + } + datePart := strings.TrimSuffix(strings.TrimPrefix(e.Name(), w.base+"-"), ".log") + t, err := time.ParseInLocation("2006-01-02", datePart, time.Local) + if err != nil { + continue + } + if t.Before(cutoff) { + _ = os.Remove(filepath.Join(w.dir, e.Name())) + } + } +} + +var ( + _ io.WriteCloser = (*dailyWriter)(nil) + _ io.Writer = (*dailyWriter)(nil) +) diff --git a/bj_power_workstation/etc/bj_power_workstation.yaml b/bj_power_workstation/etc/bj_power_workstation.yaml index 66aef5e..037fbe9 100644 --- a/bj_power_workstation/etc/bj_power_workstation.yaml +++ b/bj_power_workstation/etc/bj_power_workstation.yaml @@ -4,14 +4,14 @@ Port: 8892 Timeout: 30000 Auth: - AccessSecret: bj-power-ws-secret-2026 + AccessSecret: Hardman_2026 AccessExpire: 1800 Mes: BaseURL: http://127.0.0.1:8888 Internal: - Token: bj-power-internal-2026 + Token: Hardman_2026 Sim: Enable: true diff --git a/bj_power_workstation/internal/config/config.go b/bj_power_workstation/internal/config/config.go index 6467477..9780ba0 100644 --- a/bj_power_workstation/internal/config/config.go +++ b/bj_power_workstation/internal/config/config.go @@ -7,7 +7,7 @@ import ( // AuthConfig JWT 登录鉴权配置 // 注意:go-zero conf 仅识别 json 标签(yaml/toml 文件统一转为 JSON 后按此映射) type AuthConfig struct { - AccessSecret string `json:",default=bj-power-ws-secret-2026"` + AccessSecret string `json:",default=Hardman_2026"` AccessExpire int64 `json:",default=1800"` // 有效期(秒) } @@ -18,7 +18,7 @@ type MesConfig struct { // InternalConfig 项目间内部 API 写死 token(X-API-TOKEN) type InternalConfig struct { - Token string `json:",default=bj-power-internal-2026"` + Token string `json:",default=Hardman_2026"` } // SimConfig 内置模拟拧紧枪数据源开关 diff --git a/部署手册.md b/部署手册.md index 727ade4..99be39d 100644 --- a/部署手册.md +++ b/部署手册.md @@ -19,7 +19,7 @@ - Dashboard 通过 MES 内部接口获取数据(Redis 缓存 60s 过期),业务变更经 SSE `dashboard_update` 实时推送。 - 工位终端拧紧数据本地 SQLite 缓存,点"完成"才上报 MES。 -内部 API 统一 token:`X-API-TOKEN: bj-power-internal-2026`(各 etc/*.yaml 中 `Internal.Token` / `Wms.Token` 可改)。 +内部 API 统一 token:`X-API-TOKEN: Hardman_2026`(各 etc/*.yaml 中 `Internal.Token` / `Wms.Token` 可改,各项目 JWT AccessSecret 也统一为该值)。 ## 二、准备 @@ -128,7 +128,7 @@ server { location /api/internal/ { proxy_pass http://127.0.0.1:8888; - proxy_set_header X-API-TOKEN "bj-power-internal-2026"; + proxy_set_header X-API-TOKEN "Hardman_2026"; } location /sse { proxy_pass http://127.0.0.1:8888; @@ -155,7 +155,7 @@ server { | 检查项 | 方法 | |--------|------| | MES 健康 | 浏览器开 http://127.0.0.1:8888 能看到登录页,admin/123456 登录成功 | -| WMS 健康 | `curl -H "X-API-TOKEN: bj-power-internal-2026" http://127.0.0.1:8890/api/internal/stock/overview` 返回 JSON | +| WMS 健康 | `curl -H "X-API-TOKEN: Hardman_2026" http://127.0.0.1:8890/api/internal/stock/overview` 返回 JSON | | MES→WMS 联通 | MES 建 BOM 后发起备料,观察库存检查结果 | | 内部接口 | 同上 header 访问 `http://127.0.0.1:8888/api/internal/dashboard/overview` | | SSE 推送 | 看板打开后操作工单下发,看板秒级刷新(DevTools Network → sse 连接 pending) |