This commit is contained in:
2026-06-19 14:39:55 +08:00
parent 224aa52b7f
commit cef31fb7b7
4 changed files with 427 additions and 0 deletions

View File

@@ -1,2 +1,3 @@
export * from './feature';
export * from './product';
export * from './query-whitelist';

View File

@@ -0,0 +1,80 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace QueryWhitelistApi {
export interface OpRequest {
name: string;
id_card: string;
api_codes: string[];
remark?: string;
}
export interface EntryResult {
id: string;
name: string;
id_card_masked: string;
api_codes: string[];
status: string;
remark: string;
created_at: string;
updated_at: string;
}
export interface OpResponse {
tianyuan_code: number;
tianyuan_message: string;
transaction_id?: string;
entry?: EntryResult;
}
export interface OpLogItem {
id: number;
admin_user_id: number;
action: string;
name: string;
id_card: string;
id_card_masked: string;
api_codes: string[];
remark: string;
tianyuan_code: number;
tianyuan_message: string;
transaction_id: string;
entry_id: string;
entry_status: string;
entry_api_codes: string[];
create_time: string;
}
export interface OpLogList {
total: number;
items: OpLogItem[];
}
}
async function createQueryWhitelist(data: QueryWhitelistApi.OpRequest) {
return requestClient.post<QueryWhitelistApi.OpResponse>(
'/query-whitelist/create',
data,
);
}
async function appendQueryWhitelist(data: QueryWhitelistApi.OpRequest) {
return requestClient.post<QueryWhitelistApi.OpResponse>(
'/query-whitelist/append',
data,
);
}
async function getQueryWhitelistOpLogList(params: Recordable<any>) {
return requestClient.get<QueryWhitelistApi.OpLogList>(
'/query-whitelist/op-log/list',
{ params },
);
}
export {
appendQueryWhitelist,
createQueryWhitelist,
getQueryWhitelistOpLogList,
};

View File

@@ -0,0 +1,152 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
export interface QueryWhitelistOpLogItem {
id: number;
admin_user_id: number;
action: string;
name: string;
id_card: string;
id_card_masked: string;
api_codes: string[];
remark: string;
tianyuan_code: number;
tianyuan_message: string;
transaction_id: string;
entry_id: string;
entry_status: string;
entry_api_codes: string[];
create_time: string;
}
export function useFormSchema(
featureOptions: { label: string; value: string }[],
): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'id_card',
label: '身份证号',
rules: z
.string()
.min(1, '请输入身份证号')
.regex(/^\d{17}[\dXx]$/, '请输入18位有效身份证号'),
},
{
component: 'Input',
fieldName: 'name',
label: '姓名',
defaultValue: '*',
componentProps: {
placeholder: '填 * 表示只按身份证匹配',
},
rules: z.string().min(1, '请输入姓名'),
},
{
component: 'Select',
fieldName: 'api_codes',
label: '产品编码',
componentProps: {
mode: 'multiple',
options: featureOptions,
placeholder: '请选择产品编码',
showSearch: true,
optionFilterProp: 'label',
maxTagCount: 4,
},
rules: z.array(z.string()).min(1, '请至少选择一个产品编码'),
},
{
component: 'Textarea',
fieldName: 'remark',
label: '备注',
componentProps: {
maxlength: 500,
rows: 2,
showCount: true,
placeholder: '可选备注',
},
},
];
}
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'id_card',
label: '身份证号',
},
{
component: 'Select',
fieldName: 'action',
label: '操作类型',
componentProps: {
allowClear: true,
options: [
{ label: '创建规则', value: 'create' },
{ label: '追加接口', value: 'append' },
],
},
},
{
component: 'Select',
fieldName: 'tianyuan_result',
label: '业务结果',
componentProps: {
allowClear: true,
options: [
{ label: '成功', value: 'success' },
{ label: '失败', value: 'fail' },
],
},
},
];
}
export function useColumns(): VxeTableGridOptions['columns'] {
return [
{ field: 'id', title: 'ID', width: 80 },
{ field: 'create_time', title: '操作时间', width: 170 },
{
field: 'action',
title: '操作类型',
width: 100,
formatter: ({ cellValue }) =>
cellValue === 'create' ? '创建规则' : '追加接口',
},
{ field: 'id_card', title: '身份证号', width: 180 },
{ field: 'id_card_masked', title: '脱敏身份证', width: 160 },
{ field: 'name', title: '姓名规则', width: 100 },
{
field: 'api_codes',
title: '提交编码',
minWidth: 180,
formatter: ({ cellValue }) =>
Array.isArray(cellValue) ? cellValue.join(', ') : '',
},
{
field: 'tianyuan_code',
title: '天远码',
width: 90,
cellRender: {
name: 'CellTag',
options: [
{ value: 0, color: 'success', label: '0 成功' },
],
},
},
{ field: 'tianyuan_message', title: '天远描述', minWidth: 160 },
{ field: 'transaction_id', title: '流水号', minWidth: 200 },
{
field: 'entry_api_codes',
title: '规则当前编码',
minWidth: 180,
formatter: ({ cellValue }) =>
Array.isArray(cellValue) ? cellValue.join(', ') : '',
},
{ field: 'remark', title: '备注', minWidth: 120 },
];
}

