feat(密钥管理): 按数据权限分组隔离与筛选

- 后台密钥列表改为按 biz_shop_manage_group 分组:主管只看自己带的分组成员,超管看全量可按 group_id 筛选
- 行数据补「所属分组」名称(批量查询);列表接口带回分组筛选项
- 修复超管筛选不生效:前端筛选参数统一 snake_case(camel 被后端静默忽略)
- 列表列/筛选项文案由「所属管理员」改为「分组」
This commit is contained in:
2026-09-13 13:34:08 +08:00
parent 05ae0c62d6
commit 1b733b23b9
10 changed files with 251 additions and 96 deletions
+14 -9
View File
@@ -19,8 +19,8 @@ export interface AdminUserSecretModule {
export interface AdminUserSecretRow {
userId: number
username: string
createdById: number | null
createdByUsername: string
/** 所属数据权限分组名(可能多个)。 */
groups: string[]
similarAsin: AdminUserSecretModule
appearancePatent: AdminUserSecretModule
proxy: AdminUserSecretModule
@@ -34,26 +34,31 @@ export interface AdminUserSecretPage {
total: number
page: number
pageSize: number
/** 分组筛选项(超管=全部;主管=自己带的分组)。 */
groupOptions?: GroupOption[]
}
/** 管理员下拉项(超管筛选用)。 */
export interface AdminOption {
export interface GroupOption {
id: number
username: string
groupName: string
}
export interface UserSecretQuery {
keyword?: string
checkStatus?: string
/** 按创建人筛选(仅超管生效)。 */
createdById?: number
/** 按数据权限分组筛选(仅超管生效);后端参数为 snake_case。 */
groupId?: number
page: number
pageSize: number
}
/** 分页查询用户密钥(一行一用户):GET /api/admin/user-secrets */
/** 分页查询用户密钥(一行一用户):GET /api/admin/user-secrets(筛选参数必须 snake_case,camel 会被后端静默忽略)。 */
export async function fetchUserSecretList(params: UserSecretQuery): Promise<AdminUserSecretPage> {
const { data } = await http.get('/api/admin/user-secrets', { params })
const query: Record<string, string | number> = { page: params.page, pageSize: params.pageSize }
if (params.keyword) query.keyword = params.keyword
if (params.checkStatus) query.checkStatus = params.checkStatus
if (params.groupId) query.group_id = params.groupId
const { data } = await http.get('/api/admin/user-secrets', { params: query })
return unwrap<AdminUserSecretPage>(data)
}
@@ -3,17 +3,15 @@ import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { formatDateTime } from '@/utils/datetime'
import OldPagination from '@/components/OldPagination.vue'
import { useAdminSessionStore } from '@/stores/admin-session'
import { fetchUserList } from '@/api/users'
import {
checkUserSecret,
deleteUserSecret,
fetchUserSecretList,
type AdminUserSecretModule,
type AdminUserSecretRow,
type GroupOption,
} from '@/api/user-secrets'
const session = useAdminSessionStore()
const loading = ref(false)
const rows = ref<AdminUserSecretRow[]>([])
const total = ref(0)
@@ -21,9 +19,9 @@ const page = ref(1)
const pageSize = ref(15)
const keyword = ref('')
const statusFilter = ref('')
const createdByIdFilter = ref<number | null>(null)
/** 管理员下拉(超管可见/加载)。 */
const adminOptions = ref<Array<{ id: number; username: string }>>([])
const groupFilter = ref<number | null>(null)
/** 分组下拉(超管=全部;主管=自己带的分组,由列表接口带回)。 */
const groupOptions = ref<GroupOption[]>([])
/** 正在检测的行 userId,用于按钮 loading 态。 */
const checkingId = ref<number | null>(null)
@@ -91,28 +89,19 @@ function rowStatusTooltip(row: AdminUserSecretRow) {
return parts.join('') || '暂无检测记录'
}
async function loadAdminOptions() {
if (!session.isSuperAdmin) return
try {
const result = await fetchUserList({ page: 1, pageSize: 999, role: 'admin' })
adminOptions.value = result.admins || []
} catch (error) {
console.warn('[user-secrets] 管理员下拉加载失败', error)
}
}
async function load() {
loading.value = true
try {
const result = await fetchUserSecretList({
keyword: keyword.value.trim() || undefined,
checkStatus: statusFilter.value || undefined,
createdById: createdByIdFilter.value || undefined,
groupId: groupFilter.value || undefined,
page: page.value,
pageSize: pageSize.value,
})
rows.value = result?.items || []
total.value = Number(result?.total || 0)
groupOptions.value = result?.groupOptions || []
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '加载失败')
} finally {
@@ -128,7 +117,7 @@ function search() {
function reset() {
keyword.value = ''
statusFilter.value = ''
createdByIdFilter.value = null
groupFilter.value = null
page.value = 1
load()
}
@@ -183,10 +172,7 @@ function changeSize(size: number) {
load()
}
onMounted(() => {
loadAdminOptions()
load()
})
onMounted(load)
</script>
<template>
@@ -207,11 +193,11 @@ onMounted(() => {
<option v-for="option in STATUS_OPTIONS" :key="option.value" :value="option.value">{{ option.label }}</option>
</select>
</div>
<div class="form-group" v-if="session.isSuperAdmin" style="min-width: 170px">
<label>所属管理员</label>
<select v-model="createdByIdFilter">
<option :value="null">全部管理员</option>
<option v-for="admin in adminOptions" :key="admin.id" :value="admin.id">{{ admin.username }}</option>
<div class="form-group" v-if="groupOptions.length" style="min-width: 170px">
<label>分组</label>
<select v-model="groupFilter">
<option :value="null">全部分组</option>
<option v-for="group in groupOptions" :key="group.id" :value="group.id">{{ group.groupName }}</option>
</select>
</div>
<div class="form-group">
@@ -229,7 +215,7 @@ onMounted(() => {
<tr>
<th style="width: 58px">序号</th>
<th style="width: 260px">用户</th>
<th style="width: 130px">所属管理员</th>
<th style="width: 130px">分组</th>
<th style="width: 230px">货源查询密钥</th>
<th style="width: 230px">外观专利密钥</th>
<th style="width: 230px">代理设置</th>
@@ -245,7 +231,7 @@ onMounted(() => {
<span class="user-name">{{ row.username || '—' }}</span>
</td>
<td>
<span class="creator-name">{{ row.createdByUsername || (row.createdById ? `UID ${row.createdById}` : '—') }}</span>
<span class="creator-name">{{ row.groups?.length ? row.groups.join('、') : '—' }}</span>
</td>
<td>
<div class="module-cell">
@@ -296,7 +282,7 @@ onMounted(() => {
<td colspan="8" class="empty-tip">加载中...</td>
</tr>
<tr v-else>
<td colspan="8" class="empty-tip">{{ keyword || statusFilter || createdByIdFilter ? '暂无匹配记录' : '暂无用户密钥记录' }}</td>
<td colspan="8" class="empty-tip">{{ keyword || statusFilter || groupFilter ? '暂无匹配记录' : '暂无用户密钥记录' }}</td>
</tr>
</tbody>
</table>