add
This commit is contained in:
313
src/pages/admin/query-whitelist/index.vue
Normal file
313
src/pages/admin/query-whitelist/index.vue
Normal file
@@ -0,0 +1,313 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user