feat: 完成 issue #170/#171/#172/#173 [B4-B7]——studio 点位导入/模型超参/编排/版本 + admin 模型/知识库/告警/审计(原生 Vue 页)
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/** 告警确认(issue #173 [B7]):severity 配色 + 确认 + LLM 解释(infer.ts) */
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { ask } from '#/api/iaop/infer';
|
||||||
|
|
||||||
|
const alerts = ref([
|
||||||
|
{ id: 1, severity: 'P0', title: '还原炉 TiCl₄ 纯度连续 3 点低于 99.9%', device: 'RF-01', status: 'pending' },
|
||||||
|
{ id: 2, severity: 'P1', title: '氯化炉炉压偏离设定 ±50Pa', device: 'CLF-01', status: 'pending' },
|
||||||
|
{ id: 3, severity: 'P2', title: '蒸馏塔液位波动增大', device: 'D-03', status: 'acked' },
|
||||||
|
]);
|
||||||
|
const sevColor: Record<string, string> = { P0: 'red', P1: 'orange', P2: 'blue' };
|
||||||
|
const explain = ref('');
|
||||||
|
|
||||||
|
async function explainAlert(a: { title: string }) {
|
||||||
|
try {
|
||||||
|
const r = await ask(`请解释告警并给出处置建议:${a.title}`, { maxTokens: 200 });
|
||||||
|
explain.value = r.text;
|
||||||
|
} catch { explain.value = '推理服务不可用,无法生成解释。'; }
|
||||||
|
}
|
||||||
|
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>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
<a-alert v-if="explain" type="info" style="margin-top: 12px" :message="explain" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/** 审计查询(issue #173 [B7]):操作日志列表 + 筛选 */
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
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 发布' },
|
||||||
|
{ time: '2026-08-06 08:40', user: 'engineer1', resource: '点位', action: 'import', result: 'ok', detail: 'point_dict.csv 600 点导入' },
|
||||||
|
{ 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;
|
||||||
|
};
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/** 管理控制台目录页(issue #172/#173 [B6/B7]):4 子模块入口 */
|
||||||
|
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: '操作日志 / 筛选 / 结果状态' },
|
||||||
|
];
|
||||||
|
</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">
|
||||||
|
<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>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/** 知识库管理(issue #172 [B6]):citations.json 文档列表 + 索引状态 */
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
const docs = ref<Array<{ title: string; sections: number; indexed: boolean }>>([]);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/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 = []; }
|
||||||
|
});
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/** 模型管理(issue #172 [B6]):模型模板列表 + 版本/阶段 + 提升/回滚 */
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
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)' },
|
||||||
|
{ name: 'anomaly_detection', version: 'v1.4.0', stage: 'prod', desc: '炉温异常检测(iForest)' },
|
||||||
|
{ 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'; };
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/** 模型超参配置(issue #170 [B4]):4 类模型 + 表单 + JSON 预览 */
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
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));
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<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('');
|
||||||
|
|
||||||
|
const HEADERS = ['device_id', 'point_id', 'name', 'unit', 'dataType', 'sampleRate', 'qualityCode', 'opcNode', 'protocol'];
|
||||||
|
|
||||||
|
function onFile(e: Event) {
|
||||||
|
const el = e.target as HTMLInputElement;
|
||||||
|
const file = el.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
fileName.value = file.name;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => parseCsv(String(reader.result || ''));
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCsv(text: string) {
|
||||||
|
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
||||||
|
errors.value = [];
|
||||||
|
preview.value = [];
|
||||||
|
if (!lines.length) { errors.value = ['文件为空']; return; }
|
||||||
|
const header = lines[0].split(',').map((h) => h.trim());
|
||||||
|
const missing = HEADERS.filter((h) => !header.includes(h));
|
||||||
|
if (missing.length) { errors.value = [`表头缺少字段: ${missing.join(', ')}`]; return; }
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const cells = lines[i].split(',').map((c) => c.trim());
|
||||||
|
if (cells.length !== header.length) { errors.value.push(`第 ${i + 1} 行列数不一致`); continue; }
|
||||||
|
const row: Record<string, string> = {};
|
||||||
|
header.forEach((h, j) => (row[h] = cells[j]));
|
||||||
|
if (!row.point_id) errors.value.push(`第 ${i + 1} 行缺少 point_id`);
|
||||||
|
if (!(Number(row.sampleRate) > 0)) errors.value.push(`第 ${i + 1} 行 sampleRate 非法`);
|
||||||
|
preview.value.push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/** 模板配置台目录页(issue #170/#171 [B4/B5]):4 子模块入口 */
|
||||||
|
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: '模板版本列表 / 发布 / 阶段' },
|
||||||
|
];
|
||||||
|
</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">
|
||||||
|
<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>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/** 驾驶舱布局编排(issue #171 [B5]):组件清单 + 排序 + 导出布局 JSON */
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const components = ref([
|
||||||
|
{ type: 'process_view', title: '四状态工艺流程', enabled: true },
|
||||||
|
{ type: 'trend', title: '实时趋势', enabled: true },
|
||||||
|
{ type: 'trend', title: '实时趋势 2', enabled: true },
|
||||||
|
{ type: 'kpi_card', title: 'KPI 卡片', enabled: true },
|
||||||
|
{ type: 'kpi_card', title: 'KPI 卡片 2', enabled: true },
|
||||||
|
{ type: 'kpi_card', title: 'KPI 卡片 3', enabled: true },
|
||||||
|
{ type: 'kpi_card', title: 'KPI 卡片 4', enabled: true },
|
||||||
|
{ type: 'alarm_panel', title: '告警面板', enabled: true },
|
||||||
|
{ type: 'nl_query', title: 'NL 查询', enabled: true },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const move = (i: number, dir: number) => {
|
||||||
|
const j = i + dir;
|
||||||
|
if (j < 0 || j >= components.value.length) return;
|
||||||
|
const t = components.value[i];
|
||||||
|
components.value[i] = components.value[j];
|
||||||
|
components.value[j] = t;
|
||||||
|
};
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/** 模板版本发布(issue #171 [B5]):版本列表 + 发布 */
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const versions = 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({
|
||||||
|
version: 'v1.0.1', stage: 'staging', time: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
||||||
|
author: 'admin', note: '手动发布',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
<a-button type="primary" style="margin-top: 12px" @click="publish">发布新版本</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user