feat(app): 实现用户管理页面功能
- 修复用户列表分页参数获取错误的问题 - 添加搜索和角色筛选功能 - 实现批量删除和封禁用户操作 - 优化页面布局和样式 - 添加调试日志输出 - 更新图标资源库 - 修复API客户端请求头传递问题 - 调整侧边栏导航样式和结构 - 减少模拟数据加载时间 - 添加COOKIE常量引用
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { ofetch, type FetchOptions, type SearchParameters } from 'ofetch';
|
||||
import { log } from '$lib/log';
|
||||
import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts';
|
||||
|
||||
// 1. 定义更安全的类型,替代 any
|
||||
type QueryParams = SearchParameters; // 使用 ofetch 内置的查询参数类型,或者自定义 Record<string, string | number | boolean>
|
||||
type QueryParams = SearchParameters;
|
||||
type RequestBody = Record<string, unknown> | FormData | unknown[]; // 替代 any,使用 unknown
|
||||
|
||||
export interface ApiResult<T> {
|
||||
@@ -19,12 +20,14 @@ const client = ofetch.create({
|
||||
onRequest({ options, request }) {
|
||||
log.debug(`[API] ${options.method} ${request}`, {
|
||||
body: options.body as unknown, // 类型断言为 unknown 避免隐式 any
|
||||
headers: options.headers,
|
||||
query: options.query
|
||||
});
|
||||
},
|
||||
onResponseError({ request, response }) {
|
||||
log.error(`[API] Error ${request}`, {
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
data: response._data as unknown
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { log } from '$lib/log.ts';
|
||||
|
||||
export const roleService = {
|
||||
getRolesOptions: async (token:string) => {
|
||||
const response = await api.get<Options[]>('/roles/options', {headers: {Authorization: `${token}`}});
|
||||
const response = await api.get<Options[]>('/roles/options',undefined, {headers: {Authorization: `${token}`}});
|
||||
if (response.code != 200 || !response.data){
|
||||
log.error(response.msg);
|
||||
throw new Error(response.msg);
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
import { api } from '$lib/api/httpClient.ts';
|
||||
import type { UserProfile } from '$lib/types/user.ts';
|
||||
import type { PageResult } from '$lib/types/dataTable.ts';
|
||||
import { type SearchParameters } from 'ofetch';
|
||||
|
||||
|
||||
// 1. 定义更安全的类型,替代 any
|
||||
type QueryParams = SearchParameters;
|
||||
export const userService = {
|
||||
getUserProfile: async (token:string) => {
|
||||
const response = await api.get<UserProfile>('/users/me', {headers: {Authorization: `${token}`}});
|
||||
const response = await api.get<UserProfile>('/users/me',undefined, {headers: {Authorization: `${token}`}});
|
||||
if (response.code != 200 || !response.data){
|
||||
throw new Error(response.msg);
|
||||
}
|
||||
return response.data;
|
||||
},
|
||||
getAllUsers: async ({ page, size,token , keyword, roleId}: { page: number, size: number, token:string , keyword?: string, roleId?: number}) => {
|
||||
const formData = new FormData();
|
||||
formData.append('pageNum', page.toString());
|
||||
formData.append('pageSize', size.toString());
|
||||
if ( keyword){
|
||||
formData.append('keyword', keyword);
|
||||
}
|
||||
if ( roleId){
|
||||
formData.append('roleId', roleId.toString());
|
||||
}
|
||||
|
||||
const params: QueryParams= {
|
||||
pageNum: page,
|
||||
pageSize: size,
|
||||
...(keyword && { keyword }),
|
||||
...(roleId && { roleId })
|
||||
} ;
|
||||
const response = await api.get<PageResult<UserProfile[]>>(
|
||||
'/users',
|
||||
params,
|
||||
{
|
||||
body: formData,
|
||||
headers: {Authorization: `${token}`}
|
||||
});
|
||||
if (response.code != 200 || !response.data){
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
// import SpriteSvg from '$lib/assets/sprite.svg'
|
||||
|
||||
</script>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;" >
|
||||
@@ -56,39 +56,6 @@
|
||||
<path fill="currentColor" d="M3.75 6.5a.75.75 0 0 1 .75-.75h15a.75.75 0 0 1 0 1.5h-15a.75.75 0 0 1-.75-.75m0 5.5a.75.75 0 0 1 .75-.75h15a.75.75 0 0 1 0 1.5h-15a.75.75 0 0 1-.75-.75m0 5.5a.75.75 0 0 1 .75-.75h15a.75.75 0 0 1 0 1.5h-15a.75.75 0 0 1-.75-.75" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="logo" viewBox="0 0 375 374.999991">
|
||||
<defs>
|
||||
<clipPath id="fc5bc8767a">
|
||||
<path d="M 58.5 0 L 316.5 0 C 332.015625 0 346.894531 6.164062 357.867188 17.132812 C 368.835938 28.105469 375 42.984375 375 58.5 L 375 316.5 C 375 332.015625 368.835938 346.894531 357.867188 357.867188 C 346.894531 368.835938 332.015625 375 316.5 375 L 58.5 375 C 26.191406 375 0 348.808594 0 316.5 L 0 58.5 C 0 26.191406 26.191406 0 58.5 0 Z M 58.5 0 " clip-rule="nonzero"/>
|
||||
</clipPath>
|
||||
<clipPath id="b295c49c3e">
|
||||
<path d="M 0 0 L 375 0 L 375 375 L 0 375 Z M 0 0 " clip-rule="nonzero"/>
|
||||
</clipPath>
|
||||
<clipPath id="bfbc279fc9">
|
||||
<path d="M 58.5 0 L 316.5 0 C 332.015625 0 346.894531 6.164062 357.867188 17.132812 C 368.835938 28.105469 375 42.984375 375 58.5 L 375 316.5 C 375 332.015625 368.835938 346.894531 357.867188 357.867188 C 346.894531 368.835938 332.015625 375 316.5 375 L 58.5 375 C 26.191406 375 0 348.808594 0 316.5 L 0 58.5 C 0 26.191406 26.191406 0 58.5 0 Z M 58.5 0 " clip-rule="nonzero"/>
|
||||
</clipPath>
|
||||
<clipPath id="063da92238">
|
||||
<rect x="0" width="375" y="0" height="375"/>
|
||||
</clipPath>
|
||||
<clipPath id="cfbb605b8a">
|
||||
<path d="M 75 88.359375 L 300 88.359375 L 300 286.359375 L 75 286.359375 Z M 75 88.359375 " clip-rule="nonzero"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#fc5bc8767a)">
|
||||
<g transform="matrix(1, 0, 0, 1, 0, 0)">
|
||||
<g clip-path="url(#063da92238)">
|
||||
<g clip-path="url(#b295c49c3e)">
|
||||
<g clip-path="url(#bfbc279fc9)">
|
||||
<rect x="-82.5" width="540" fill="#1e1e1e" height="539.999987" y="-82.499998" fill-opacity="1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g clip-path="url(#cfbb605b8a)">
|
||||
<path fill="#ffffff" d="M 294.644531 174.898438 L 291.878906 170.023438 L 291.832031 170.023438 L 273.359375 138.019531 L 249.835938 97.03125 L 247.199219 92.507812 C 245.707031 89.945312 242.964844 88.371094 240.003906 88.371094 L 221.074219 88.371094 C 214.949219 88.371094 210.917969 94.753906 213.546875 100.285156 L 223.050781 120.25 C 223.140625 120.445312 223.242188 120.636719 223.351562 120.824219 L 243.175781 155.304688 L 251.59375 169.8125 C 257.941406 180.757812 257.964844 194.253906 251.660156 205.222656 L 244.746094 217.246094 L 244.5625 217.246094 L 242.058594 221.632812 C 238.851562 227.257812 230.734375 227.230469 227.558594 221.585938 L 155.027344 92.621094 C 153.550781 89.996094 150.773438 88.371094 147.765625 88.371094 L 135.160156 88.371094 C 132.257812 88.371094 129.5625 89.882812 128.050781 92.359375 L 125.230469 96.972656 L 101.675781 138.019531 L 78.15625 178.835938 C 75.042969 184.171875 75.003906 190.757812 78.050781 196.132812 L 78.132812 196.277344 L 81.566406 202.160156 L 101.675781 237.128906 L 125.203125 277.953125 L 127.835938 282.472656 C 129.332031 285.03125 132.070312 286.605469 135.035156 286.605469 L 156.164062 286.605469 C 162.601562 286.605469 166.609375 279.613281 163.351562 274.0625 L 151.742188 254.253906 L 131.859375 219.671875 L 121.308594 201.484375 C 116.289062 192.828125 116.289062 182.148438 121.308594 173.496094 L 131.851562 155.320312 L 133.03125 153.261719 C 136.242188 147.679688 144.296875 147.691406 147.492188 153.28125 L 221.234375 282.40625 C 222.71875 285.003906 225.476562 286.605469 228.46875 286.605469 L 240.003906 286.605469 C 242.964844 286.605469 245.707031 285.03125 247.199219 282.472656 L 249.820312 277.976562 L 273.363281 237.121094 L 296.882812 196.136719 C 299.996094 190.796875 300.015625 184.195312 296.921875 178.839844 L 294.644531 174.898438 " fill-opacity="1" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</symbol>
|
||||
|
||||
<symbol id="info" viewBox="0 0 24 24">
|
||||
<path fill="#3B82F6" d="M12 1.999c5.524 0 10.002 4.478 10.002 10.002c0 5.523-4.478 10.001-10.002 10.001S2 17.524 2 12.001C1.999 6.477 6.476 1.999 12 1.999" class="duoicon-secondary-layer" opacity="0.3" />
|
||||
@@ -120,14 +87,14 @@
|
||||
<path fill="currentColor" d="M1 4.75C1 3.784 1.784 3 2.75 3h10.5c.966 0 1.75.784 1.75 1.75v6.5A1.75 1.75 0 0 1 13.25 13H2.75A1.75 1.75 0 0 1 1 11.25zM2.75 4a.75.75 0 0 0-.75.75v6.5c0 .414.336.75.75.75h10.5a.75.75 0 0 0 .75-.75v-6.5a.75.75 0 0 0-.75-.75zM9.5 6a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zM5.261 7.714a1.357 1.357 0 1 0 0-2.714a1.357 1.357 0 0 0 0 2.714m-1.403.678A.86.86 0 0 0 3 9.25a1.67 1.67 0 0 0 1.265 1.62l.053.014c.62.155 1.267.155 1.886 0l.054-.013a1.67 1.67 0 0 0 1.265-1.62a.86.86 0 0 0-.858-.859z" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="sign-out" viewBox="0 0 20 20">
|
||||
<path fill="currentColor" d="M8.5 11.25a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5M11 3.5a.5.5 0 0 0-.576-.494l-7 1.07A.5.5 0 0 0 3 4.57v10.86a.5.5 0 0 0 .424.494l7 1.071a.5.5 0 0 0 .576-.494V10h5.172l-.997.874a.5.5 0 0 0 .658.752l1.996-1.75a.5.5 0 0 0 0-.752l-1.996-1.75a.499.499 0 1 0-.658.752l.997.874H11zm-1 .582V15.92L4 15V4.999zM12.5 16H12v-5h1v4.5a.5.5 0 0 1-.5.5M12 8V4h.5a.5.5 0 0 1 .5.5V8z" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="sign-out" viewBox="0 0 20 20"><path fill="currentColor" d="M8.5 11.25a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5M11 3.5a.5.5 0 0 0-.576-.494l-7 1.07A.5.5 0 0 0 3 4.57v10.86a.5.5 0 0 0 .424.494l7 1.071a.5.5 0 0 0 .576-.494V10h5.172l-.997.874a.5.5 0 0 0 .658.752l1.996-1.75a.5.5 0 0 0 0-.752l-1.996-1.75a.499.499 0 1 0-.658.752l.997.874H11zm-1 .582V15.92L4 15V4.999zM12.5 16H12v-5h1v4.5a.5.5 0 0 1-.5.5M12 8V4h.5a.5.5 0 0 1 .5.5V8z" /></symbol>
|
||||
<symbol id="auth" viewBox="0 0 16 16"><path fill="currentColor" d="M9.07 6.746a2 2 0 0 0 2.91.001l.324-.344c.297.14.577.316.835.519l-.126.422a2 2 0 0 0 1.456 2.518l.348.083a4.7 4.7 0 0 1 .011 1.017l-.46.117a2 2 0 0 0-1.431 2.479l.156.556q-.383.294-.822.497l-.337-.357a2 2 0 0 0-2.91-.002l-.325.345a4.3 4.3 0 0 1-.835-.519l.126-.423a2 2 0 0 0-1.456-2.518l-.35-.083a4.7 4.7 0 0 1-.01-1.016l.462-.118a2 2 0 0 0 1.43-2.478l-.156-.557q.383-.294.822-.497zm-1.423-4.6a.5.5 0 0 1 .707 0C9.594 3.39 10.97 4 12.5 4a.5.5 0 0 1 .5.5v1.1a5.5 5.5 0 0 0-7.494 7.204C3.846 11.59 3 9.812 3 7.502V4.5a.5.5 0 0 1 .5-.5c1.53 0 2.904-.61 4.147-1.854M10.501 9.5a1 1 0 1 0 0 2a1 1 0 0 0 0-2"/></symbol>
|
||||
|
||||
<symbol id="chevron-up-down" viewBox="0 0 16 16"><path fill="currentColor" d="M4.22 6.53a.75.75 0 0 0 1.06 0L8 3.81l2.72 2.72a.75.75 0 1 0 1.06-1.06L8.53 2.22a.75.75 0 0 0-1.06 0L4.22 5.47a.75.75 0 0 0 0 1.06m0 2.94a.75.75 0 0 1 1.06 0L8 12.19l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0l-3.25-3.25a.75.75 0 0 1 0-1.06"/></symbol>
|
||||
|
||||
|
||||
<symbol id="laptop-settings" viewBox="0 0 20 20"><path fill="currentColor" d="M4.5 5A1.5 1.5 0 0 0 3 6.5v6A1.5 1.5 0 0 0 4.5 14h4.522A5.5 5.5 0 0 1 17 9.6V6.5A1.5 1.5 0 0 0 15.5 5zm-2 10h6.522q.047.516.185 1H2.5a.5.5 0 0 1 0-1m9.565-3.558a2 2 0 0 1-1.43 2.478l-.462.118a4.7 4.7 0 0 0 .01 1.016l.35.083a2 2 0 0 1 1.456 2.519l-.127.423q.388.306.835.517l.325-.344a2 2 0 0 1 2.91.002l.337.358q.44-.203.822-.498l-.156-.556a2 2 0 0 1 1.43-2.478l.46-.118a4.7 4.7 0 0 0-.01-1.017l-.348-.082a2 2 0 0 1-1.456-2.52l.126-.421a4.3 4.3 0 0 0-.835-.519l-.325.344a2 2 0 0 1-2.91-.001l-.337-.358a4.3 4.3 0 0 0-.821.497zm2.434 4.058a1 1 0 1 1 0-2a1 1 0 0 1 0 2"/></symbol>
|
||||
<symbol id="people-search" viewBox="0 0 20 20"><path fill="currentColor" d="M10 2a4 4 0 1 0 0 8a4 4 0 0 0 0-8m4.865 14.797c-1.071.683-2.454 1.064-3.962 1.171a1.5 1.5 0 0 0-.342-.529l-2-1.999A4.5 4.5 0 0 0 9 13.5a4.5 4.5 0 0 0-.758-2.5H15a2 2 0 0 1 2 2c0 1.691-.833 2.966-2.135 3.797M4.5 17c.786 0 1.512-.26 2.096-.697l2.55 2.55a.5.5 0 1 0 .708-.707l-2.55-2.55A3.5 3.5 0 1 0 4.5 17m0-1a2.5 2.5 0 1 1 0-5a2.5 2.5 0 0 1 0 5"/></symbol>
|
||||
<symbol id="search-12" viewBox="0 0 12 12"><path fill="currentColor" d="M5 1a4 4 0 1 0 2.248 7.31l2.472 2.47a.75.75 0 1 0 1.06-1.06L8.31 7.248A4 4 0 0 0 5 1M2.5 5a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0"/></symbol>
|
||||
<symbol id="delete-12" viewBox="0 0 12 12"><path fill="currentColor" d="M5 3h2a1 1 0 0 0-2 0M4 3a2 2 0 1 1 4 0h2.5a.5.5 0 0 1 0 1h-.441l-.443 5.17A2 2 0 0 1 7.623 11H4.377a2 2 0 0 1-1.993-1.83L1.941 4H1.5a.5.5 0 0 1 0-1zm3.5 3a.5.5 0 0 0-1 0v2a.5.5 0 0 0 1 0zM5 5.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5M3.38 9.085a1 1 0 0 0 .997.915h3.246a1 1 0 0 0 .996-.915L9.055 4h-6.11z"/></symbol>
|
||||
<symbol id="person-add" viewBox="0 0 16 16"><path fill="currentColor" d="M9.626 5.07a5.5 5.5 0 0 0-3.299 1.847A2.751 2.751 0 1 1 9.626 5.07M5.6 8c-.384.75-.6 1.6-.6 2.5c0 1.31.458 2.512 1.222 3.457C3.555 13.653 2 11.803 2 10v-.5A1.5 1.5 0 0 1 3.5 8zm4.9 7a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9m0-7a.5.5 0 0 1 .5.5V10h1.5a.5.5 0 0 1 0 1H11v1.5a.5.5 0 0 1-1 0V11H8.5a.5.5 0 0 1 0-1H10V8.5a.5.5 0 0 1 .5-.5"/></symbol>
|
||||
|
||||
<symbol id="logo" viewBox="0 0 1028 1024"><path d="M550.68864 672c25.6-54.4 76.8-96 134.4-115.2l41.6-89.6c-3.2-6.4-6.4-12.8-6.4-19.2-3.2-6.4-3.2-16-6.4-22.4-25.6-6.4-41.6-19.2-51.2-38.4-16-32 3.2-76.8 41.6-115.2-25.6-35.2-57.6-64-92.8-89.6-38.4 41.6-80 60.8-112 48-35.2-12.8-51.2-57.6-51.2-112-41.6-6.4-86.4-3.2-128 3.2 3.2 60.8-12.8 105.6-44.8 121.6-35.2 12.8-76.8-3.2-118.4-41.6-35.2 25.6-64 57.6-89.6 92.8 41.6 38.4 60.8 80 48 112-12.8 35.2-57.6 51.2-112 51.2-6.4 41.6-3.2 86.4 3.2 128 54.4-3.2 99.2 12.8 115.2 44.8 16 32-3.2 76.8-41.6 115.2 25.6 35.2 57.6 64 92.8 89.6 38.4-41.6 80-60.8 112-48 35.2 12.8 51.2 57.6 51.2 112 41.6 6.4 86.4 3.2 128-3.2-3.2-54.4 12.8-99.2 44.8-115.2 3.2-3.2 9.6-3.2 12.8-3.2 6.4-35.2 12.8-73.6 28.8-105.6z m-156.8 6.4C304.28864 678.4 227.48864 604.8 227.48864 512c0-92.8 73.6-166.4 166.4-166.4s166.4 73.6 166.4 166.4c3.2 92.8-73.6 166.4-166.4 166.4z" fill="currentColor" ></path><path d="M1001.88864 288l-54.4 96-99.2-48 41.6-102.4c-48 3.2-96 28.8-118.4 76.8-22.4 48-16 105.6 16 144L707.48864 620.8c-48 3.2-92.8 28.8-115.2 76.8-22.4 48-16 99.2 12.8 140.8l60.8-102.4 99.2 48-44.8 112c48-3.2 96-28.8 118.4-76.8 22.4-48 16-102.4-16-144l80-166.4c48-3.2 92.8-28.8 115.2-76.8 19.2-51.2 12.8-105.6-16-144z" fill="currentColor" ></path></symbol>
|
||||
</svg>
|
||||
@@ -123,77 +123,79 @@
|
||||
let menuItems = $derived(computeNavState(rawNavItems, page.url.pathname, expandedIds));
|
||||
</script>
|
||||
|
||||
<div class="h-screen relative bg-base-200">
|
||||
<aside class="custom-scrollbar h-full w-full overflow-hidden flex flex-col ">
|
||||
<div class="h-14 shadow-2xl shadow-base-100 ">
|
||||
123
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1 ">
|
||||
<ul class="menu bg-base-200 w-64 p-4 text-base-content flex-nowrap ">
|
||||
{#each menuItems as item (item.id)}
|
||||
<li class="{item.isActive ? 'menu-active' : ''} w-full rounded-box ">
|
||||
{#if item.subItems && item.subItems.length > 0}
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||
<a href={resolve(item.href)} class="p-2" onclick="{() => handleClick(item)}">
|
||||
{#if item.icon}
|
||||
<Icon id={item.icon} size="24"/>
|
||||
{/if}
|
||||
<span class="menu-dropdown-toggle {item.isOpen ? 'menu-dropdown-show ' : ''}">{item.label}</span>
|
||||
</a>
|
||||
<ul class="menu-dropdown rounded-box {item.isOpen ? 'menu-dropdown-show' : ''}">
|
||||
{#each item.subItems as subItem (subItem.id)}
|
||||
<li class="{subItem.isActive ? 'menu-active' : ''} rounded-box ">
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||
<a href={resolve(subItem.href)} class="p-2 " onclick={() => handleClick(subItem)}>
|
||||
{#if subItem.icon}
|
||||
<Icon id={subItem.icon} size="24"/>
|
||||
{/if}
|
||||
<span class="menu-dropdown-toggle">
|
||||
{subItem.label}
|
||||
</span>
|
||||
</a>
|
||||
{#if subItem.subItems && subItem.subItems.length > 0}
|
||||
<ul class="menu-dropdown {subItem.isOpen ? 'menu-dropdown-show' : ''}" >
|
||||
{#each subItem.subItems as childItem (childItem.id)}
|
||||
<li class="{childItem.isActive ? 'menu-active' : ''} rounded-box">
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||
<a href={resolve(childItem.href)} class="p-2">
|
||||
{#if childItem.icon}
|
||||
<Icon id={childItem.icon} size="24"/>
|
||||
{:else}
|
||||
<div class="w-0.5/2 h-1">
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
{childItem.label}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<aside class="custom-scrollbar h-screen bg-base-200 flex flex-col ">
|
||||
<div class="flex items-center h-18 space-x-2 w-full">
|
||||
<div class="space-x-4 pl-6 w-full">
|
||||
<Icon id="logo" className="inline" size="36"/><h1 class="font-sans text-xs inline align-bottom">IT Management System</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1 ">
|
||||
<ul class="menu bg-base-200 w-64 px-4 pb-4text-base-content flex-nowrap ">
|
||||
{#each menuItems as item (item.id)}
|
||||
<li class="{item.isActive ? 'menu-active' : ''} w-full rounded-box ">
|
||||
{#if item.subItems && item.subItems.length > 0}
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||
<a href={resolve(item.href)} class="p-2" onclick="{() => handleClick(item)}">
|
||||
{#if item.icon}
|
||||
<Icon id={item.icon} size="24"/>
|
||||
{/if}
|
||||
<span class="menu-dropdown-toggle {item.isOpen ? 'menu-dropdown-show ' : ''}">{item.label}</span>
|
||||
</a>
|
||||
<ul class="menu-dropdown rounded-box {item.isOpen ? 'menu-dropdown-show' : ''}">
|
||||
{#each item.subItems as subItem (subItem.id)}
|
||||
<li class="{subItem.isActive ? 'menu-active' : ''} rounded-box ">
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||
<a href={resolve(subItem.href)} class="p-2 " onclick={() => handleClick(subItem)}>
|
||||
{#if subItem.icon}
|
||||
<Icon id={subItem.icon} size="24"/>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<span class="menu-dropdown-toggle">
|
||||
{subItem.label}
|
||||
</span>
|
||||
</a>
|
||||
{#if subItem.subItems && subItem.subItems.length > 0}
|
||||
<ul class="menu-dropdown {subItem.isOpen ? 'menu-dropdown-show' : ''}" >
|
||||
{#each subItem.subItems as childItem (childItem.id)}
|
||||
<li class="{childItem.isActive ? 'menu-active' : ''} rounded-box">
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||
<a href={resolve(childItem.href)} class="p-2">
|
||||
{#if childItem.icon}
|
||||
<Icon id={childItem.icon} size="24"/>
|
||||
{:else}
|
||||
<div class="w-0.5/2 h-1">
|
||||
|
||||
{:else }
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||
<a href={resolve(item.href)} class="p-2">
|
||||
{#if item.icon}
|
||||
<Icon id={item.icon} size="24"/>
|
||||
{/if}
|
||||
{item.label}
|
||||
</a>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{childItem.label}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{:else }
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||
<a href={resolve(item.href)} class="p-2">
|
||||
{#if item.icon}
|
||||
<Icon id={item.icon} size="24"/>
|
||||
{/if}
|
||||
{item.label}
|
||||
</a>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<div class="h-24 w-full shadow-2xl ">
|
||||
12312
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
<div class="h-24 w-full shadow-2xl ">
|
||||
12312
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
<script lang="ts">
|
||||
import type { PageResult } from '$lib/types/dataTable';
|
||||
import type { UserProfile } from '$lib/types/user';
|
||||
import type { Options } from '$lib/types/api';
|
||||
import Icon from '$lib/components/icon/Icon.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
let { users, rolesOptions } = $props<{
|
||||
let {
|
||||
users,
|
||||
selectedIds = $bindable([]),
|
||||
onPageChange
|
||||
} = $props<{
|
||||
users: PageResult<UserProfile[]>;
|
||||
rolesOptions: Options[];
|
||||
selectedIds: number[];
|
||||
onPageChange: (page: number) => void;
|
||||
}>();
|
||||
|
||||
// --- 1. 状态管理 (Svelte 5 Runes) ---
|
||||
let selectedIds = $state<number[]>([]);
|
||||
let searchQuery = $state(page.url.searchParams.get('q') || '');
|
||||
let currentRole = $derived(page.url.searchParams.get('role') || '');
|
||||
// --- 内部状态逻辑 (与UI展示紧密相关) ---
|
||||
|
||||
// --- 2. 选择逻辑 ---
|
||||
// 计算属性:是否全选
|
||||
let isAllSelected = $derived(
|
||||
users.records.length > 0 && selectedIds.length === users.records.length
|
||||
);
|
||||
// 计算属性:是否部分选中 (用于控制 checkbox 的 indeterminate 状态)
|
||||
|
||||
// 计算属性:是否部分选中 (indeterminate)
|
||||
let isIndeterminate = $derived(
|
||||
selectedIds.length > 0 && selectedIds.length < users.records.length
|
||||
);
|
||||
@@ -50,46 +48,9 @@
|
||||
});
|
||||
}
|
||||
|
||||
// --- 3. URL 参数更新逻辑 (搜索/筛选/分页) ---
|
||||
function updateParams(key: string, value: string | number | null) {
|
||||
const url = new URL(page.url);
|
||||
if (value === null || value === '') {
|
||||
url.searchParams.delete(key);
|
||||
} else {
|
||||
url.searchParams.set(key, String(value));
|
||||
}
|
||||
// 重置页码回第一页 (除非我们在通过分页器操作)
|
||||
if (key !== 'page') {
|
||||
url.searchParams.set('page', '1');
|
||||
}
|
||||
|
||||
goto(resolve(url), { keepFocus: true, noScroll: true });
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
updateParams('q', searchQuery);
|
||||
}
|
||||
|
||||
function handleRoleChange(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
updateParams('role', target.value);
|
||||
}
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
if (newPage < 1 || newPage > users.pages) return;
|
||||
updateParams('page', newPage);
|
||||
}
|
||||
|
||||
|
||||
function handleBatchAction(action: 'delete' | 'ban') {
|
||||
if (selectedIds.length === 0) return alert('请先选择用户');
|
||||
console.log(`执行批量操作: ${action}`, selectedIds);
|
||||
// 这里调用 API...
|
||||
}
|
||||
|
||||
|
||||
// 分页逻辑辅助函数
|
||||
function getPaginationRange(current: number, total: number) {
|
||||
const delta = 2; // 当前页码前后显示的页数
|
||||
const delta = 2;
|
||||
const range = [];
|
||||
const rangeWithDots: (number | string)[] = [];
|
||||
let l: number | undefined;
|
||||
@@ -123,207 +84,113 @@
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
{#if users.total > 0 || searchQuery || currentRole}
|
||||
<div class="bg-base-100 rounded-box shadow-sm border border-base-200 flex flex-col h-full">
|
||||
<!-- 工具栏 -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 p-4 border-b border-base-200">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<!-- 搜索框 -->
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<svg class="h-4 w-4 opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g stroke-linejoin="round" stroke-linecap="round" stroke-width="2.5" fill="none" stroke="currentColor">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.3-4.3"></path>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="overflow-x-auto flex-1 bg-base-100 min-h-[300px] ">
|
||||
<table class="table ">
|
||||
<thead class="z-0">
|
||||
<tr>
|
||||
<th class="w-12">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm"
|
||||
checked={isAllSelected}
|
||||
use:indeterminate={isIndeterminate}
|
||||
onchange={toggleAll}
|
||||
/>
|
||||
</label>
|
||||
</th>
|
||||
{#each newRowTitles as item (item.title)}
|
||||
<th style="width: {item.width}%" >{item.title}</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{#each users.records as record (record.id)}
|
||||
<tr class="hover">
|
||||
<th>
|
||||
<label>
|
||||
<input
|
||||
type="search"
|
||||
bind:value={searchQuery}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder="搜索用户..."
|
||||
class="grow"
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm"
|
||||
checked={selectedIds.includes(record.id)}
|
||||
onchange={() => toggleOne(record.id)}
|
||||
/>
|
||||
<button class="btn btn-xs btn-ghost" onclick={handleSearch}>搜索</button>
|
||||
</label>
|
||||
|
||||
<!-- 角色筛选 -->
|
||||
{#if rolesOptions}
|
||||
<div class="join">
|
||||
<input
|
||||
class="join-item btn btn-sm {currentRole === '' ? 'btn-active btn-neutral' : ''}"
|
||||
type="radio"
|
||||
name="roles"
|
||||
aria-label="全部"
|
||||
value=""
|
||||
checked={currentRole === ''}
|
||||
onchange={handleRoleChange}
|
||||
/>
|
||||
{#each rolesOptions as role (role.value)}
|
||||
<input
|
||||
class="join-item btn btn-sm {currentRole === String(role.value) ? 'btn-active btn-neutral' : ''}"
|
||||
type="radio"
|
||||
name="roles"
|
||||
aria-label={role.label}
|
||||
value={role.value}
|
||||
checked={currentRole === String(role.value)}
|
||||
onchange={handleRoleChange}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="btn btn-primary btn-sm">
|
||||
<Icon id="user-plus" size="16" /> 添加用户
|
||||
</button>
|
||||
|
||||
<div class="dropdown dropdown-bottom dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-sm btn-square btn-ghost">
|
||||
<Icon id="dots-vertical" size="20" />
|
||||
</div>
|
||||
|
||||
<ul tabindex="-1" class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow-lg border border-base-200">
|
||||
<li>
|
||||
<button onclick={() => handleBatchAction('delete')} class="text-error">
|
||||
<Icon id="trash" size="16"/> 批量删除 ({selectedIds.length})
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onclick={() => handleBatchAction('ban')}>
|
||||
<Icon id="ban" size="16"/> 批量封禁 ({selectedIds.length})
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<div class="overflow-x-auto flex-1">
|
||||
<table class="table table-pin-rows">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-12">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm"
|
||||
checked={isAllSelected}
|
||||
use:indeterminate={isIndeterminate}
|
||||
onchange={toggleAll}
|
||||
/>
|
||||
</label>
|
||||
</th>
|
||||
{#each newRowTitles as item (item.title)}
|
||||
<th style="width: {item.width}%">{item.title}</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{#if users.records && users.records.length > 0}
|
||||
<tbody>
|
||||
{#each users.records as record (record.id)}
|
||||
<tr class="hover">
|
||||
<th>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm"
|
||||
checked={selectedIds.includes(record.id)}
|
||||
onchange={() => toggleOne(record.id)}
|
||||
/>
|
||||
</label>
|
||||
</th>
|
||||
<td class="font-mono text-xs opacity-70">{record.id}</td>
|
||||
<td class="font-bold">{record.username}</td>
|
||||
<td>{record.nickname || '-'}</td>
|
||||
<td>
|
||||
<div class="avatar">
|
||||
<div class="w-8 h-8 rounded-full bg-base-300 ring ring-base-200 ring-offset-base-100 ring-offset-2">
|
||||
{#if record.avatar}
|
||||
<img src={record.avatar} alt={record.username} />
|
||||
{:else}
|
||||
<span class="text-xs flex items-center justify-center h-full w-full uppercase">
|
||||
{record.username.slice(0, 2)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each record.roles as role (role.id)}
|
||||
<span class="badge badge-sm {role.id === 1 ? 'badge-primary' : 'badge-ghost'}">
|
||||
{role.name}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
{/if}
|
||||
</table>
|
||||
|
||||
{#if users.records.length === 0}
|
||||
<div class="flex flex-col items-center justify-center p-10 text-base-content/50">
|
||||
<Icon id="search-off" size="48" />
|
||||
<p class="mt-2">未找到匹配的用户</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- 分页底栏 -->
|
||||
{#if users.total > 0}
|
||||
<div class="border-t border-base-200 p-4 flex items-center justify-between bg-base-100">
|
||||
<div class="text-sm text-base-content/70">
|
||||
显示 {(users.current - 1) * users.size + 1} 到 {Math.min(users.current * users.size, users.total)} 条,共 {users.total} 条
|
||||
</div>
|
||||
|
||||
<div class="join">
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
disabled={users.current === 1}
|
||||
onclick={() => handlePageChange(users.current - 1)}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
|
||||
{#each getPaginationRange(users.current, users.pages) as pageNum}
|
||||
{#if pageNum === '...'}
|
||||
<button class="join-item btn btn-sm btn-disabled">...</button>
|
||||
</th>
|
||||
<td class="font-mono text-xs opacity-70">{record.id}</td>
|
||||
<td class="font-bold">{record.username}</td>
|
||||
<td>{record.nickname || '-'}</td>
|
||||
<td>
|
||||
<div class="avatar">
|
||||
<div class="w-8 h-8 rounded-full bg-base-300 ring ring-base-200 ring-offset-base-100 ring-offset-2">
|
||||
{#if record.avatar}
|
||||
<img src={record.avatar} alt={record.username} />
|
||||
{:else}
|
||||
<button
|
||||
class="join-item btn btn-sm {users.current === pageNum ? 'btn-active btn-primary' : ''}"
|
||||
onclick={() => handlePageChange(Number(pageNum))}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
<span class="text-xs flex items-center justify-center h-full w-full uppercase">
|
||||
{record.username.slice(0, 2)}
|
||||
</span>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
disabled={users.current === users.pages}
|
||||
onclick={() => handlePageChange(users.current + 1)}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="hero bg-base-200 rounded-box min-h-[400px]">
|
||||
<div class="hero-content text-center">
|
||||
<div class="max-w-md">
|
||||
<h1 class="text-3xl font-bold">没有数据</h1>
|
||||
<p class="py-6">当前系统中没有用户数据。</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each record.roles as role (role.id)}
|
||||
<span class="badge badge-sm {role.id === 1 ? 'badge-primary' : 'badge-ghost'}">
|
||||
{role.name}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{#if users.records.length === 0}
|
||||
<div class="flex flex-col items-center justify-center p-10 text-base-content/50">
|
||||
<Icon id="search-off" size="48" />
|
||||
<p class="mt-2">未找到匹配的用户</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if users.total > 0}
|
||||
<div class="border-t border-base-200 p-4 flex items-center justify-between bg-base-100">
|
||||
<div class="text-sm text-base-content/70">
|
||||
显示 {(users.current - 1) * users.size + 1} 到 {Math.min(users.current * users.size, users.total)} 条,共 {users.total} 条
|
||||
</div>
|
||||
|
||||
<div class="join">
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
disabled={users.current === 1}
|
||||
onclick={() => onPageChange(users.current - 1)}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
|
||||
{#each getPaginationRange(users.current, users.pages) as pageNum (pageNum)}
|
||||
{#if pageNum === '...'}
|
||||
<button class="join-item btn btn-sm btn-disabled">...</button>
|
||||
{:else}
|
||||
<button
|
||||
class="join-item btn btn-sm {users.current === pageNum ? 'btn-active btn-primary' : ''}"
|
||||
onclick={() => onPageChange(Number(pageNum))}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
disabled={users.current === users.pages}
|
||||
onclick={() => onPageChange(users.current + 1)}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -17,5 +17,9 @@ export type IconId =
|
||||
"user-profile"|
|
||||
"auth"|
|
||||
"chevron-up-down"|
|
||||
"laptop-settings"
|
||||
"laptop-settings"|
|
||||
"people-search"|
|
||||
"search-12"|
|
||||
"delete-12" |
|
||||
"person-add"
|
||||
;
|
||||
@@ -6,16 +6,13 @@
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen bg-base-300 overflow-hidden relative">
|
||||
|
||||
<AppSidebar />
|
||||
|
||||
<div class="flex-1 flex flex-col min-w-0 overflow-hidden relative">
|
||||
|
||||
<div class="flex-1 flex flex-col min-w-0 overflow-hidden relative h-full">
|
||||
<AppHeader />
|
||||
|
||||
<main class="flex-1 px-4">
|
||||
<main class="flex-1 p-4 ">
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
@@ -14,10 +14,11 @@ export const load:PageServerLoad = async ({ cookies ,url }) => {
|
||||
}
|
||||
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const size = Number(url.searchParams.get('page')) || 10;
|
||||
const size = Number(url.searchParams.get('size')) || 10;
|
||||
const keyword = url.searchParams.get('q') || undefined;
|
||||
const role = Number(url.searchParams.get('role')) || undefined;
|
||||
|
||||
log.debug('getAllUsers', { page, size, keyword, role });
|
||||
|
||||
|
||||
const getRoles = async() => {
|
||||
@@ -26,7 +27,7 @@ export const load:PageServerLoad = async ({ cookies ,url }) => {
|
||||
|
||||
|
||||
const getUserList = async() => {
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return await userService.getAllUsers({ page: page, size: size , keyword:keyword, roleId:role,token:token});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +1,136 @@
|
||||
<script lang="ts">
|
||||
import UsersTable from '$lib/components/table/UsersTable.svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
import TableLoadingState from '$lib/components/loading/TableLoadingState.svelte';
|
||||
import TableLoadingError from '$lib/components/error/TableLoadingError.svelte';
|
||||
|
||||
const {data} = $props();
|
||||
import { resolve } from '$app/paths';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import Icon from '$lib/components/icon/Icon.svelte';
|
||||
|
||||
|
||||
let promiseCombined = $derived(Promise.all([
|
||||
data.streamed.userList,
|
||||
data.streamed.rolesOptions
|
||||
]));
|
||||
import TableLoadingState from '$lib/components/loading/TableLoadingState.svelte';
|
||||
import TableLoadingError from '$lib/components/error/TableLoadingError.svelte';
|
||||
import UsersTable from '$lib/components/table/UsersTable.svelte';
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
// --- 1. 状态管理 ---
|
||||
// selectedIds 需要在父组件,因为"批量操作按钮"在 Toolbar 里
|
||||
let selectedIds = $state<number[]>([]);
|
||||
|
||||
let searchQuery = $state(page.url.searchParams.get('q') || '');
|
||||
let currentRole = $derived(page.url.searchParams.get('role') || '');
|
||||
|
||||
// --- 2. URL 参数更新逻辑 ---
|
||||
function updateParams(key: string, value: string | number | null) {
|
||||
const url = new URL(page.url);
|
||||
if (value === null || value === '') {
|
||||
url.searchParams.delete(key);
|
||||
} else {
|
||||
url.searchParams.set(key, String(value));
|
||||
}
|
||||
if (key !== 'page') {
|
||||
url.searchParams.set('page', '1');
|
||||
}
|
||||
// 切换筛选条件时清空选中状态,避免误操作
|
||||
selectedIds = [];
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
updateParams('q', searchQuery);
|
||||
}
|
||||
|
||||
function handleRoleChange(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
updateParams('role', target.value);
|
||||
}
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
updateParams('page', newPage);
|
||||
}
|
||||
|
||||
function handleBatchAction(action: 'delete' | 'ban') {
|
||||
if (selectedIds.length === 0) return alert('请先选择用户');
|
||||
console.log(`执行批量操作: ${action}`, selectedIds);
|
||||
// TODO: 调用 API...
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>用户管理 | 系统设置</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex justify-between items-center select-none">
|
||||
<p class="font-bold">用户管理</p>
|
||||
<div class="breadcrumbs textarea-md text-base-content/70 ">
|
||||
<ul>
|
||||
<li><a href={resolve('/app/dashboard')}>仪表盘</a></li>
|
||||
<li>系统设置</li>
|
||||
<li>认证管理</li>
|
||||
<li><a href={resolve('/app/settings/auth/users')}>用户管理</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 bg-base-100 rounded-box shadow-sm relative overflow-hidden min-h-[400px]">
|
||||
{#await promiseCombined}
|
||||
<TableLoadingState />
|
||||
{:then [users, rolesOptions]}
|
||||
<!-- 数据加载完成,渲染表格 -->
|
||||
<div class="h-full w-full ">
|
||||
<UsersTable {users} {rolesOptions} />
|
||||
<div class=" h-full flex flex-col ">
|
||||
<div class="flex justify-between items-center select-none pb-2">
|
||||
<p class="font-bold text-lg">用户管理</p>
|
||||
<div class="breadcrumbs text-sm text-base-content/70">
|
||||
<ul>
|
||||
<li><a href={resolve('/app/dashboard')}>仪表盘</a></li>
|
||||
<li>系统设置</li>
|
||||
<li>认证管理</li>
|
||||
<li><a href={resolve('/app/settings/auth/users')}>用户管理</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{:catch error}
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-between p-4 border-b border-base-200 bg-base-100 relative rounded-t-box">
|
||||
<div class="flex flex-wrap items-center ">
|
||||
<label class="input input-bordered input-sm flex items-center gap-2">
|
||||
<Icon id="search-12" size="16" class="opacity-50" />
|
||||
<input
|
||||
type="search"
|
||||
bind:value={searchQuery}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder="搜索用户..."
|
||||
class="grow"
|
||||
/>
|
||||
<button class="btn btn-xs btn-ghost" onclick={handleSearch}>搜索</button>
|
||||
</label>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="btn btn-primary btn-sm">
|
||||
<Icon id="person-add" size="16" /> 添加用户
|
||||
</button>
|
||||
|
||||
<div class="dropdown dropdown-bottom dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-sm btn-square btn-ghost">
|
||||
<Icon id="menu" size="20" />
|
||||
</div>
|
||||
<div tabindex="-1" class="dropdown-content w-48 bg-base-100 rounded-box z-[1] mt-2 p-2 shadow-lg border border-base-200 join join-vertical">
|
||||
|
||||
<button
|
||||
onclick={() => handleBatchAction('delete')}
|
||||
class:btn-disabled={selectedIds.length === 0}
|
||||
class=" join-item btn btn-error"
|
||||
>
|
||||
<span>批量删除 ({selectedIds.length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={() => handleBatchAction('ban')}
|
||||
class:btn-disabled={selectedIds.length === 0}
|
||||
class="join-item btn btn-neutral"
|
||||
>
|
||||
<span>批量封禁 ({selectedIds.length})</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 relative bg-base-100 flex flex-col rounded-b-box">
|
||||
{#await data.streamed.userList}
|
||||
<TableLoadingState />
|
||||
{:then users}
|
||||
<UsersTable
|
||||
{users}
|
||||
bind:selectedIds
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
{:catch error}
|
||||
<TableLoadingError error={error} />
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<TableLoadingError error={error}/>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user