feat: 完成 issue #183 [E2] 前端接入注册表 API——registry.ts 四接口封装 + models.vue API 优先/localStorage 降级/迁移提示 + version.vue 发布=注册
This commit is contained in:
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* 模型注册表 API 客户端(issue #183 [E2])
|
||||||
|
*
|
||||||
|
* 封装 E1(#182)四接口(nginx 反代 /api/v1/registry/ → registry 服务 :8002):
|
||||||
|
* GET /api/v1/registry/models 列表(?stage= 过滤)
|
||||||
|
* POST /api/v1/registry/models 注册新版本
|
||||||
|
* POST /api/v1/registry/models/{name}/{ver}/promote 阶段提升
|
||||||
|
* POST /api/v1/registry/models/{name}/rollback 回滚
|
||||||
|
*
|
||||||
|
* 鉴权:携带 FBA access token(useAccessStore,与 requestClient 同源)。
|
||||||
|
* 统一错误:抛 Error(msg 来自服务端 / HTTP 状态),由调用方降级或提示。
|
||||||
|
*/
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
export interface RegistryModel {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
backbone: string;
|
||||||
|
stage: 'dev' | 'staging' | 'prod';
|
||||||
|
description?: string;
|
||||||
|
registered_at?: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryListResp {
|
||||||
|
models: RegistryModel[];
|
||||||
|
stage_pointers?: Record<string, Record<string, string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const REG_BASE = '/api/v1/registry';
|
||||||
|
|
||||||
|
function bearer(): string {
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const token = accessStore.accessToken as string | undefined;
|
||||||
|
return token ? `Bearer ${token}` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function regFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
|
let resp: Response;
|
||||||
|
try {
|
||||||
|
resp = await fetch(`${REG_BASE}${path}`, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: bearer(),
|
||||||
|
...(init?.headers || {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
throw new Error('注册表服务不可达');
|
||||||
|
}
|
||||||
|
const raw = await resp.text();
|
||||||
|
const data = (raw ? JSON.parse(raw) : {}) as { msg?: string; [k: string]: unknown };
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(data.msg || `注册表 HTTP ${resp.status}`);
|
||||||
|
}
|
||||||
|
return data as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 模型列表(可按 stage 过滤) */
|
||||||
|
export async function fetchRegistryModels(stage?: string): Promise<RegistryListResp> {
|
||||||
|
return regFetch<RegistryListResp>(stage ? `/models?stage=${stage}` : '/models');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册新版本(stage 默认 dev) */
|
||||||
|
export async function registerModel(body: {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
backbone?: string;
|
||||||
|
description?: string;
|
||||||
|
stage?: string;
|
||||||
|
}): Promise<{ ok: boolean; model: RegistryModel }> {
|
||||||
|
return regFetch('/models', { method: 'POST', body: JSON.stringify(body) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 阶段提升 */
|
||||||
|
export async function promoteModel(
|
||||||
|
name: string,
|
||||||
|
version: string,
|
||||||
|
): Promise<{ ok: boolean; model: RegistryModel }> {
|
||||||
|
return regFetch(`/models/${encodeURIComponent(name)}/${encodeURIComponent(version)}/promote`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 回滚某 stage 指针到指定版本 */
|
||||||
|
export async function rollbackModel(
|
||||||
|
name: string,
|
||||||
|
stage: string,
|
||||||
|
version: string,
|
||||||
|
): Promise<{ ok: boolean; model: RegistryModel }> {
|
||||||
|
return regFetch(`/models/${encodeURIComponent(name)}/rollback`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ stage, version }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册表可用性探测(离线时调用方降级 localStorage) */
|
||||||
|
export async function registryAvailable(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fetchRegistryModels();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,117 +1,194 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* 模型管理(issue #172 [B6] 重构;#D2 样式对齐;2026-08-06 数据回退修复)
|
* 模型管理(issue #183 [E2] 前端接入注册表 API)
|
||||||
*
|
* - 数据源:服务端注册表 API(list/promote/rollback 全走服务端,多用户共享);
|
||||||
* 数据语义对齐旧版 web/admin/admin.js:
|
* - 降级:API 不可达时回退 localStorage(iaop.admin.models.v1,旧版 key),UI 标注「离线演示模式」;
|
||||||
* - 模型注册表持久化在 localStorage(key 与旧版相同:iaop.admin.models.v1),
|
* - 迁移:检测 localStorage 有数据且服务端在线时提示导入。
|
||||||
* 同源(39.101.182.167:8090)下旧版页面积累的数据自动迁移可见;
|
|
||||||
* - 首次无数据时用旧版种子数据(含 author / updated_at);
|
|
||||||
* - 提升/回滚写回 localStorage(旧版行为),而非仅改内存。
|
|
||||||
*/
|
*/
|
||||||
import { ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { Page, VbenButton } from '@vben/common-ui';
|
import { Page, VbenButton } from '@vben/common-ui';
|
||||||
|
import { message } from 'antdv-next';
|
||||||
|
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
fetchRegistryModels,
|
||||||
|
promoteModel,
|
||||||
|
registerModel,
|
||||||
|
rollbackModel,
|
||||||
|
type RegistryModel,
|
||||||
|
} from '#/api/iaop/registry';
|
||||||
|
|
||||||
interface ModelItem {
|
const MODELS_KEY = 'iaop.admin.models.v1';
|
||||||
|
|
||||||
|
interface LocalModel {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
stage: string;
|
stage: string;
|
||||||
author: string;
|
desc?: string;
|
||||||
updated_at: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODELS_KEY = 'iaop.admin.models.v1'; // 与旧版 admin.js 相同,同源共享
|
function loadLocal(): LocalModel[] {
|
||||||
|
|
||||||
function seedModels(): ModelItem[] {
|
|
||||||
return [
|
|
||||||
{ name: 'quality_forecast', version: '1.2.0', stage: 'prod', author: 'bot_dev1', updated_at: '2026-08-01T09:00:00Z' },
|
|
||||||
{ name: 'anomaly_detection', version: '1.0.3', stage: 'staging', author: 'bot_dev1', updated_at: '2026-08-02T09:00:00Z' },
|
|
||||||
{ name: 'process_optimizer', version: '0.9.1', stage: 'dev', author: 'engineer', updated_at: '2026-08-03T09:00:00Z' },
|
|
||||||
{ name: 'cross_process_optimizer', version: '0.5.0', stage: 'dev', author: 'engineer', updated_at: '2026-08-04T09:00:00Z' },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadModels(): ModelItem[] {
|
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(MODELS_KEY);
|
const raw = localStorage.getItem(MODELS_KEY);
|
||||||
if (raw) {
|
return raw ? (JSON.parse(raw) as LocalModel[]) : [];
|
||||||
const arr = JSON.parse(raw);
|
} catch {
|
||||||
if (Array.isArray(arr) && arr.length > 0) return arr;
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function saveLocal(list: LocalModel[]) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(MODELS_KEY, JSON.stringify(list));
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
}
|
}
|
||||||
} catch { /* 隐私模式等忽略 */ }
|
|
||||||
const seed = seedModels();
|
|
||||||
saveModels(seed);
|
|
||||||
return seed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveModels(list: ModelItem[]) {
|
const online = ref(false); // 注册表服务在线?
|
||||||
try { localStorage.setItem(MODELS_KEY, JSON.stringify(list)); } catch { /* ignore */ }
|
const loading = ref(true);
|
||||||
}
|
const models = ref<RegistryModel[]>([]);
|
||||||
|
const migrateVisible = ref(false);
|
||||||
const models = ref<ModelItem[]>(loadModels());
|
|
||||||
|
|
||||||
const gridOptions: VxeTableGridOptions = {
|
const gridOptions: VxeTableGridOptions = {
|
||||||
rowConfig: { keyField: 'name' },
|
rowConfig: { keyField: 'name' },
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
stripe: true,
|
stripe: true,
|
||||||
pagerConfig: { enabled: true, pageSize: 10 },
|
|
||||||
columns: [
|
columns: [
|
||||||
{ title: '模型', field: 'name', minWidth: 180 },
|
{ title: '模型', field: 'name', minWidth: 180 },
|
||||||
{ title: '版本', field: 'version', width: 110 },
|
{ title: '版本', field: 'version', width: 120 },
|
||||||
{ title: '阶段', field: 'stage', width: 100, slots: { default: 'stage' } },
|
{ title: '阶段', field: 'stage', width: 110, slots: { default: 'stage' } },
|
||||||
{ title: '作者', field: 'author', width: 110 },
|
{ title: '说明', field: 'description', minWidth: 220 },
|
||||||
{ title: '更新时间', field: 'updated_at', width: 130, slots: { default: 'updated' } },
|
|
||||||
{ title: '操作', field: 'action', width: 170, slots: { default: 'action' } },
|
{ title: '操作', field: 'action', width: 170, slots: { default: 'action' } },
|
||||||
],
|
],
|
||||||
proxy: {
|
proxy: {
|
||||||
query: async ({ page } = {}) => {
|
query: async ({ page }) => ({
|
||||||
// page 可能缺省(分页未就绪时的首次查询),兜底返回全量
|
items: models.value.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize),
|
||||||
const cur = page?.currentPage ?? 1;
|
|
||||||
const size = page?.pageSize ?? (models.value.length || 10);
|
|
||||||
return {
|
|
||||||
items: models.value.slice((cur - 1) * size, cur * size),
|
|
||||||
total: models.value.length,
|
total: models.value.length,
|
||||||
};
|
}),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||||
|
|
||||||
function touch(m: ModelItem) {
|
async function refresh() {
|
||||||
m.updated_at = new Date().toISOString();
|
|
||||||
saveModels(models.value);
|
|
||||||
gridApi.query();
|
gridApi.query();
|
||||||
}
|
}
|
||||||
|
|
||||||
function promote(m: ModelItem) {
|
async function loadFromServer() {
|
||||||
if (m.stage === 'dev') m.stage = 'staging';
|
const resp = await fetchRegistryModels();
|
||||||
else if (m.stage === 'staging') m.stage = 'prod';
|
models.value = resp.models;
|
||||||
touch(m);
|
|
||||||
}
|
}
|
||||||
function rollback(m: ModelItem) {
|
|
||||||
if (m.stage === 'prod') m.stage = 'staging';
|
async function init() {
|
||||||
else if (m.stage === 'staging') m.stage = 'dev';
|
loading.value = true;
|
||||||
touch(m);
|
try {
|
||||||
|
await loadFromServer();
|
||||||
|
online.value = true;
|
||||||
|
// 迁移提示:本地有数据且服务端在线(且本地有服务端没有的模型)
|
||||||
|
const local = loadLocal();
|
||||||
|
const serverNames = new Set(models.value.map((m) => `${m.name}@${m.version}`));
|
||||||
|
const localExtra = local.filter((m) => !serverNames.has(`${m.name}@${m.version}`));
|
||||||
|
if (localExtra.length) {
|
||||||
|
migrateVisible.value = true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
online.value = false;
|
||||||
|
const local = loadLocal();
|
||||||
|
models.value = local.map((m) => ({
|
||||||
|
name: m.name, version: m.version, stage: m.stage as RegistryModel['stage'],
|
||||||
|
description: m.desc, backbone: 'generic',
|
||||||
|
}));
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function promote(m: RegistryModel) {
|
||||||
|
if (online.value) {
|
||||||
|
try {
|
||||||
|
await promoteModel(m.name, m.version);
|
||||||
|
await loadFromServer();
|
||||||
|
refresh();
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
message.error(`提升失败:${(e as Error).message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 离线降级
|
||||||
|
m.stage = m.stage === 'staging' ? 'prod' : m.stage === 'dev' ? 'staging' : m.stage;
|
||||||
|
saveLocal(models.value.map((x) => ({ name: x.name, version: x.version, stage: x.stage, desc: x.description })));
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rollback(m: RegistryModel) {
|
||||||
|
if (online.value) {
|
||||||
|
try {
|
||||||
|
await rollbackModel(m.name, 'prod', m.version);
|
||||||
|
await loadFromServer();
|
||||||
|
refresh();
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
message.error(`回滚失败:${(e as Error).message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.stage = 'staging';
|
||||||
|
saveLocal(models.value.map((x) => ({ name: x.name, version: x.version, stage: x.stage, desc: x.description })));
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrateLocal() {
|
||||||
|
const local = loadLocal();
|
||||||
|
let ok = 0;
|
||||||
|
let fail = 0;
|
||||||
|
for (const m of local) {
|
||||||
|
try {
|
||||||
|
await registerModel({ name: m.name, version: m.version, stage: m.stage, description: m.desc });
|
||||||
|
ok++;
|
||||||
|
} catch {
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await loadFromServer();
|
||||||
|
refresh();
|
||||||
|
migrateVisible.value = false;
|
||||||
|
if (fail === 0) {
|
||||||
|
localStorage.removeItem(MODELS_KEY);
|
||||||
|
message.success(`已导入 ${ok} 条本地模型到注册表`);
|
||||||
|
} else {
|
||||||
|
message.warning(`导入完成:成功 ${ok},失败 ${fail}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagColor = (stage: string) =>
|
||||||
|
stage === 'prod' ? 'green' : stage === 'staging' ? 'orange' : 'blue';
|
||||||
|
|
||||||
|
onMounted(init);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page title="模型管理" description="PRD 5.3 · 模板注册表 / 版本阶段流转">
|
<Page title="模型管理" :description="online ? 'PRD 5.3 · 服务端注册表(多用户共享)' : 'PRD 5.3 · 离线演示模式(注册表服务不可达,数据存本地)'">
|
||||||
|
<template #extra>
|
||||||
|
<a-tag :color="online ? 'green' : 'orange'">{{ online ? '● 服务端在线' : '● 离线演示模式' }}</a-tag>
|
||||||
|
</template>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
<Grid>
|
<Grid>
|
||||||
<template #stage="{ row }">
|
<template #stage="{ row }">
|
||||||
<a-tag :color="row.stage === 'prod' ? 'green' : row.stage === 'staging' ? 'orange' : 'blue'">{{ row.stage }}</a-tag>
|
<a-tag :color="tagColor(row.stage)">{{ row.stage }}</a-tag>
|
||||||
</template>
|
|
||||||
<template #updated="{ row }">
|
|
||||||
{{ (row.updated_at || '').slice(0, 10) }}
|
|
||||||
</template>
|
</template>
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<VbenButton size="small" :disabled="row.stage === 'prod'" @click="promote(row)">提升</VbenButton>
|
<VbenButton size="small" :disabled="row.stage === 'prod'" @click="promote(row)">提升</VbenButton>
|
||||||
<VbenButton size="small" variant="outline" :disabled="row.stage === 'dev'" @click="rollback(row)">回滚</VbenButton>
|
<VbenButton size="small" variant="outline" :disabled="row.stage !== 'prod'" @click="rollback(row)">回滚</VbenButton>
|
||||||
</template>
|
</template>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</a-spin>
|
||||||
|
<a-modal v-model:open="migrateVisible" title="本地模型导入注册表"
|
||||||
|
:on-ok="migrateLocal" ok-text="导入服务端" cancel-text="暂不">
|
||||||
|
<p>检测到本地 localStorage 中还有模型数据(旧版演示存储),是否导入服务端注册表?
|
||||||
|
导入后多用户共享,且本机数据将清除。</p>
|
||||||
|
</a-modal>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,13 +1,28 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 模板版本发布(issue #183 [E2]):发布 = 注册新版本到服务端注册表;
|
||||||
|
* 注册表离线时降级本地记录并标注。
|
||||||
|
*/
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { Page, VbenButton } from '@vben/common-ui';
|
import { Page, VbenButton } from '@vben/common-ui';
|
||||||
import { MaterialSymbolsAdd } from '@vben/icons';
|
import { MaterialSymbolsAdd } from '@vben/icons';
|
||||||
|
import { message } from 'antdv-next';
|
||||||
|
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { registerModel } from '#/api/iaop/registry';
|
||||||
|
|
||||||
const rows = ref([
|
interface VersionRow {
|
||||||
|
version: string;
|
||||||
|
stage: string;
|
||||||
|
time: string;
|
||||||
|
author: string;
|
||||||
|
note: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const online = ref(false);
|
||||||
|
const rows = ref<VersionRow[]>([
|
||||||
{ version: 'v1.0.0', stage: 'prod', time: '2026-08-05 10:00', author: 'bot_dev1', note: 'Ti 一期发布' },
|
{ 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.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: '布局资产初版' },
|
{ version: 'v0.8.0', stage: 'dev', time: '2026-08-03 09:00', author: 'bot_dev1', note: '布局资产初版' },
|
||||||
@@ -24,32 +39,44 @@ const gridOptions: VxeTableGridOptions = {
|
|||||||
{ title: '作者', field: 'author', width: 110 },
|
{ title: '作者', field: 'author', width: 110 },
|
||||||
{ title: '说明', field: 'note', minWidth: 200 },
|
{ title: '说明', field: 'note', minWidth: 200 },
|
||||||
],
|
],
|
||||||
pagerConfig: { enabled: true, pageSize: 10 },
|
|
||||||
proxy: {
|
proxy: {
|
||||||
query: async ({ page } = {}) => {
|
query: async ({ page }) => ({
|
||||||
const cur = page?.currentPage ?? 1;
|
items: rows.value.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize),
|
||||||
const size = page?.pageSize ?? 10;
|
|
||||||
return {
|
|
||||||
items: rows.value.slice((cur - 1) * size, cur * size),
|
|
||||||
total: rows.value.length,
|
total: rows.value.length,
|
||||||
};
|
}),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||||
|
|
||||||
function publish() {
|
async function publish() {
|
||||||
|
const version = `v${Date.now() % 100000}`;
|
||||||
|
const name = 'iaop-template';
|
||||||
|
try {
|
||||||
|
// 发布 = 注册新版本到服务端注册表(staging)
|
||||||
|
await registerModel({ name, version, stage: 'staging', description: '手动发布(前端)' });
|
||||||
|
online.value = true;
|
||||||
rows.value.unshift({
|
rows.value.unshift({
|
||||||
version: 'v1.0.1', stage: 'staging', time: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
version, stage: 'staging',
|
||||||
author: 'admin', note: '手动发布',
|
time: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
||||||
|
author: 'admin', note: '已发布到服务端注册表',
|
||||||
});
|
});
|
||||||
|
message.success(`已注册 ${name}@${version}(staging)`);
|
||||||
|
} catch {
|
||||||
|
online.value = false;
|
||||||
|
rows.value.unshift({
|
||||||
|
version, stage: 'staging',
|
||||||
|
time: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
||||||
|
author: 'admin', note: '离线降级(仅本地记录)',
|
||||||
|
});
|
||||||
|
message.warning('注册表服务不可达:本次发布仅本地记录');
|
||||||
|
}
|
||||||
gridApi.query();
|
gridApi.query();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page title="模板版本发布" description="PRD 5.7 · 版本管理 / 阶段流转">
|
<Page title="模板版本发布" :description="online ? 'PRD 5.7 · 发布 = 注册到服务端注册表' : 'PRD 5.7 · 版本管理(注册表离线时降级本地)'">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<VbenButton variant="solid" @click="publish"><MaterialSymbolsAdd class="size-4" />发布新版本</VbenButton>
|
<VbenButton variant="solid" @click="publish"><MaterialSymbolsAdd class="size-4" />发布新版本</VbenButton>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user