View File

@@ -0,0 +1,194 @@
<script lang="ts" setup>
import type { QueryWhitelistApi } from '#/api/product-manage';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { computed, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Alert, Button, Card, Descriptions, message, Space } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
appendQueryWhitelist,
createQueryWhitelist,
getFeatureList,
getQueryWhitelistOpLogList,
} from '#/api/product-manage';
import { useColumns, useFormSchema, useGridFormSchema } from './data';
const submitting = ref(false);
const lastResult = ref<QueryWhitelistApi.OpResponse | null>(null);
const featureOptions = ref<{ label: string; value: string }[]>([]);
const featureSelectOptions = computed(() => featureOptions.value);
async function loadFeatures() {
const { items } = await getFeatureList({ page: 1, pageSize: 500 });
featureOptions.value = (items ?? []).map((item) => ({
label: `${item.api_id}${item.name}`,
value: item.api_id,
}));
}
const [ConfigForm, configFormApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
},
schema: computed(() => useFormSchema(featureSelectOptions.value)),
showDefaultActions: false,
wrapperClass: 'grid-cols-1 md:grid-cols-2',
});
async function submitAction(action: 'append' | 'create') {
const { valid } = await configFormApi.validate();
if (!valid) {
return;
}
const values = await configFormApi.getValues();
submitting.value = true;
try {
const payload = {
name: values.name || '*',
id_card: values.id_card,
api_codes: values.api_codes,
remark: values.remark || '',
};
const result =
action === 'create'
? await createQueryWhitelist(payload)
: await appendQueryWhitelist(payload);
lastResult.value = result;
if (result.tianyuan_code === 0) {
message.success(result.tianyuan_message || '操作成功');
} else {
message.warning(result.tianyuan_message || '天远 API 返回失败');
}
await gridApi.query();
} catch (error: any) {
message.error(error?.message || '操作失败');
} finally {
submitting.value = false;
}
}
function selectJudicialFeatures() {
const judicialCodes = featureOptions.value
.filter((item) => item.label.includes('司法'))
.map((item) => item.value);
configFormApi.setFieldValue('api_codes', judicialCodes);
}
function clearApiCodes() {
configFormApi.setFieldValue('api_codes', []);
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
submitOnChange: false,
},
gridOptions: {
columns: useColumns(),
minHeight: 500,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getQueryWhitelistOpLogList({
page: page.currentPage,
page_size: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
} as VxeTableGridOptions,
});
onMounted(() => {
loadFeatures().catch(() => {
message.error('加载模块列表失败');
});
});
</script>
<template>
<Page auto-content-height>
<div class="flex flex-col gap-4">
<div class="grid grid-cols-1 gap-4 xl:grid-cols-3">
<Card class="xl:col-span-2" title="查询白名单配置" :bordered="false">
<ConfigForm />
<div class="mt-4 flex flex-wrap gap-2">
<Button type="primary" :loading="submitting" @click="submitAction('create')">
创建规则
</Button>
<Button :loading="submitting" @click="submitAction('append')">
追加接口
</Button>
<Button @click="selectJudicialFeatures">一键选中司法</Button>
<Button @click="clearApiCodes">清空编码</Button>
</div>
</Card>
<Card title="最近一次天远返回" :bordered="false">
<template v-if="lastResult">
<Alert
:type="lastResult.tianyuan_code === 0 ? 'success' : 'warning'"
:message="`${lastResult.tianyuan_code} ${lastResult.tianyuan_message}`"
show-icon
class="mb-3"
/>
<Descriptions :column="1" size="small" bordered>
<Descriptions.Item label="流水号">
{{ lastResult.transaction_id || '-' }}
</Descriptions.Item>
<template v-if="lastResult.entry">
<Descriptions.Item label="规则ID">
{{ lastResult.entry.id }}
</Descriptions.Item>
<Descriptions.Item label="脱敏身份证">
{{ lastResult.entry.id_card_masked }}
</Descriptions.Item>
<Descriptions.Item label="规则状态">
{{ lastResult.entry.status }}
</Descriptions.Item>
<Descriptions.Item label="当前编码">
<Space wrap>
<span
v-for="code in lastResult.entry.api_codes"
:key="code"
class="text-xs"
>
{{ code }}
</span>
</Space>
</Descriptions.Item>
</template>
</Descriptions>
</template>
<div v-else class="text-gray-400">提交后将在此展示天远 API 返回结果</div>
</Card>
</div>
<Card title="操作记录" :bordered="false">
<Grid table-title="操作记录列表" />
</Card>
</div>
</Page>
</template>