feat: 完成 issue #178 [D1] 样式对齐(上)——驾驶舱+对话助手硬编码色改 vben 主题 token(54 处 hsl(var(--*))),Page 容器
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 对话助手(Epic #163 / issue #169 [B3])
|
||||
* 对齐旧 web/chat/assistant.js:
|
||||
* - 消息列表 + 输入区(AntD)
|
||||
* - infer.ts chat completions + 模型选择(/v1/models)
|
||||
* - citations.json 引用来源展示(PRD 5.4 溯源)
|
||||
* - 网关离线/dry-run 降级提示
|
||||
* 对话助手(issue #178 [D1] 样式对齐)
|
||||
* 样式全部改 vben 主题 token(明暗主题自适应),Page 容器。
|
||||
*/
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { ask, fetchHealth, fetchModels } from '#/api/iaop/infer';
|
||||
|
||||
interface Msg {
|
||||
@@ -64,7 +62,6 @@ async function send() {
|
||||
const r = await ask(q, { model: model.value, maxTokens: 512 });
|
||||
const meta = (r.meta || {}) as Record<string, unknown>;
|
||||
offline.value = Boolean(meta.dry_run);
|
||||
// 引用溯源:按关键词在 citations 文档中检索(演示:命中即附来源)
|
||||
const hits = Object.keys(citations.value).filter((k) =>
|
||||
q.includes(k) || k.split('·')[0].length > 0 && q.includes(k.split('·')[0]),
|
||||
).slice(0, 3);
|
||||
@@ -80,18 +77,15 @@ async function send() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-page">
|
||||
<div class="chat-head">
|
||||
<h1>iAOP 对话助手</h1>
|
||||
<div class="chat-opts">
|
||||
<Page title="iAOP 对话助手" description="工艺问答 · LLM 网关(PRD 5.4 溯源)" class="chat-page">
|
||||
<template #extra>
|
||||
<a-select v-model:value="model" style="width: 220px" placeholder="模型">
|
||||
<a-select-option v-for="m in models" :key="m" :value="m">{{ m }}</a-select-option>
|
||||
</a-select>
|
||||
<a-tag :color="offline ? 'red' : 'green'">
|
||||
{{ offline ? '网关离线 / dry-run' : '网关在线' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-alert v-if="offline" type="warning" show-icon style="margin-bottom: 12px">
|
||||
<template #message>推理服务离线或 dry-run 占位:回答可能为占位内容(未连接上游推理服务)。</template>
|
||||
@@ -119,37 +113,20 @@ async function send() {
|
||||
/>
|
||||
<a-button type="primary" :loading="loading" @click="send">发送</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chat-page {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
.chat-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.chat-head h1 {
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
}
|
||||
.chat-opts {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
height: calc(100vh - 170px);
|
||||
}
|
||||
.chat-list {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
background: hsl(var(--muted) / 0.6);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
@@ -166,16 +143,17 @@ async function send() {
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: hsl(var(--card));
|
||||
color: hsl(var(--card-foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
}
|
||||
.role-user .bubble {
|
||||
background: #1677ff;
|
||||
color: #fff;
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border-color: transparent;
|
||||
}
|
||||
.bubble-loading {
|
||||
color: #999;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
.citations {
|
||||
margin-top: 6px;
|
||||
@@ -183,8 +161,8 @@ async function send() {
|
||||
.cit-tag {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
color: #1677ff;
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 0.08);
|
||||
border-radius: 10px;
|
||||
padding: 2px 8px;
|
||||
margin-right: 6px;
|
||||
|
||||
+9
-11
@@ -1,8 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 告警面板(issue #167 [B1])
|
||||
* 对齐旧 cockpit.js AlarmPanel:alarm_panel.ti.json 驱动
|
||||
* (severity 配色 / SOP / 确认),一期演示数据 + 确认流转(本地状态)。
|
||||
* 告警面板(issue #178 [D1] 样式对齐)
|
||||
* severity 配色改 vben 主题 token:P0→destructive / P1→warning / P2→primary。
|
||||
*/
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
@@ -19,12 +18,10 @@ const alerts = ref<Array<{ id: number; severity: string; title: string; device:
|
||||
{ id: 3, severity: 'P2', title: '蒸馏塔液位波动增大', device: 'D-03', time: '08:12', acked: false },
|
||||
]);
|
||||
|
||||
const severityColors = computed<Record<string, string>>(
|
||||
() => (config.value.severityStyles as Record<string, Record<string, string>>) || {},
|
||||
);
|
||||
const colorOf = (sev: string): string => {
|
||||
const c = severityColors.value[sev];
|
||||
return (c && (c.color || c.background)) || (sev === 'P0' ? '#ff4d4f' : sev === 'P1' ? '#faad14' : '#1677ff');
|
||||
if (sev === 'P0') return 'hsl(var(--destructive))';
|
||||
if (sev === 'P1') return 'hsl(var(--warning))';
|
||||
return 'hsl(var(--primary))';
|
||||
};
|
||||
|
||||
async function ack(id: number) {
|
||||
@@ -60,16 +57,17 @@ onMounted(async () => {
|
||||
.w-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: hsl(var(--card-foreground));
|
||||
}
|
||||
.alarm-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px dashed rgba(0, 0, 0, 0.08);
|
||||
border-bottom: 1px dashed hsl(var(--border));
|
||||
}
|
||||
.sev {
|
||||
color: #fff;
|
||||
color: hsl(var(--primary-foreground));
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
padding: 1px 6px;
|
||||
@@ -80,7 +78,7 @@ onMounted(async () => {
|
||||
font-size: 13px;
|
||||
}
|
||||
.alarm-title small {
|
||||
color: var(--cockpit-fg-muted, rgba(0, 0, 0, 0.45));
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-left: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
+14
-14
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* KPI 卡片(issue #167 [B1])
|
||||
* 对齐旧 cockpit.js KpiCard:props.kpi {label, value, unit, thresholds},
|
||||
* 按阈值着色(ok / warn / alarm)。
|
||||
* KPI 卡片(issue #178 [D1] 样式对齐)
|
||||
* 阈值配色保留语义,色值取自 vben 主题 token:
|
||||
* ok→primary / warn→warning / alarm→destructive。
|
||||
*/
|
||||
import { computed } from 'vue';
|
||||
|
||||
@@ -47,7 +47,7 @@ const level = computed<'ok' | 'warn' | 'alarm'>(() => {
|
||||
}
|
||||
.kpi-label {
|
||||
font-size: 13px;
|
||||
color: var(--cockpit-fg-muted, rgba(0, 0, 0, 0.45));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
.kpi-value {
|
||||
font-size: 24px;
|
||||
@@ -58,25 +58,25 @@ const level = computed<'ok' | 'warn' | 'alarm'>(() => {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
margin-left: 4px;
|
||||
color: var(--cockpit-fg-muted, rgba(0, 0, 0, 0.45));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
.kpi-level {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.kpi-ok {
|
||||
color: #1677ff;
|
||||
border-color: rgba(22, 119, 255, 0.35);
|
||||
background: rgba(22, 119, 255, 0.05);
|
||||
color: hsl(var(--primary));
|
||||
border-color: hsl(var(--primary) / 0.35);
|
||||
background: hsl(var(--primary) / 0.06);
|
||||
}
|
||||
.kpi-warn {
|
||||
color: #faad14;
|
||||
border-color: rgba(250, 173, 20, 0.5);
|
||||
background: rgba(250, 173, 20, 0.08);
|
||||
color: hsl(var(--warning));
|
||||
border-color: hsl(var(--warning) / 0.5);
|
||||
background: hsl(var(--warning) / 0.1);
|
||||
}
|
||||
.kpi-alarm {
|
||||
color: #ff4d4f;
|
||||
border-color: rgba(255, 77, 79, 0.5);
|
||||
background: rgba(255, 77, 79, 0.08);
|
||||
color: hsl(var(--destructive));
|
||||
border-color: hsl(var(--destructive) / 0.5);
|
||||
background: hsl(var(--destructive) / 0.08);
|
||||
}
|
||||
</style>
|
||||
|
||||
+6
-4
@@ -1,8 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* NL 查询(issue #168 [B2])
|
||||
* 驾驶舱内联 NL 查询:输入 → infer.ts ask()(/v1/chat/completions)→ 展示回答。
|
||||
* 网关离线(dry-run 占位/失败)给出降级提示。
|
||||
* NL 查询(issue #178 [D1] 样式对齐)
|
||||
* 输入/回答区样式使用 vben 主题 token。
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
@@ -57,6 +56,7 @@ async function submit() {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: hsl(var(--card-foreground));
|
||||
}
|
||||
.nl-row {
|
||||
display: flex;
|
||||
@@ -64,7 +64,9 @@ async function submit() {
|
||||
}
|
||||
.nl-answer {
|
||||
margin-top: 10px;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
|
||||
+17
-19
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 四状态工艺流程视图(issue #167 [B1])
|
||||
* 对齐旧 cockpit.js ProcessView:stages 来自布局资产 props.stages,
|
||||
* 状态色走 .state-* 四类(running/warning/alarm/offline),一期轮流着色。
|
||||
* 四状态工艺流程视图(issue #178 [D1] 样式对齐)
|
||||
* 状态色取自 vben 主题 token:running→primary / warning→warning /
|
||||
* alarm→destructive / offline→muted,随明暗主题自适应。
|
||||
*/
|
||||
import { computed } from 'vue';
|
||||
|
||||
@@ -31,7 +31,6 @@ const stageState = (idx: number): string => stateList[idx % stateList.length];
|
||||
<template>
|
||||
<div class="process-view">
|
||||
<div class="w-title">{{ props?.src ? '四状态工艺流程(' + props.src + ')' : '四状态工艺流程' }}</div>
|
||||
<div class="w-body">
|
||||
<div class="process-flow">
|
||||
<template v-if="stages.length">
|
||||
<template v-for="(s, i) in stages" :key="i">
|
||||
@@ -48,14 +47,13 @@ const stageState = (idx: number): string => stateList[idx % stateList.length];
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.w-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: var(--cockpit-fg, rgba(0, 0, 0, 0.88));
|
||||
color: hsl(var(--card-foreground));
|
||||
}
|
||||
.process-flow {
|
||||
display: flex;
|
||||
@@ -83,27 +81,27 @@ const stageState = (idx: number): string => stateList[idx % stateList.length];
|
||||
margin-top: 4px;
|
||||
}
|
||||
.stage-arrow {
|
||||
color: var(--cockpit-fg-muted, #999);
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 18px;
|
||||
}
|
||||
.state-running {
|
||||
color: #1677ff;
|
||||
border-color: #1677ff;
|
||||
background: rgba(22, 119, 255, 0.06);
|
||||
color: hsl(var(--primary));
|
||||
border-color: hsl(var(--primary) / 0.4);
|
||||
background: hsl(var(--primary) / 0.08);
|
||||
}
|
||||
.state-warning {
|
||||
color: #faad14;
|
||||
border-color: #faad14;
|
||||
background: rgba(250, 173, 20, 0.08);
|
||||
color: hsl(var(--warning));
|
||||
border-color: hsl(var(--warning) / 0.5);
|
||||
background: hsl(var(--warning) / 0.1);
|
||||
}
|
||||
.state-alarm {
|
||||
color: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
background: rgba(255, 77, 79, 0.08);
|
||||
color: hsl(var(--destructive));
|
||||
border-color: hsl(var(--destructive) / 0.5);
|
||||
background: hsl(var(--destructive) / 0.08);
|
||||
}
|
||||
.state-offline {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
border-color: rgba(0, 0, 0, 0.25);
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
color: hsl(var(--muted-foreground));
|
||||
border-color: hsl(var(--muted));
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
}
|
||||
</style>
|
||||
|
||||
+5
-6
@@ -1,9 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 实时趋势图(issue #168 [B2])
|
||||
* 原生 SVG 折线(零依赖,对齐旧 cockpit.js canvas 曲线):
|
||||
* props.series 点位名(如 CLF-01.TEMP),种子数据 + 2s 滚动刷新,
|
||||
* 接总线后替换为实时点位数据源。
|
||||
* 实时趋势图(issue #178 [D1] 样式对齐)
|
||||
* 原生 SVG 折线(零依赖),线条/文字用 vben 主题 token。
|
||||
*/
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
@@ -62,8 +60,8 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
<div class="trend-chart">
|
||||
<div class="w-title">{{ props?.title || `实时趋势(${series})` }}</div>
|
||||
<svg :width="W" :height="H" viewBox="0 0 320 90" preserveAspectRatio="none">
|
||||
<polyline :points="polyline" fill="none" stroke="var(--cockpit-accent, #1677ff)" stroke-width="2" />
|
||||
<text x="4" y="12" font-size="11" fill="var(--cockpit-fg-muted, #999)">
|
||||
<polyline :points="polyline" fill="none" stroke="hsl(var(--primary))" stroke-width="2" />
|
||||
<text x="4" y="12" font-size="11" fill="hsl(var(--muted-foreground))">
|
||||
bind: {{ series }} · 2s 滚动
|
||||
</text>
|
||||
</svg>
|
||||
@@ -75,6 +73,7 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: hsl(var(--card-foreground));
|
||||
}
|
||||
.trend-chart svg {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 驾驶舱主页面(Epic #163 / issue #167 [B1])
|
||||
* 布局对齐旧 web/cockpit/cockpit.js:模板 JSON(RenderPlan)驱动网格渲染,
|
||||
* widget 类型分发到子组件(process_view / kpi_card / alarm_panel;
|
||||
* trend / nl_query 由 B2 提供,此处占位)。
|
||||
* 驾驶舱主页面(issue #178 [D1] 样式对齐)
|
||||
* 模板 JSON(RenderPlan)驱动网格渲染,widget 类型分发到子组件。
|
||||
* 样式全部使用 vben 主题 token(hsl(var(--*))),随明/暗主题自适应。
|
||||
*/
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { loadCockpitPlan } from '#/api/iaop/templates';
|
||||
|
||||
import AlarmPanel from './components/AlarmPanel.vue';
|
||||
@@ -23,7 +24,6 @@ interface PlanWidget {
|
||||
}
|
||||
|
||||
const planTitle = ref('海绵钛车间驾驶舱');
|
||||
const themeTokens = ref<Record<string, string>>({});
|
||||
const widgets = ref<PlanWidget[]>([]);
|
||||
const loading = ref(true);
|
||||
const errorMsg = ref('');
|
||||
@@ -34,12 +34,12 @@ onMounted(async () => {
|
||||
try {
|
||||
const plan = await loadCockpitPlan('ti-cl4');
|
||||
planTitle.value = plan.title || planTitle.value;
|
||||
themeTokens.value = plan.themeTokens || {};
|
||||
widgets.value = (plan.widgets as unknown as PlanWidget[]) || [];
|
||||
// 主题 token 注入 CSS 变量(切模板 = 换一套变量)
|
||||
const root = document.documentElement;
|
||||
Object.entries(themeTokens.value).forEach(([k, v]) => root.style.setProperty(k, v));
|
||||
// 2s 轮询推理服务状态(对齐旧版徽标)
|
||||
Object.entries((plan.themeTokens as Record<string, string>) || {}).forEach(([k, v]) =>
|
||||
root.style.setProperty(k, v),
|
||||
);
|
||||
timer = window.setInterval(refreshHealth, 2000);
|
||||
await refreshHealth();
|
||||
} catch {
|
||||
@@ -64,68 +64,34 @@ async function refreshHealth() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cockpit-page">
|
||||
<div class="cockpit-header">
|
||||
<h1>{{ planTitle }}</h1>
|
||||
<div class="cockpit-meta">
|
||||
<Page :title="planTitle" description="海绵钛(Ti)行业模板 · 模板 JSON 驱动">
|
||||
<template #extra>
|
||||
<a-tag :color="inferOnline ? 'green' : 'red'">
|
||||
{{ inferOnline ? '● 推理服务在线' : '○ 推理服务离线' }}
|
||||
</a-tag>
|
||||
<a-tag>行业模板:海绵钛(Ti)</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<a-alert v-if="errorMsg" type="error" :message="errorMsg" style="margin-bottom: 12px" />
|
||||
<div
|
||||
class="cockpit-grid"
|
||||
:style="{ gridTemplateColumns: `repeat(${widgets[0]?.grid?.style ? '12' : '12'}, 1fr)` }"
|
||||
>
|
||||
<div class="cockpit-grid">
|
||||
<template v-for="(w, idx) in widgets" :key="idx">
|
||||
<div
|
||||
class="cockpit-widget"
|
||||
:style="w.grid?.style || `grid-column: span 12`"
|
||||
>
|
||||
<div class="cockpit-widget" :style="w.grid?.style || 'grid-column: span 12'">
|
||||
<ProcessFlow v-if="w.type === 'process_view'" :props="w.props" />
|
||||
<KpiCard v-else-if="w.type === 'kpi_card'" :props="w.props" />
|
||||
<AlarmPanel v-else-if="w.type === 'alarm_panel'" :props="w.props" />
|
||||
<TrendChart v-else-if="w.type === 'trend'" :props="w.props" />
|
||||
<NlQuery v-else-if="w.type === 'nl_query'" :props="w.props" />
|
||||
<a-card
|
||||
v-else
|
||||
size="small"
|
||||
:title="w.props?.title || w.description || '未知组件'"
|
||||
>
|
||||
<a-card v-else size="small" :title="w.props?.title || w.description || '未知组件'">
|
||||
<a-empty description="未知 widget 类型" />
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cockpit-page {
|
||||
padding: 8px;
|
||||
background: var(--cockpit-bg, #f0f2f5);
|
||||
min-height: calc(100vh - 140px);
|
||||
}
|
||||
.cockpit-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.cockpit-header h1 {
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
color: var(--cockpit-fg, rgba(0, 0, 0, 0.88));
|
||||
}
|
||||
.cockpit-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.cockpit-grid {
|
||||
display: grid;
|
||||
grid-auto-rows: minmax(60px, auto);
|
||||
@@ -133,9 +99,11 @@ async function refreshHealth() {
|
||||
}
|
||||
.cockpit-widget {
|
||||
min-width: 0;
|
||||
background: var(--cockpit-surface, #fff);
|
||||
background: hsl(var(--card));
|
||||
color: hsl(var(--card-foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 1px 2px hsl(var(--foreground) / 0.08);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user