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:
@@ -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>
|
||||
Reference in New Issue
Block a user