feat(mock): mock 演进轮——应用列表/代理测试页 + 网关 mock 数据扩充

- views/app/AppList.vue(新)+ views/proxytest/ProxyTest.vue(新):应用列表与代理测试演示页
- router/routes + apiDefinitions + App.vue/layout:新页路由与菜单接线
- assets/mock/gatewayApi.ts:mock 数据扩充(+136 行)
- RouteList:列表页联动增强
- vite.config:mock 相关配置调整
- theme.ts:sync 刷新至 20261006-2013-75c1d10 并补回本仓唯一允许适配(顶部 // @ts-nocheck,strict vue-tsc 契约);typecheck 零错误
This commit is contained in:
chenxing
2026-10-06 20:22:02 +08:00
parent 8a21d85036
commit 77060325ef
10 changed files with 711 additions and 7 deletions
+281
View File
@@ -0,0 +1,281 @@
<template>
<div class="main-wrapper">
<div class="main-container">
<div class="search-wrap fix">
<span class="tip">
应用管理(open_api_app):ANON=仅内部入口免签,仍须带 X-App-Key;appKey 填 <b>*</b> =
匿名窗口(无票据放行,≤31 天强制到期);改回 SIGNED 前先确认密钥链有该键。
</span>
<div class="fr">
<Button type="primary" @click="openCreate">新增应用</Button>
<Button class="ml8" @click="refresh">刷新</Button>
</div>
</div>
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" :dataFilter="normalizeRows"
:tableHeight="tableHeight">
<template #appKey="{ row }">
<span v-if="row.appKey === '*'" class="window-tag">*(匿名窗口)</span>
<span v-else class="mono">{{ row.appKey }}</span>
</template>
<template #appName="{ row }">
<Input v-model="row.appName" size="small" placeholder="-" />
</template>
<template #authMode="{ row }">
<Select v-model="row.authMode" size="small" style="width: 110px"
@on-change="onModeChange(row)">
<Option value="SIGNED">SIGNED</Option>
<Option value="ANON">ANON</Option>
</Select>
</template>
<template #expireAt="{ row }">
<template v-if="row.authMode === 'ANON'">
<Input v-model="row.expireAtText" size="small" placeholder="yyyy-MM-dd HH:mm:ss"
style="width: 170px" :class="expireClass(row)" />
<div :class="expireClass(row)" class="expire-hint">{{ expireHint(row) }}</div>
</template>
<span v-else class="expire-none">-</span>
</template>
<template #enabled="{ row }">
<i-switch v-model="row.enabled" size="small" />
</template>
<template #remark="{ row }">
<Input v-model="row.remark" size="small" placeholder="-" />
</template>
<template #gmtModified="{ row }">
<span>{{ row.gmtModified ? Utils.dateFormat(row.gmtModified, 'yyyy-MM-dd HH:mm:ss') : '-' }}</span>
</template>
<template #action="{ row }">
<Button type="primary" size="small" :loading="savingId === row.id" @click="save(row)">保存</Button>
</template>
</TablePageAlovaNew>
<Modal v-model="createShow" :mask-closable="false" width="560" title="新增应用">
<Form :label-width="110" class="app-create-form">
<FormItem label="appKey:" required>
<Input v-model="createForm.appKey" placeholder="调用方 appKey(唯一;填 * = 匿名窗口行)" />
</FormItem>
<FormItem label="展示名:">
<Input v-model="createForm.appName" placeholder="可选" />
</FormItem>
<FormItem label="authMode:" required>
<Select v-model="createForm.authMode" style="width: 180px">
<Option value="SIGNED">SIGNED(验签,缺省)</Option>
<Option value="ANON">ANON(内部入口免签)</Option>
</Select>
</FormItem>
<FormItem v-if="createForm.authMode === 'ANON'" label="免签到期:" required>
<Input v-model="createForm.expireAt" placeholder="yyyy-MM-dd HH:mm:ss(必填,≤31 天)" />
</FormItem>
<FormItem label="备注:">
<Input v-model="createForm.remark" placeholder="可选" />
</FormItem>
<div class="form-tip" v-if="createForm.authMode === 'ANON'">
免签最长 31 天(每次保存重新起算),到期惰性恢复验签;ANON 应用不要求密钥链有键。
</div>
</Form>
<template #footer>
<Button @click="createShow = false">取消</Button>
<Button type="primary" :loading="creating" @click="submitCreate">确定</Button>
</template>
</Modal>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import useCurrentInstance from '@/utils/hook/useCurrentInstance';
import TablePageAlovaNew from '@/components/TablePageAlovaNew.vue';
import Utils from '@/utils';
defineOptions({ name: 'AppList' });
const { $utils } = useCurrentInstance();
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
const savingId = ref<number | null>(null);
const tableApi = (params: { pageIndex: number; pageSize: number }) =>
Apis.GatewayAdmin.apps({ params, cacheFor: null });
const tableHeight = ref($utils.useTableHeight({ isOperat: false, isPage: true, isSearch: true }));
const columns = [
{ title: 'id', key: 'id', width: 70, align: 'center' },
{ title: 'appKey', slot: 'appKey', width: 160 },
{ title: '展示名', slot: 'appName', width: 150 },
{ title: 'authMode', slot: 'authMode', width: 130 },
{ title: '免签到期', slot: 'expireAt', minWidth: 210 },
{ title: '启用', slot: 'enabled', width: 80, align: 'center' },
{ title: '备注', slot: 'remark', minWidth: 140 },
{ title: '更新时间', slot: 'gmtModified', width: 170 },
{ title: '操作', slot: 'action', width: 90, align: 'center', fixed: 'right' as const },
];
/** 服务端 expireAt 为 ISO 串:补一行内编辑用的 "yyyy-MM-dd HH:mm:ss" 文本 */
function normalizeRows(rows: any[]) {
return (rows || []).map((r: any) => ({
...r,
expireAtText: r.expireAt ? Utils.dateFormat(r.expireAt, 'yyyy-MM-dd HH:mm:ss') : '',
}));
}
const DAY_MS = 24 * 3600 * 1000;
const MAX_TTL_DAYS = 31;
/** 到期可视化:已过期标红、临期(≤3 天)标橙 */
function expireState(row: any): 'expired' | 'soon' | 'ok' {
if (!row.expireAt) return 'expired';
const remain = new Date(String(row.expireAt).replace(/-/g, '/').replace(/T/, ' ')).getTime() - Date.now();
if (remain <= 0) return 'expired';
if (remain <= 3 * DAY_MS) return 'soon';
return 'ok';
}
function expireClass(row: any) {
const s = expireState(row);
return { 'expire-danger': s === 'expired', 'expire-warning': s === 'soon' };
}
function expireHint(row: any) {
const s = expireState(row);
if (s === 'expired') return '已过期(免签失效,等同无行)';
if (s === 'soon') return '临期,续期请重新保存';
return `有效期 ≤${MAX_TTL_DAYS} 天,续期重新起算`;
}
/** 切到 SIGNED 即视为放弃免签:清空行内到期文本(后端落库置 NULL) */
function onModeChange(row: any) {
if (row.authMode !== 'ANON') {
row.expireAtText = '';
}
}
function refresh() {
tableRef.value?.refreshData();
}
/** 行内编辑:只提交治理字段(appKey 是行身份,后端不接受改动);ANON 行连同到期一起提交 */
async function save(row: any) {
savingId.value = row.id;
try {
const data: Record<string, any> = {
appName: row.appName,
authMode: row.authMode,
enabled: row.enabled,
remark: row.remark,
};
if (row.authMode === 'ANON') {
data.expireAt = row.expireAtText;
}
const d: any = await Apis.GatewayAdmin.updateApp({
pathParams: { id: row.id },
data,
});
Message.success(`已保存,应用表加载 ${d?.apps ?? '-'} 条`);
} catch (e: any) {
// 错误提示由 alova 全局 onBizError 弹出,这里只做回滚,避免双 toast
tableRef.value?.refreshData();
} finally {
savingId.value = null;
}
}
/* 新增应用弹窗(O3:治理页闭环,不留"只有 UI 没有后端"断层) */
const createShow = ref(false);
const creating = ref(false);
const createForm = reactive({
appKey: '',
appName: '',
authMode: 'ANON',
expireAt: '',
remark: '',
});
function openCreate() {
createForm.appKey = '';
createForm.appName = '';
createForm.authMode = 'ANON';
createForm.expireAt = '';
createForm.remark = '';
createShow.value = true;
}
async function submitCreate() {
if (!createForm.appKey || !createForm.appKey.trim()) {
Message.warning('appKey 必填');
return;
}
if (createForm.authMode === 'ANON' && !createForm.expireAt.trim()) {
Message.warning('authMode=ANON 时免签到期必填(≤31 天)');
return;
}
creating.value = true;
try {
const d: any = await Apis.GatewayAdmin.createApp({ data: { ...createForm } });
Message.success(`已创建,应用表加载 ${d?.apps ?? '-'} 条`);
createShow.value = false;
tableRef.value?.reloadData('add' as any);
} catch (e: any) {
// 错误提示由 alova 全局 onBizError 弹出
} finally {
creating.value = false;
}
}
</script>
<style lang="scss" scoped>
.search-wrap {
line-height: 32px;
.tip {
color: #848c98;
font-size: 13px;
b {
color: #ed4014;
}
}
.ml8 {
margin-left: 8px;
}
}
.mono {
font-family: Menlo, Consolas, monospace;
}
.window-tag {
color: #ff9900;
font-family: Menlo, Consolas, monospace;
font-weight: 600;
}
.expire-hint {
font-size: 12px;
line-height: 16px;
}
.expire-none {
color: #c5c8ce;
}
.expire-warning,
.expire-warning.expire-hint {
color: #ff9900;
}
.expire-danger,
.expire-danger.expire-hint {
color: #ed4014;
}
.form-tip {
margin: -6px 0 0 110px;
font-size: 12px;
color: #848c98;
line-height: 18px;
}
</style>