Files
tyapi-frontend/src/pages/admin/query-whitelist/index.vue
2026-06-18 21:16:07 +08:00

314 lines
9.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<ListPageLayout
title="查询白名单"
subtitle="配置命中后返回「查询为空」的规则;「全部接口」仅作用于入参必填身份证的 API"
>
<template #actions>
<div class="flex flex-wrap gap-2">
<el-button :loading="importing" @click="handleImportLegacy">
导入历史硬编码
</el-button>
<el-button type="primary" @click="handleCreate">
<PlusIcon class="w-4 h-4 mr-1" />
新增规则
</el-button>
</div>
</template>
<template #filters>
<FilterSection>
<FilterItem label="生效范围">
<el-select v-model="filters.scope" clearable placeholder="全部" class="w-full" @change="handleFilterChange">
<el-option label="全局规则" value="global" />
<el-option label="指定用户" value="user" />
</el-select>
</FilterItem>
<FilterItem label="用户ID">
<el-input v-model="filters.user_id" placeholder="用户 UUID 或 *" clearable class="w-full" />
</FilterItem>
<FilterItem label="状态">
<el-select v-model="filters.status" clearable placeholder="全部" class="w-full" @change="handleFilterChange">
<el-option label="启用" value="enabled" />
<el-option label="禁用" value="disabled" />
</el-select>
</FilterItem>
<FilterItem label="API编码">
<el-input v-model="filters.api_code" placeholder="如 FLXG0V4B" clearable class="w-full" />
</FilterItem>
<FilterItem label="关键词">
<el-input
v-model="filters.keyword"
placeholder="姓名 / 备注 / 脱敏身份证"
clearable
class="w-full"
@input="handleSearch"
/>
</FilterItem>
<template #stats>
{{ total }} 条规则
</template>
<template #buttons>
<el-button @click="resetFilters">重置</el-button>
<el-button type="primary" @click="loadEntries">查询</el-button>
</template>
</FilterSection>
</template>
<template #table>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<el-table v-loading="loading" :data="entries" stripe class="w-full">
<el-table-column label="生效范围" width="110">
<template #default="{ row }">
<el-tag :type="row.is_global ? 'warning' : 'primary'" size="small">
{{ row.is_global ? '全局' : '指定用户' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="user_id" label="用户ID" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
<span class="font-mono text-xs">{{ row.is_global ? '*' : row.user_id }}</span>
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" width="100">
<template #default="{ row }">
<span>{{ row.name === '*' ? '任意(*)' : row.name }}</span>
</template>
</el-table-column>
<el-table-column prop="id_card_masked" label="身份证" width="180">
<template #default="{ row }">
<span class="font-mono">{{ row.id_card_masked }}</span>
</template>
</el-table-column>
<el-table-column label="生效接口" min-width="160">
<template #default="{ row }">
<template v-if="row.api_codes?.includes('*')">
<el-tag size="small" type="info">全部接口</el-tag>
</template>
<template v-else>
<el-tag v-for="code in row.api_codes" :key="code" size="small" class="mr-1 mb-1">{{ code }}</el-tag>
</template>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="90">
<template #default="{ row }">
<el-switch
:model-value="row.status === 'enabled'"
active-text="启用"
inactive-text="禁用"
inline-prompt
@change="(val) => handleToggleStatus(row, val)"
/>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
<el-table-column prop="created_at" label="创建时间" width="170">
<template #default="{ row }">{{ formatDate(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<template #pagination>
<el-pagination
v-if="total > 0"
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</template>
<template #extra>
<QueryWhitelistFormDialog
v-model="showFormDialog"
:entry="currentEntry"
@success="loadEntries"
/>
</template>
</ListPageLayout>
</template>
<script setup>
import { queryWhitelistApi } from '@/api/queryWhitelist.js'
import FilterItem from '@/components/common/FilterItem.vue'
import FilterSection from '@/components/common/FilterSection.vue'
import ListPageLayout from '@/components/common/ListPageLayout.vue'
import { PlusIcon } from '@heroicons/vue/24/outline'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, reactive, ref } from 'vue'
import QueryWhitelistFormDialog from './components/QueryWhitelistFormDialog.vue'
const loading = ref(false)
const importing = ref(false)
const entries = ref([])
const total = ref(0)
const showFormDialog = ref(false)
const currentEntry = ref(null)
const filters = reactive({
scope: '',
user_id: '',
status: '',
api_code: '',
keyword: ''
})
const pagination = reactive({
page: 1,
pageSize: 20
})
let searchTimer = null
const buildParams = () => {
const params = {
page: pagination.page,
page_size: pagination.pageSize
}
if (filters.status) params.status = filters.status
if (filters.api_code) params.api_code = filters.api_code
if (filters.keyword) params.keyword = filters.keyword
if (filters.scope === 'global') {
params.user_id = '*'
} else if (filters.scope === 'user') {
params.user_id = filters.user_id || undefined
if (!filters.user_id) {
// 指定用户但未填 ID 时,排除全局需在服务端支持;这里用非 * 过滤需填 user_id
}
} else if (filters.user_id) {
params.user_id = filters.user_id
}
return params
}
const loadEntries = async () => {
loading.value = true
try {
const res = await queryWhitelistApi.getEntries(buildParams())
entries.value = res.data?.items || []
total.value = res.data?.total || 0
} catch (e) {
console.error(e)
ElMessage.error('加载查询白名单失败')
} finally {
loading.value = false
}
}
const handleFilterChange = () => {
pagination.page = 1
loadEntries()
}
const handleSearch = () => {
if (searchTimer) clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
pagination.page = 1
loadEntries()
}, 400)
}
const resetFilters = () => {
Object.assign(filters, { scope: '', user_id: '', status: '', api_code: '', keyword: '' })
pagination.page = 1
loadEntries()
}
const handleSizeChange = (size) => {
pagination.pageSize = size
pagination.page = 1
loadEntries()
}
const handleCurrentChange = (page) => {
pagination.page = page
loadEntries()
}
const handleCreate = () => {
currentEntry.value = null
showFormDialog.value = true
}
const handleEdit = (row) => {
currentEntry.value = { ...row }
showFormDialog.value = true
}
const handleToggleStatus = async (row, enabled) => {
const status = enabled ? 'enabled' : 'disabled'
try {
await queryWhitelistApi.updateEntryStatus(row.id, status)
row.status = status
ElMessage.success(enabled ? '已启用' : '已禁用')
} catch (e) {
ElMessage.error(e.message || '状态更新失败')
loadEntries()
}
}
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm(
`确定删除该规则?(${row.id_card_masked} / ${row.name === '*' ? '任意姓名' : row.name}`,
'确认删除',
{ type: 'warning' }
)
await queryWhitelistApi.deleteEntry(row.id)
ElMessage.success('删除成功')
loadEntries()
} catch (e) {
if (e !== 'cancel') ElMessage.error(e.message || '删除失败')
}
}
const handleImportLegacy = async () => {
try {
await ElMessageBox.confirm(
'将导入原 processor 中 17 条硬编码身份证为「全局规则」user_id=*name=*,全部接口)。已存在的会自动跳过。',
'导入历史硬编码',
{ type: 'info', confirmButtonText: '开始导入' }
)
importing.value = true
const res = await queryWhitelistApi.importLegacy()
const { imported, skipped, total: t } = res.data || {}
ElMessage.success(`导入完成:新增 ${imported} 条,跳过 ${skipped} 条,共 ${t}`)
loadEntries()
} catch (e) {
if (e !== 'cancel') ElMessage.error(e.message || '导入失败')
} finally {
importing.value = false
}
}
const formatDate = (dateString) => {
if (!dateString) return '-'
return new Date(dateString).toLocaleString('zh-CN')
}
onMounted(loadEntries)
</script>