feat(需求): ①用户管理补「角色」筛选(前端+后端 role 参数) ②软件/数字人版本页补版本号搜索 ③全站 el-select 统一 filterable(分组/所属管理员/角色等大下拉可模糊搜索)

This commit is contained in:
2026-09-06 23:43:00 +08:00
parent 970affec95
commit f00fdcd42a
10 changed files with 115 additions and 25 deletions
+4
View File
@@ -13,6 +13,8 @@ export interface UserListParams {
keyword?: string keyword?: string
createdById?: number | null createdById?: number | null
adminId?: number | null adminId?: number | null
/** 角色过滤(''=全部)。 */
role?: string
} }
/** 序列化到 GET /api/admin/users 的查询参数(Java snake_case)。 */ /** 序列化到 GET /api/admin/users 的查询参数(Java snake_case)。 */
@@ -23,6 +25,7 @@ export interface UserListQuery {
search?: string search?: string
created_by_id?: number created_by_id?: number
admin_id?: number admin_id?: number
role?: string
} }
/** 用户列表项即 AdminUser(与 Java AdminUserItemVo 字段对齐)。 */ /** 用户列表项即 AdminUser(与 Java AdminUserItemVo 字段对齐)。 */
@@ -67,6 +70,7 @@ export function toUserListQuery(params: UserListParams): UserListQuery {
if (params.keyword) query.username = params.keyword if (params.keyword) query.username = params.keyword
if (params.createdById != null) query.created_by_id = params.createdById if (params.createdById != null) query.created_by_id = params.createdById
if (params.adminId != null) query.admin_id = params.adminId if (params.adminId != null) query.admin_id = params.adminId
if (params.role) query.role = params.role
return query return query
} }
@@ -93,7 +93,7 @@ async function create(): Promise<void> {
/> />
</el-form-item> </el-form-item>
<el-form-item v-if="operatorSuper" label="角色"> <el-form-item v-if="operatorSuper" label="角色">
<el-select v-model="form.role" style="width: 200px"> <el-select filterable v-model="form.role" style="width: 200px">
<el-option v-for="opt in ROLE_CHOICES" :key="opt.value" :label="opt.label" :value="opt.value" /> <el-option v-for="opt in ROLE_CHOICES" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -295,12 +295,12 @@ onMounted(loadMenus)
<el-input v-model="menuForm.name" placeholder="如:用户管理" /> <el-input v-model="menuForm.name" placeholder="如:用户管理" />
</el-form-item> </el-form-item>
<el-form-item label="菜单类型"> <el-form-item label="菜单类型">
<el-select v-model="menuForm.menuType" style="width: 100%" @change="onMenuTypeChange"> <el-select filterable v-model="menuForm.menuType" style="width: 100%" @change="onMenuTypeChange">
<el-option v-for="opt in MENU_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" /> <el-option v-for="opt in MENU_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="父菜单"> <el-form-item label="父菜单">
<el-select v-model="menuForm.parentId" clearable placeholder="无(一级菜单)" style="width: 100%"> <el-select filterable v-model="menuForm.parentId" clearable placeholder="无(一级菜单)" style="width: 100%">
<el-option <el-option
v-for="node in parentOptions" v-for="node in parentOptions"
:key="node.id" :key="node.id"
@@ -139,10 +139,17 @@ onMounted(loadUsers)
</div> </div>
<div v-if="isSuperAdmin" class="form-group" style="min-width: 160px"> <div v-if="isSuperAdmin" class="form-group" style="min-width: 160px">
<label>所属管理员</label> <label>所属管理员</label>
<select v-model="filters.createdByAdminId"> <el-select v-model="filters.createdByAdminId" class="admin-filter" filterable clearable placeholder="全部" @change="search">
<option value="">全部</option> <el-option v-for="admin in admins" :key="admin.id" :label="admin.username" :value="admin.id" />
<option v-for="admin in admins" :key="admin.id" :value="admin.id">{{ admin.username }}</option> </el-select>
</select> </div>
<div class="form-group" style="min-width: 140px">
<label>角色</label>
<el-select v-model="filters.role" class="admin-filter" filterable clearable placeholder="全部角色" @change="search">
<el-option label="超级管理员" value="super_admin" />
<el-option label="管理员" value="admin" />
<el-option label="普通账号" value="normal" />
</el-select>
</div> </div>
<button class="btn" type="button" @click="search">查询</button> <button class="btn" type="button" @click="search">查询</button>
</div> </div>
@@ -7,18 +7,20 @@ export interface UserFilterState {
keyword: string keyword: string
/** 按创建管理员过滤(可选)。 */ /** 按创建管理员过滤(可选)。 */
createdByAdminId: number | null createdByAdminId: number | null
/** 角色过滤(''=全部;super_admin/admin/normal)。 */
role: string
page: number page: number
pageSize: number pageSize: number
} }
export function createUserFilterState(): UserFilterState { export function createUserFilterState(): UserFilterState {
return { keyword: '', createdByAdminId: null, page: USER_PAGE_MIN_PAGE, pageSize: USER_PAGE_DEFAULT_SIZE } return { keyword: '', createdByAdminId: null, role: '', page: USER_PAGE_MIN_PAGE, pageSize: USER_PAGE_DEFAULT_SIZE }
} }
/** 修改筛选条件时重置回第一页(避免停在可能越界的旧页)。 */ /** 修改筛选条件时重置回第一页(避免停在可能越界的旧页)。 */
export function withFilterReset( export function withFilterReset(
state: UserFilterState, state: UserFilterState,
next: Partial<Pick<UserFilterState, 'keyword' | 'createdByAdminId'>>, next: Partial<Pick<UserFilterState, 'keyword' | 'createdByAdminId' | 'role'>>,
): UserFilterState { ): UserFilterState {
return { ...state, ...next, page: USER_PAGE_MIN_PAGE } return { ...state, ...next, page: USER_PAGE_MIN_PAGE }
} }
@@ -36,10 +38,13 @@ export function isFilterActive(state: UserFilterState): boolean {
/** 页面状态 → 查询适配层参数(空关键字不下发)。 */ /** 页面状态 → 查询适配层参数(空关键字不下发)。 */
export function toUserListParams(state: UserFilterState): UserListParams { export function toUserListParams(state: UserFilterState): UserListParams {
const keyword = state.keyword.trim() const keyword = state.keyword.trim()
return { const params: UserListParams = {
page: state.page, page: state.page,
pageSize: state.pageSize, pageSize: state.pageSize,
keyword: keyword || undefined, keyword: keyword || undefined,
createdById: state.createdByAdminId, createdById: state.createdByAdminId,
} }
const role = (state.role || '').trim()
if (role) params.role = role
return params
} }
@@ -426,7 +426,7 @@ onMounted(() => {
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="国家"> <el-form-item label="国家">
<el-select v-model="createCountry" placeholder="请选择国家" clearable style="width: 100%"> <el-select filterable v-model="createCountry" placeholder="请选择国家" clearable style="width: 100%">
<el-option v-for="code in QUERY_ASIN_COUNTRIES" :key="code" :label="asinCountryLabel(code)" :value="code" /> <el-option v-for="code in QUERY_ASIN_COUNTRIES" :key="code" :label="asinCountryLabel(code)" :value="code" />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -461,7 +461,7 @@ onMounted(() => {
<el-dialog v-model="importVisible" :title="importMode === 'add' ? '导入添加查询 ASIN' : '删除导入查询 ASIN'" width="520px"> <el-dialog v-model="importVisible" :title="importMode === 'add' ? '导入添加查询 ASIN' : '删除导入查询 ASIN'" width="520px">
<el-form label-position="top"> <el-form label-position="top">
<el-form-item label="分组(Excel 未提供时的兜底,可选)"> <el-form-item label="分组(Excel 未提供时的兜底,可选)">
<el-select v-model="importGroupId" placeholder="可不选" clearable style="width: 100%"> <el-select filterable v-model="importGroupId" placeholder="可不选" clearable style="width: 100%">
<el-option v-for="group in groups" :key="group.id" :label="group.groupName" :value="group.id" /> <el-option v-for="group in groups" :key="group.id" :label="group.groupName" :value="group.id" />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -473,7 +473,7 @@ onMounted(() => {
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="国家"> <el-form-item label="国家">
<el-select v-model="createCountry" placeholder="请选择国家" clearable style="width: 100%"> <el-select filterable v-model="createCountry" placeholder="请选择国家" clearable style="width: 100%">
<el-option v-for="code in COUNTRIES" :key="code" :label="asinCountryLabel(code)" :value="code" /> <el-option v-for="code in COUNTRIES" :key="code" :label="asinCountryLabel(code)" :value="code" />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -514,7 +514,7 @@ onMounted(() => {
<el-dialog v-model="importVisible" :title="importMode === 'add' ? '导入最低价 ASIN' : '删除导入最低价 ASIN'" width="520px"> <el-dialog v-model="importVisible" :title="importMode === 'add' ? '导入最低价 ASIN' : '删除导入最低价 ASIN'" width="520px">
<el-form label-position="top"> <el-form label-position="top">
<el-form-item label="分组(必选)"> <el-form-item label="分组(必选)">
<el-select v-model="importGroupId" placeholder="请选择分组" style="width: 100%"> <el-select filterable v-model="importGroupId" placeholder="请选择分组" style="width: 100%">
<el-option v-for="group in groups" :key="group.id" :label="group.groupName" :value="group.id" /> <el-option v-for="group in groups" :key="group.id" :label="group.groupName" :value="group.id" />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -29,11 +29,18 @@ const items = ref<DigitalHumanVersion[]>([])
/** 分页(对齐 admin.js:6311 pageSize=20 + 数字页码 + 跳转;Java 列表接口不分页,前端分页呈现)。 */ /** 分页(对齐 admin.js:6311 pageSize=20 + 数字页码 + 跳转;Java 列表接口不分页,前端分页呈现)。 */
const pageSize = 20 const pageSize = 20
const page = ref(1) const page = ref(1)
/** 版本号搜索(客户端过滤)。 */
const versionKeyword = ref('')
const filteredItems = computed(() => {
const kw = (versionKeyword.value || '').trim().toLowerCase()
if (!kw) return items.value
return items.value.filter((item) => `${item.version}`.toLowerCase().includes(kw))
})
const pagedItems = computed(() => { const pagedItems = computed(() => {
const start = (page.value - 1) * pageSize const start = (page.value - 1) * pageSize
return items.value.slice(start, start + pageSize) return filteredItems.value.slice(start, start + pageSize)
}) })
const totalPages = computed(() => Math.max(1, Math.ceil(items.value.length / pageSize))) const totalPages = computed(() => Math.max(1, Math.ceil(filteredItems.value.length / pageSize)))
const jumpPage = ref('') const jumpPage = ref('')
const uploadVisible = ref(false) const uploadVisible = ref(false)
@@ -191,7 +198,10 @@ onMounted(load)
<section class="panel-box"> <section class="panel-box">
<div class="dh-head"> <div class="dh-head">
<h3>版本列表</h3> <h3>版本列表</h3>
<button class="btn" type="button" @click="uploadVisible = true">上传新版本</button> <div class="dh-head-tools">
<input v-model="versionKeyword" type="text" placeholder="搜索版本号" class="dh-keyword" />
<button class="btn" type="button" @click="uploadVisible = true">上传新版本</button>
</div>
</div> </div>
<div class="table-scroll digital-human-version-table-scroll"> <div class="table-scroll digital-human-version-table-scroll">
@@ -242,13 +252,13 @@ onMounted(load)
<td colspan="8" class="empty-tip">加载中...</td> <td colspan="8" class="empty-tip">加载中...</td>
</tr> </tr>
<tr v-else> <tr v-else>
<td colspan="8" class="empty-tip">暂无版本记录</td> <td colspan="8" class="empty-tip">{{ versionKeyword ? '暂无匹配版本' : '暂无版本记录' }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div v-if="items.length > pageSize" class="pagination"> <div v-if="filteredItems.length > pageSize" class="pagination">
<button type="button" :disabled="page <= 1" @click="changePage(page - 1)">上一页</button> <button type="button" :disabled="page <= 1" @click="changePage(page - 1)">上一页</button>
<span class="page-nums"> <span class="page-nums">
<template v-for="n in totalPages" :key="n"> <template v-for="n in totalPages" :key="n">
@@ -322,6 +332,33 @@ h3 {
gap: 12px; gap: 12px;
margin-bottom: 16px; margin-bottom: 16px;
} }
.dh-head-tools {
display: flex;
align-items: center;
gap: 10px;
}
.dh-keyword {
width: 200px;
min-height: 42px;
padding: 6px 12px;
border: 1px solid #cbd9e6;
border-radius: 9px;
background: #f8fbfd;
color: #24384d;
font-size: 13.5px;
font-family: inherit;
color-scheme: light;
outline: none;
transition: border-color 160ms ease, box-shadow 160ms ease, background 160ms ease;
}
.dh-keyword:hover {
border-color: #9fb7cd;
}
.dh-keyword:focus {
background: #ffffff;
border-color: #5f85ad;
box-shadow: 0 0 0 3px rgba(95, 133, 173, 0.16);
}
.btn { .btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -2,7 +2,7 @@
import { formatDateTime } from '@/utils/datetime' import { formatDateTime } from '@/utils/datetime'
/** 软件版本管理页 · 像素复刻旧版 admin.html panel-version(自绘:面板头+上传新版本、版本列表、全量无分页同旧版)。 /** 软件版本管理页 · 像素复刻旧版 admin.html panel-version(自绘:面板头+上传新版本、版本列表、全量无分页同旧版)。
* script 逻辑沿用现有 Vue 实现(上传弹窗成功留驻含链接)。 */ * script 逻辑沿用现有 Vue 实现(上传弹窗成功留驻含链接)。 */
import { onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { fetchSoftwareVersions, uploadSoftwareVersion } from './version-api.ts' import { fetchSoftwareVersions, uploadSoftwareVersion } from './version-api.ts'
import type { SoftwareVersionItem } from './version-dto.ts' import type { SoftwareVersionItem } from './version-dto.ts'
@@ -10,6 +10,13 @@ import { isNonEmptyVersion } from './version-dto.ts'
const loading = ref(false) const loading = ref(false)
const items = ref<SoftwareVersionItem[]>([]) const items = ref<SoftwareVersionItem[]>([])
/** 版本号搜索(客户端过滤)。 */
const versionKeyword = ref('')
const filteredItems = computed(() => {
const kw = (versionKeyword.value || '').trim().toLowerCase()
if (!kw) return items.value
return items.value.filter((item) => `${item.version}`.toLowerCase().includes(kw))
})
const uploadVisible = ref(false) const uploadVisible = ref(false)
const uploading = ref(false) const uploading = ref(false)
@@ -83,7 +90,10 @@ onMounted(load)
<section class="panel-box"> <section class="panel-box">
<div class="version-head"> <div class="version-head">
<h3>版本列表</h3> <h3>版本列表</h3>
<button class="btn" type="button" @click="openUpload">上传新版本</button> <div class="version-head-tools">
<input v-model="versionKeyword" type="text" placeholder="搜索版本号" class="version-keyword" />
<button class="btn" type="button" @click="openUpload">上传新版本</button>
</div>
</div> </div>
<div class="table-scroll version-table-scroll"> <div class="table-scroll version-table-scroll">
@@ -97,8 +107,8 @@ onMounted(load)
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template v-if="items.length"> <template v-if="filteredItems.length">
<tr v-for="row in items" :key="row.version + row.id"> <tr v-for="row in filteredItems" :key="row.version + row.id">
<td>{{ row.version }}</td> <td>{{ row.version }}</td>
<td> <td>
<a v-if="row.fileUrl" class="link-cell" :href="row.fileUrl" target="_blank" rel="noopener" :title="row.fileUrl">{{ row.fileUrl }}</a> <a v-if="row.fileUrl" class="link-cell" :href="row.fileUrl" target="_blank" rel="noopener" :title="row.fileUrl">{{ row.fileUrl }}</a>
@@ -115,7 +125,7 @@ onMounted(load)
<td colspan="4" class="empty-tip">加载中...</td> <td colspan="4" class="empty-tip">加载中...</td>
</tr> </tr>
<tr v-else> <tr v-else>
<td colspan="4" class="empty-tip">暂无版本记录</td> <td colspan="4" class="empty-tip">{{ versionKeyword ? '暂无匹配版本' : '暂无版本记录' }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -174,6 +184,33 @@ h3 {
gap: 12px; gap: 12px;
margin-bottom: 16px; margin-bottom: 16px;
} }
.version-head-tools {
display: flex;
align-items: center;
gap: 10px;
}
.version-keyword {
width: 200px;
min-height: 42px;
padding: 6px 12px;
border: 1px solid #cbd9e6;
border-radius: 9px;
background: #f8fbfd;
color: #24384d;
font-size: 13.5px;
font-family: inherit;
color-scheme: light;
outline: none;
transition: border-color 160ms ease, box-shadow 160ms ease, background 160ms ease;
}
.version-keyword:hover {
border-color: #9fb7cd;
}
.version-keyword:focus {
background: #ffffff;
border-color: #5f85ad;
box-shadow: 0 0 0 3px rgba(95, 133, 173, 0.16);
}
.btn { .btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -291,7 +291,7 @@ onMounted(() => {
<el-dialog v-model="dialogVisible" :title="editingId == null ? '新增店铺' : '编辑店铺'" width="600px"> <el-dialog v-model="dialogVisible" :title="editingId == null ? '新增店铺' : '编辑店铺'" width="600px">
<el-form label-width="120px"> <el-form label-width="120px">
<el-form-item label="所属分组" required> <el-form-item label="所属分组" required>
<el-select v-model="form.groupId" placeholder="选择分组"> <el-select filterable v-model="form.groupId" placeholder="选择分组">
<el-option v-for="group in groupOptions" :key="group.id" :label="group.groupName" :value="group.id" /> <el-option v-for="group in groupOptions" :key="group.id" :label="group.groupName" :value="group.id" />
</el-select> </el-select>
</el-form-item> </el-form-item>