feat: 完成 issue #179 [D2] 样式对齐(下)——studio/admin 10 页接入 vben 体系(Page+vxe-grid+vben-form+iconify),硬编码色/emoji 清零
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
/** 告警确认(issue #173 [B7]):severity 配色 + 确认 + LLM 解释(infer.ts) */
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ask } from '#/api/iaop/infer';
|
||||
|
||||
const alerts = ref([
|
||||
@@ -12,39 +15,54 @@ const alerts = ref([
|
||||
const sevColor: Record<string, string> = { P0: 'red', P1: 'orange', P2: 'blue' };
|
||||
const explain = ref('');
|
||||
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
rowConfig: { keyField: 'id' },
|
||||
height: 'auto',
|
||||
stripe: true,
|
||||
columns: [
|
||||
{ title: '级别', field: 'severity', width: 90, slots: { default: 'severity' } },
|
||||
{ title: '告警', field: 'title', minWidth: 260 },
|
||||
{ title: '点位', field: 'device', width: 110 },
|
||||
{ title: '状态', field: 'status', width: 110, slots: { default: 'status' } },
|
||||
{ title: '操作', field: 'action', width: 210, slots: { default: 'action' } },
|
||||
],
|
||||
proxy: {
|
||||
query: async ({ page }) => ({
|
||||
items: alerts.value.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize),
|
||||
total: alerts.value.length,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
async function explainAlert(a: { title: string }) {
|
||||
try {
|
||||
const r = await ask(`请解释告警并给出处置建议:${a.title}`, { maxTokens: 200 });
|
||||
explain.value = r.text;
|
||||
} catch { explain.value = '推理服务不可用,无法生成解释。'; }
|
||||
} catch {
|
||||
explain.value = '推理服务不可用,无法生成解释。';
|
||||
}
|
||||
}
|
||||
function ack(a: { status: string }) {
|
||||
a.status = 'acked';
|
||||
}
|
||||
const ack = (a: { status: string }) => (a.status = 'acked');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 style="font-size: 18px">告警确认(PRD 8.1 Alert 状态机)</h1>
|
||||
<a-table :data-source="alerts" size="small" :pagination="false"
|
||||
:columns="[
|
||||
{ title: '级别', dataIndex: 'severity', key: 'severity', width: 80 },
|
||||
{ title: '告警', dataIndex: 'title', key: 'title' },
|
||||
{ title: '点位', dataIndex: 'device', key: 'device', width: 100 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||
{ title: '操作', key: 'op', width: 210 },
|
||||
]">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'severity'">
|
||||
<a-tag :color="sevColor[record.severity]">{{ record.severity }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 'acked' ? 'green' : 'orange'">{{ record.status }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'op'">
|
||||
<a-button size="small" @click="explainAlert(record)">LLM 解释</a-button>
|
||||
<a-button size="small" type="primary" style="margin-left: 6px" :disabled="record.status === 'acked'" @click="ack(record)">确认</a-button>
|
||||
</template>
|
||||
<Page title="告警确认" description="PRD 8.1 · Alert 状态机 / LLM 解释">
|
||||
<Grid>
|
||||
<template #severity="{ row }">
|
||||
<a-tag :color="sevColor[row.severity]">{{ row.severity }}</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
<template #status="{ row }">
|
||||
<a-tag :color="row.status === 'acked' ? 'green' : 'orange'">{{ row.status }}</a-tag>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<VbenButton size="small" @click="explainAlert(row)">LLM 解释</VbenButton>
|
||||
<VbenButton size="small" variant="solid" :disabled="row.status === 'acked'" @click="ack(row)">确认</VbenButton>
|
||||
</template>
|
||||
</Grid>
|
||||
<a-alert v-if="explain" type="info" style="margin-top: 12px" :message="explain" />
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
/** 审计查询(issue #173 [B7]):操作日志列表 + 筛选 */
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
const logs = ref([
|
||||
{ time: '2026-08-06 09:10', user: 'admin', resource: '模型', action: 'promote', result: 'ok', detail: 'quality_forecast v2.1.0 → prod' },
|
||||
{ time: '2026-08-06 08:55', user: 'admin', resource: '模板', action: 'publish', result: 'ok', detail: 'Ti 模板 v1.0.0 发布' },
|
||||
@@ -9,30 +13,51 @@ const logs = ref([
|
||||
{ time: '2026-08-06 08:12', user: 'viewer1', resource: '驾驶舱', action: 'view', result: 'ok', detail: '只读查看' },
|
||||
]);
|
||||
const kw = ref('');
|
||||
|
||||
const filtered = () => {
|
||||
const k = kw.value.trim();
|
||||
return k ? logs.value.filter((l) => JSON.stringify(l).includes(k)) : logs.value;
|
||||
};
|
||||
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
rowConfig: { keyField: 'time' },
|
||||
height: 'auto',
|
||||
stripe: true,
|
||||
columns: [
|
||||
{ title: '时间', field: 'time', width: 160 },
|
||||
{ title: '操作者', field: 'user', width: 110 },
|
||||
{ title: '资源', field: 'resource', width: 100 },
|
||||
{ title: '动作', field: 'action', width: 100 },
|
||||
{ title: '结果', field: 'result', width: 90, slots: { default: 'result' } },
|
||||
{ title: '详情', field: 'detail', minWidth: 240 },
|
||||
],
|
||||
proxy: {
|
||||
query: async ({ page }) => {
|
||||
const rows = filtered();
|
||||
return {
|
||||
items: rows.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize),
|
||||
total: rows.length,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
function onSearch() {
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 style="font-size: 18px">审计查询(PRD 9 安全-数据)</h1>
|
||||
<a-input v-model:value="kw" placeholder="筛选:用户 / 资源 / 动作" style="max-width: 320px; margin-bottom: 12px" allow-clear />
|
||||
<a-table :data-source="filtered()" size="small" :pagination="{ pageSize: 10 }"
|
||||
:columns="[
|
||||
{ title: '时间', dataIndex: 'time', key: 'time', width: 150 },
|
||||
{ title: '操作者', dataIndex: 'user', key: 'user', width: 100 },
|
||||
{ title: '资源', dataIndex: 'resource', key: 'resource', width: 90 },
|
||||
{ title: '动作', dataIndex: 'action', key: 'action', width: 90 },
|
||||
{ title: '结果', dataIndex: 'result', key: 'result', width: 80 },
|
||||
{ title: '详情', dataIndex: 'detail', key: 'detail' },
|
||||
]">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'result'">
|
||||
<a-tag :color="record.result === 'ok' ? 'green' : 'red'">{{ record.result }}</a-tag>
|
||||
</template>
|
||||
<Page title="审计查询" description="PRD 9 · 安全-数据 / 操作日志">
|
||||
<template #extra>
|
||||
<a-input v-model:value="kw" placeholder="筛选:用户 / 资源 / 动作" style="width: 280px" allow-clear @change="onSearch" @press-enter="onSearch" />
|
||||
</template>
|
||||
<Grid>
|
||||
<template #result="{ row }">
|
||||
<a-tag :color="row.result === 'ok' ? 'green' : 'red'">{{ row.result }}</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -1,24 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
/** 管理控制台目录页(issue #172/#173 [B6/B7]):4 子模块入口 */
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { BxsFile, FlatColorIconsFolder, MaterialSymbolsEdit, MingcuteProfileLine } from '@vben/icons';
|
||||
|
||||
const entries = [
|
||||
{ path: '/iaop/admin/models', icon: '🧠', title: '模型管理', desc: '模型模板版本 / 阶段提升 / 回滚' },
|
||||
{ path: '/iaop/admin/kb', icon: '📚', title: '知识库管理', desc: 'RAG 文档 / 章节 / 索引状态' },
|
||||
{ path: '/iaop/admin/alerts', icon: '🚨', title: '告警确认', desc: 'severity 告警 / LLM 解释 / 确认' },
|
||||
{ path: '/iaop/admin/audit', icon: '📜', title: '审计查询', desc: '操作日志 / 筛选 / 结果状态' },
|
||||
{ path: '/iaop/admin/models', icon: MaterialSymbolsEdit, title: '模型管理', desc: '模型模板版本 / 阶段提升 / 回滚' },
|
||||
{ path: '/iaop/admin/kb', icon: BxsFile, title: '知识库管理', desc: 'RAG 文档 / 章节 / 索引状态' },
|
||||
{ path: '/iaop/admin/alerts', icon: MingcuteProfileLine, title: '告警确认', desc: 'severity 告警 / LLM 解释 / 确认' },
|
||||
{ path: '/iaop/admin/audit', icon: FlatColorIconsFolder, title: '审计查询', desc: '操作日志 / 筛选 / 结果状态' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 style="font-size: 18px">管理控制台(PRD 5.3 / 5.4 / 8.1 / 9)</h1>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; margin-top: 16px">
|
||||
<Page title="管理控制台" description="PRD 5.3 / 5.4 / 8.1 / 9 · 模型 · 知识库 · 告警 · 审计">
|
||||
<div class="entry-grid">
|
||||
<a-card v-for="e in entries" :key="e.path" hoverable size="small">
|
||||
<router-link :to="e.path" style="text-decoration: none; color: inherit">
|
||||
<div style="font-size: 26px">{{ e.icon }}</div>
|
||||
<div style="font-weight: 600; margin-top: 6px">{{ e.title }}</div>
|
||||
<div style="font-size: 12px; color: #888; margin-top: 4px">{{ e.desc }}</div>
|
||||
<router-link :to="e.path" class="entry-link">
|
||||
<component :is="e.icon" class="size-8" />
|
||||
<div class="entry-title">{{ e.title }}</div>
|
||||
<div class="entry-desc">{{ e.desc }}</div>
|
||||
</router-link>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.entry-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.entry-link {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: hsl(var(--card-foreground));
|
||||
}
|
||||
.entry-title {
|
||||
font-weight: 600;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.entry-desc {
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,33 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
/** 知识库管理(issue #172 [B6]):citations.json 文档列表 + 索引状态 */
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
const docs = ref<Array<{ title: string; sections: number; indexed: boolean }>>([]);
|
||||
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
rowConfig: { keyField: 'title' },
|
||||
height: 'auto',
|
||||
stripe: true,
|
||||
columns: [
|
||||
{ title: '文档', field: 'title', minWidth: 260 },
|
||||
{ title: '章节数', field: 'sections', width: 120 },
|
||||
{ title: '索引状态', field: 'indexed', width: 140, slots: { default: 'indexed' } },
|
||||
],
|
||||
proxy: {
|
||||
query: async ({ page }) => ({
|
||||
items: docs.value.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize),
|
||||
total: docs.value.length,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const resp = await fetch(`${import.meta.env.BASE_URL}iaop/citations.json`);
|
||||
const d = await resp.json();
|
||||
const docsObj = (d as { documents?: Record<string, string[]> }).documents || {};
|
||||
docs.value = Object.entries(docsObj).map(([title, secs]) => ({ title, sections: secs.length, indexed: true }));
|
||||
} catch { docs.value = []; }
|
||||
} catch {
|
||||
docs.value = [];
|
||||
}
|
||||
gridApi.query();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 style="font-size: 18px">知识库管理(RAG / PRD 5.4)</h1>
|
||||
<a-table :data-source="docs" size="small" :pagination="false"
|
||||
:columns="[
|
||||
{ title: '文档', dataIndex: 'title', key: 'title' },
|
||||
{ title: '章节数', dataIndex: 'sections', key: 'sections' },
|
||||
{ title: '索引状态', dataIndex: 'indexed', key: 'indexed' },
|
||||
]">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'indexed'">
|
||||
<a-tag :color="record.indexed ? 'green' : 'red'">{{ record.indexed ? '已索引' : '待索引' }}</a-tag>
|
||||
</template>
|
||||
<Page title="知识库管理" description="PRD 5.4 · RAG 文档 / 章节 / 索引状态">
|
||||
<Grid>
|
||||
<template #indexed="{ row }">
|
||||
<a-tag :color="row.indexed ? 'green' : 'red'">{{ row.indexed ? '已索引' : '待索引' }}</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
/** 模型管理(issue #172 [B6]):模型模板列表 + 版本/阶段 + 提升/回滚 */
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
const models = ref([
|
||||
{ name: 'quality_forecast', version: 'v2.1.0', stage: 'prod', desc: 'TiCl4 纯度预测(GBDT)' },
|
||||
{ name: 'quality_forecast', version: 'v2.2.0', stage: 'staging', desc: 'TiCl4 纯度预测(DNN)' },
|
||||
@@ -9,30 +13,48 @@ const models = ref([
|
||||
{ name: 'cross_process_optimizer', version: 'v1.1.0', stage: 'dev', desc: '跨工序寻优' },
|
||||
]);
|
||||
|
||||
const promote = (m: { stage: string }) => { if (m.stage === 'staging') m.stage = 'prod'; else if (m.stage === 'dev') m.stage = 'staging'; };
|
||||
const rollback = (m: { stage: string }) => { if (m.stage === 'prod') m.stage = 'staging'; };
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
rowConfig: { keyField: 'name' },
|
||||
height: 'auto',
|
||||
stripe: true,
|
||||
columns: [
|
||||
{ title: '模型', field: 'name', minWidth: 180 },
|
||||
{ title: '版本', field: 'version', width: 120 },
|
||||
{ title: '阶段', field: 'stage', width: 110, slots: { default: 'stage' } },
|
||||
{ title: '说明', field: 'desc', minWidth: 220 },
|
||||
{ title: '操作', field: 'action', width: 170, slots: { default: 'action' } },
|
||||
],
|
||||
proxy: {
|
||||
query: async ({ page }) => ({
|
||||
items: models.value.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize),
|
||||
total: models.value.length,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
function promote(m: { stage: string }) {
|
||||
if (m.stage === 'staging') m.stage = 'prod';
|
||||
else if (m.stage === 'dev') m.stage = 'staging';
|
||||
gridApi.query();
|
||||
}
|
||||
function rollback(m: { stage: string }) {
|
||||
if (m.stage === 'prod') m.stage = 'staging';
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 style="font-size: 18px">模型管理(PRD 5.3 模板注册表)</h1>
|
||||
<a-table :data-source="models" size="small" :pagination="false"
|
||||
:columns="[
|
||||
{ title: '模型', dataIndex: 'name', key: 'name' },
|
||||
{ title: '版本', dataIndex: 'version', key: 'version' },
|
||||
{ title: '阶段', dataIndex: 'stage', key: 'stage' },
|
||||
{ title: '说明', dataIndex: 'desc', key: 'desc' },
|
||||
{ title: '操作', key: 'op' },
|
||||
]">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'stage'">
|
||||
<a-tag :color="record.stage === 'prod' ? 'green' : record.stage === 'staging' ? 'orange' : 'blue'">{{ record.stage }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'op'">
|
||||
<a-button size="small" type="primary" :disabled="record.stage === 'prod'" @click="promote(record)">提升</a-button>
|
||||
<a-button size="small" style="margin-left: 6px" :disabled="record.stage !== 'prod'" @click="rollback(record)">回滚</a-button>
|
||||
</template>
|
||||
<Page title="模型管理" description="PRD 5.3 · 模板注册表 / 版本阶段流转">
|
||||
<Grid>
|
||||
<template #stage="{ row }">
|
||||
<a-tag :color="row.stage === 'prod' ? 'green' : row.stage === 'staging' ? 'orange' : 'blue'">{{ row.stage }}</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
<template #action="{ row }">
|
||||
<VbenButton size="small" :disabled="row.stage === 'prod'" @click="promote(row)">提升</VbenButton>
|
||||
<VbenButton size="small" variant="outline" :disabled="row.stage !== 'prod'" @click="rollback(row)">回滚</VbenButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -1,50 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
/** 模型超参配置(issue #170 [B4]):4 类模型 + 表单 + JSON 预览 */
|
||||
import { computed, ref } from 'vue';
|
||||
import { h, ref } from 'vue';
|
||||
|
||||
import { Page, VbenButton } from '@vben/common-ui';
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
const kinds = ['质量预测', '工艺优化', '异常检测', '跨工序寻优'];
|
||||
const kind = ref(kinds[0]);
|
||||
const params = ref({
|
||||
learning_rate: 0.01, epochs: 100, batch_size: 64,
|
||||
feature_window: 'PT30M', quality_tag: 'TiCl4_purity', backbone: 'gbdt',
|
||||
});
|
||||
|
||||
const json = computed(() => JSON.stringify({ template: 'ti-cl4', kind: kind.value, hyperparams: params.value }, null, 2));
|
||||
const preview = ref('');
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
layout: 'vertical',
|
||||
schema: [
|
||||
{ component: 'Select', componentProps: { options: kinds.map((k) => ({ label: k, value: k })) }, fieldName: 'kind', label: '模型类型', defaultValue: '质量预测' },
|
||||
{ component: 'InputNumber', fieldName: 'learning_rate', label: '学习率 learning_rate', defaultValue: 0.01 },
|
||||
{ component: 'InputNumber', fieldName: 'epochs', label: '训练轮数 epochs', defaultValue: 100 },
|
||||
{ component: 'InputNumber', fieldName: 'batch_size', label: '批大小 batch_size', defaultValue: 64 },
|
||||
{ component: 'Input', fieldName: 'feature_window', label: '特征窗口 feature_window', defaultValue: 'PT30M' },
|
||||
{ component: 'Input', fieldName: 'quality_tag', label: '质量标签 quality_tag', defaultValue: 'TiCl4_purity' },
|
||||
{ component: 'Select', componentProps: { options: [{ label: 'GBDT', value: 'gbdt' }, { label: 'DNN', value: 'dnn' }, { label: 'iForest', value: 'iforest' }, { label: 'LOF', value: 'lof' }] }, fieldName: 'backbone', label: '主干 backbone', defaultValue: 'gbdt' },
|
||||
],
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2',
|
||||
handleSubmit: async (values) => {
|
||||
preview.value = JSON.stringify({ template: 'ti-cl4', hyperparams: values }, null, 2);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 style="font-size: 18px">模型超参配置(PRD 5.3 超参包 JSON)</h1>
|
||||
<a-radio-group v-model:value="kind" button-style="solid">
|
||||
<a-radio-button v-for="k in kinds" :key="k" :value="k">{{ k }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
<div style="display: flex; gap: 24px; margin-top: 16px">
|
||||
<a-form layout="vertical" style="max-width: 420px; flex: 1">
|
||||
<a-form-item label="学习率 learning_rate">
|
||||
<a-input-number v-model:value="params.learning_rate" :step="0.001" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="训练轮数 epochs">
|
||||
<a-input-number v-model:value="params.epochs" :min="1" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="批大小 batch_size">
|
||||
<a-input-number v-model:value="params.batch_size" :min="1" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="特征窗口 feature_window"><a-input v-model:value="params.feature_window" /></a-form-item>
|
||||
<a-form-item label="质量标签 quality_tag"><a-input v-model:value="params.quality_tag" /></a-form-item>
|
||||
<a-form-item label="主干 backbone">
|
||||
<a-select v-model:value="params.backbone">
|
||||
<a-select-option value="gbdt">GBDT</a-select-option>
|
||||
<a-select-option value="dnn">DNN</a-select-option>
|
||||
<a-select-option value="iforest">iForest</a-select-option>
|
||||
<a-select-option value="lof">LOF</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-button type="primary">保存超参包</a-button>
|
||||
</a-form>
|
||||
<div style="flex: 1">
|
||||
<div style="font-size: 13px; color: #888; margin-bottom: 6px">超参包 JSON 实时预览</div>
|
||||
<pre style="background: #0f172a; color: #7dd3fc; border-radius: 8px; padding: 12px; font-size: 12px; overflow: auto">{{ json }}</pre>
|
||||
<Page title="模型超参配置" description="PRD 5.3 · 超参包 JSON(vben-form)">
|
||||
<div class="hyper-layout">
|
||||
<Form class="hyper-form" />
|
||||
<div class="hyper-preview">
|
||||
<div class="preview-title">超参包 JSON 实时预览</div>
|
||||
<pre class="preview-body">{{ preview || '提交表单后此处生成超参包 JSON' }}</pre>
|
||||
<VbenButton type="submit" variant="solid" @click="formApi.submitForm">保存超参包</VbenButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hyper-layout {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
.hyper-form {
|
||||
flex: 1;
|
||||
max-width: 560px;
|
||||
}
|
||||
.hyper-preview {
|
||||
flex: 1;
|
||||
}
|
||||
.preview-title {
|
||||
font-size: 13px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.preview-body {
|
||||
background: hsl(var(--muted) / 0.6);
|
||||
color: hsl(var(--foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
overflow: auto;
|
||||
max-height: 360px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
/** 点位字典导入(issue #170 [B4]):上传 CSV -> 解析预览 + 校验错误列表 */
|
||||
import { ref } from 'vue';
|
||||
|
||||
const preview = ref<Array<Record<string, string>>>([]);
|
||||
const errors = ref<string[]>([]);
|
||||
const fileName = ref('');
|
||||
import { Page, VbenButton } from '@vben/common-ui';
|
||||
import { BxsFile } from '@vben/icons';
|
||||
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
const HEADERS = ['device_id', 'point_id', 'name', 'unit', 'dataType', 'sampleRate', 'qualityCode', 'opcNode', 'protocol'];
|
||||
|
||||
const errors = ref<string[]>([]);
|
||||
const fileName = ref('');
|
||||
const preview = ref<Array<Record<string, string>>>([]);
|
||||
|
||||
const gridOptions: VxeTableGridOptions<Record<string, string>> = {
|
||||
rowConfig: { keyField: 'point_id' },
|
||||
height: 'auto',
|
||||
stripe: true,
|
||||
columns: HEADERS.map((h) => ({ title: h, field: h, minWidth: 90 })),
|
||||
proxy: {
|
||||
query: async ({ page }) => ({
|
||||
items: preview.value.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize),
|
||||
total: preview.value.length,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
function onFile(e: Event) {
|
||||
const el = e.target as HTMLInputElement;
|
||||
const file = el.files?.[0];
|
||||
@@ -35,18 +55,19 @@ function parseCsv(text: string) {
|
||||
if (!(Number(row.sampleRate) > 0)) errors.value.push(`第 ${i + 1} 行 sampleRate 非法`);
|
||||
preview.value.push(row);
|
||||
}
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 style="font-size: 18px">点位字典导入(PRD 5.1)</h1>
|
||||
<a-upload :show-upload-list="false" :before-upload="() => false" @change="onFile">
|
||||
<a-button type="primary">选择 CSV 文件</a-button>
|
||||
</a-upload>
|
||||
<div v-if="fileName" style="margin-top: 8px"><a-tag color="blue">{{ fileName }}</a-tag></div>
|
||||
<Page title="点位字典导入" description="PRD 5.1 · CSV 上传 → 校验 → 预览">
|
||||
<template #extra>
|
||||
<a-upload :show-upload-list="false" :before-upload="() => false" @change="onFile">
|
||||
<VbenButton variant="solid"><BxsFile class="size-4" />选择 CSV 文件</VbenButton>
|
||||
</a-upload>
|
||||
</template>
|
||||
<a-tag v-if="fileName" color="blue">{{ fileName }}</a-tag>
|
||||
<a-alert v-for="(e, i) in errors" :key="i" type="error" :message="e" style="margin-top: 6px" />
|
||||
<a-table v-if="preview.length" style="margin-top: 12px" :data-source="preview.slice(0, 20)"
|
||||
:columns="HEADERS.map((h) => ({ title: h, dataIndex: h, key: h }))" :pagination="{ pageSize: 10 }" size="small" />
|
||||
</div>
|
||||
<Grid />
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -1,24 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
/** 模板配置台目录页(issue #170/#171 [B4/B5]):4 子模块入口 */
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { BxsFile, FlatColorIconsFolder, MaterialSymbolsAdd, MaterialSymbolsEdit } from '@vben/icons';
|
||||
|
||||
const entries = [
|
||||
{ path: '/iaop/studio/import', icon: '📥', title: '点位导入', desc: '点位字典 CSV 上传 / 校验 / 预览' },
|
||||
{ path: '/iaop/studio/hyper', icon: '⚙️', title: '模型超参', desc: '4 类模型超参包配置(JSON 预览)' },
|
||||
{ path: '/iaop/studio/layout', icon: '🧩', title: '驾驶舱编排', desc: '组件清单排序 / 启用 / 导出布局' },
|
||||
{ path: '/iaop/studio/version', icon: '🏷️', title: '版本发布', desc: '模板版本列表 / 发布 / 阶段' },
|
||||
{ path: '/iaop/studio/import', icon: BxsFile, title: '点位导入', desc: '点位字典 CSV 上传 / 校验 / 预览' },
|
||||
{ path: '/iaop/studio/hyper', icon: MaterialSymbolsAdd, title: '模型超参', desc: '4 类模型超参包配置(JSON 预览)' },
|
||||
{ path: '/iaop/studio/layout', icon: MaterialSymbolsEdit, title: '驾驶舱编排', desc: '组件清单排序 / 启用 / 导出布局' },
|
||||
{ path: '/iaop/studio/version', icon: FlatColorIconsFolder, title: '版本发布', desc: '模板版本列表 / 发布 / 阶段' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 style="font-size: 18px">模板配置台(PRD 5.1 / 5.3 / 5.7)</h1>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; margin-top: 16px">
|
||||
<Page title="模板配置台" description="PRD 5.1 / 5.3 / 5.7 · 点位导入 · 模型超参 · 布局编排 · 版本发布">
|
||||
<div class="entry-grid">
|
||||
<a-card v-for="e in entries" :key="e.path" hoverable size="small">
|
||||
<router-link :to="e.path" style="text-decoration: none; color: inherit">
|
||||
<div style="font-size: 26px">{{ e.icon }}</div>
|
||||
<div style="font-weight: 600; margin-top: 6px">{{ e.title }}</div>
|
||||
<div style="font-size: 12px; color: #888; margin-top: 4px">{{ e.desc }}</div>
|
||||
<router-link :to="e.path" class="entry-link">
|
||||
<component :is="e.icon" class="size-8" />
|
||||
<div class="entry-title">{{ e.title }}</div>
|
||||
<div class="entry-desc">{{ e.desc }}</div>
|
||||
</router-link>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.entry-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.entry-link {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: hsl(var(--card-foreground));
|
||||
}
|
||||
.entry-title {
|
||||
font-weight: 600;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.entry-desc {
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
/** 驾驶舱布局编排(issue #171 [B5]):组件清单 + 排序 + 导出布局 JSON */
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, VbenButton } from '@vben/common-ui';
|
||||
import { MaterialSymbolsEdit } from '@vben/icons';
|
||||
|
||||
const components = ref([
|
||||
{ type: 'process_view', title: '四状态工艺流程', enabled: true },
|
||||
{ type: 'trend', title: '实时趋势', enabled: true },
|
||||
@@ -21,28 +23,59 @@ const move = (i: number, dir: number) => {
|
||||
components.value[i] = components.value[j];
|
||||
components.value[j] = t;
|
||||
};
|
||||
|
||||
const layoutJson = () => JSON.stringify({ widgets: components.value.filter((c) => c.enabled) }, null, 2);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 style="font-size: 18px">驾驶舱布局编排(PRD 5.7)</h1>
|
||||
<div style="display: flex; gap: 24px">
|
||||
<div style="flex: 1">
|
||||
<div style="font-size: 13px; color: #888; margin-bottom: 6px">组件库(点击↑↓调整顺序)</div>
|
||||
<Page title="驾驶舱布局编排" description="PRD 5.7 · 组件清单排序 / 启用 / 导出布局 JSON">
|
||||
<div class="layout-row">
|
||||
<div class="col">
|
||||
<div class="col-title">组件库(点击 ↑ ↓ 调整顺序)</div>
|
||||
<a-list size="small" bordered>
|
||||
<a-list-item v-for="(c, i) in components" :key="i">
|
||||
<a-checkbox v-model:checked="c.enabled" />
|
||||
<span style="flex: 1; margin-left: 8px">{{ c.title }}</span>
|
||||
<span class="item-title">{{ c.title }}</span>
|
||||
<a-button size="small" @click="move(i, -1)">↑</a-button>
|
||||
<a-button size="small" style="margin-left: 4px" @click="move(i, 1)">↓</a-button>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</div>
|
||||
<div style="flex: 1">
|
||||
<div style="font-size: 13px; color: #888; margin-bottom: 6px">导出布局 JSON(由 B1 驾驶舱渲染)</div>
|
||||
<pre style="background: #0f172a; color: #7dd3fc; border-radius: 8px; padding: 12px; font-size: 12px; max-height: 320px; overflow: auto">{{ JSON.stringify({ widgets: components.value.filter(c => c.enabled) }, null, 2) }}</pre>
|
||||
<a-button type="primary" style="margin-top: 8px">发布布局(版本发布见「版本发布」页)</a-button>
|
||||
<div class="col">
|
||||
<div class="col-title">导出布局 JSON(由驾驶舱渲染)</div>
|
||||
<pre class="json-body">{{ layoutJson() }}</pre>
|
||||
<VbenButton variant="solid"><MaterialSymbolsEdit class="size-4" />发布布局(版本发布见「版本发布」页)</VbenButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.layout-row {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
.col {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.col-title {
|
||||
font-size: 13px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.item-title {
|
||||
flex: 1;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.json-body {
|
||||
background: hsl(var(--muted) / 0.6);
|
||||
color: hsl(var(--foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,38 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
/** 模板版本发布(issue #171 [B5]):版本列表 + 发布 */
|
||||
import { ref } from 'vue';
|
||||
|
||||
const versions = ref([
|
||||
import { Page, VbenButton } from '@vben/common-ui';
|
||||
import { MaterialSymbolsAdd } from '@vben/icons';
|
||||
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
const rows = ref([
|
||||
{ version: 'v1.0.0', stage: 'prod', time: '2026-08-05 10:00', author: 'bot_dev1', note: 'Ti 一期发布' },
|
||||
{ version: 'v0.9.0', stage: 'staging', time: '2026-08-04 16:30', author: 'bot_dev1', note: '验收候选' },
|
||||
{ version: 'v0.8.0', stage: 'dev', time: '2026-08-03 09:00', author: 'bot_dev1', note: '布局资产初版' },
|
||||
]);
|
||||
|
||||
const publish = () => {
|
||||
versions.value.unshift({
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
rowConfig: { keyField: 'version' },
|
||||
height: 'auto',
|
||||
stripe: true,
|
||||
columns: [
|
||||
{ title: '版本', field: 'version', width: 110 },
|
||||
{ title: '阶段', field: 'stage', width: 100, slots: { default: 'stage' } },
|
||||
{ title: '时间', field: 'time', width: 170 },
|
||||
{ title: '作者', field: 'author', width: 110 },
|
||||
{ title: '说明', field: 'note', minWidth: 200 },
|
||||
],
|
||||
proxy: {
|
||||
query: async ({ page }) => ({
|
||||
items: rows.value.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize),
|
||||
total: rows.value.length,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
function publish() {
|
||||
rows.value.unshift({
|
||||
version: 'v1.0.1', stage: 'staging', time: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
||||
author: 'admin', note: '手动发布',
|
||||
});
|
||||
};
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 style="font-size: 18px">模板版本发布(PRD 5.7 版本管理)</h1>
|
||||
<a-table :data-source="versions" size="small" :pagination="false"
|
||||
:columns="[
|
||||
{ title: '版本', dataIndex: 'version', key: 'version' },
|
||||
{ title: '阶段', dataIndex: 'stage', key: 'stage' },
|
||||
{ title: '时间', dataIndex: 'time', key: 'time' },
|
||||
{ title: '作者', dataIndex: 'author', key: 'author' },
|
||||
{ title: '说明', dataIndex: 'note', key: 'note' },
|
||||
]">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'stage'">
|
||||
<a-tag :color="record.stage === 'prod' ? 'green' : record.stage === 'staging' ? 'orange' : 'blue'">{{ record.stage }}</a-tag>
|
||||
</template>
|
||||
<Page title="模板版本发布" description="PRD 5.7 · 版本管理 / 阶段流转">
|
||||
<template #extra>
|
||||
<VbenButton variant="solid" @click="publish"><MaterialSymbolsAdd class="size-4" />发布新版本</VbenButton>
|
||||
</template>
|
||||
<Grid>
|
||||
<template #stage="{ row }">
|
||||
<a-tag :color="row.stage === 'prod' ? 'green' : row.stage === 'staging' ? 'orange' : 'blue'">{{ row.stage }}</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-button type="primary" style="margin-top: 12px" @click="publish">发布新版本</a-button>
|
||||
</div>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user