feat(auth): 实现基于令牌的用户认证和访问控制
- 在用户相关页面服务端加载函数中添加令牌检查,防止未授权访问 - 更新用户服务方法以支持携带认证令牌请求API - 修改用户资料和用户列表组件以适配新的认证流程 - 引入侧边栏状态管理并在布局中注册上下文 - 调整HTTP客户端逻辑以正确传递请求头信息 - 更新用户类型定义以匹配后端返回的角色结构 - 优化应用头部和侧边栏组件的UI细节和交互逻辑
This commit is contained in:
@@ -14,6 +14,7 @@ const API_BASE_URL = import.meta.env.VITE_PUBLIC_API_URL || 'http://localhost:18
|
|||||||
const normalizeHeaders = (headers?: HeadersInit):Record<string, string> =>{
|
const normalizeHeaders = (headers?: HeadersInit):Record<string, string> =>{
|
||||||
const result:Record<string,string> = {};
|
const result:Record<string,string> = {};
|
||||||
|
|
||||||
|
console.log('normalizeHeaders', headers);
|
||||||
if (!headers){
|
if (!headers){
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -28,9 +29,14 @@ const normalizeHeaders = (headers?: HeadersInit):Record<string, string> =>{
|
|||||||
})
|
})
|
||||||
}else {
|
}else {
|
||||||
Object.keys(headers).forEach(key => {
|
Object.keys(headers).forEach(key => {
|
||||||
result[key.toLowerCase()] = headers[key.toLowerCase()] as string;
|
const value = (headers as Record<string, string>)[key];
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
result[key.toLowerCase()] = value;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('normalizeHeaders result:', result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
export class HttpError extends Error {
|
export class HttpError extends Error {
|
||||||
@@ -59,13 +65,14 @@ const httpRequest = async <T>(
|
|||||||
const fullUrl = `${API_BASE_URL}${url}`;
|
const fullUrl = `${API_BASE_URL}${url}`;
|
||||||
const { body, headers, ...rest } = options;
|
const { body, headers, ...rest } = options;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const requestHeaders: Record<string, string> = normalizeHeaders(headers);
|
const requestHeaders: Record<string, string> = normalizeHeaders(headers);
|
||||||
let requestBody: BodyInit | undefined;
|
let requestBody: BodyInit | undefined;
|
||||||
|
|
||||||
const canHaveBody = method !== 'GET' ;
|
const canHaveBody = method !== 'GET' ;
|
||||||
|
|
||||||
if (canHaveBody) {
|
if (canHaveBody) {
|
||||||
console.log('body', body);
|
|
||||||
if (body instanceof FormData) {
|
if (body instanceof FormData) {
|
||||||
requestBody = body;
|
requestBody = body;
|
||||||
} else if (body) {
|
} else if (body) {
|
||||||
@@ -74,12 +81,11 @@ const httpRequest = async <T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... Token 处理逻辑保持不变 ...
|
|
||||||
// if (currentToken && currentTokenHead) {
|
|
||||||
// requestHeaders['authorization'] = `${currentTokenHead} ${currentToken}`;
|
|
||||||
// }
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
|
||||||
const response = await fetch(fullUrl, {
|
const response = await fetch(fullUrl, {
|
||||||
method,
|
method,
|
||||||
headers: requestHeaders,
|
headers: requestHeaders,
|
||||||
@@ -89,6 +95,8 @@ const httpRequest = async <T>(
|
|||||||
...rest
|
...rest
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('response', response);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorDetail;
|
let errorDetail;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,18 +3,23 @@ import type { UserProfile } from '$lib/types/user.ts';
|
|||||||
import type { PageResult } from '$lib/types/dataTable.ts';
|
import type { PageResult } from '$lib/types/dataTable.ts';
|
||||||
|
|
||||||
export const userService = {
|
export const userService = {
|
||||||
getUserProfile: async ({ tokenHead, token}) => {
|
getUserProfile: async (token:string) => {
|
||||||
const response = await api.get<UserProfile>('/user/profile');
|
const response = await api.get<UserProfile>('/user/profile', {headers: {Authorization: `${token}`}});
|
||||||
if (response.code != 200 || !response.data){
|
if (response.code != 200 || !response.data){
|
||||||
throw new Error(response.msg);
|
throw new Error(response.msg);
|
||||||
}
|
}
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
getAllUsers: async ({ page, size}: { page: number, size: number}) => {
|
getAllUsers: async ({ page, size,token}: { page: number, size: number, token:string}) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('pageNum', page.toString());
|
formData.append('pageNum', page.toString());
|
||||||
formData.append('pageSize', size.toString());
|
formData.append('pageSize', size.toString());
|
||||||
const response = await api.get<PageResult<UserProfile>[]>('/user/all', {body: formData});
|
const response = await api.get<PageResult<UserProfile>>(
|
||||||
|
'/user/all',
|
||||||
|
{
|
||||||
|
body: formData,
|
||||||
|
headers: {Authorization: `${token}`}
|
||||||
|
});
|
||||||
if (response.code != 200 || !response.data){
|
if (response.code != 200 || !response.data){
|
||||||
throw new Error(response.msg);
|
throw new Error(response.msg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,24 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import Icon from '$lib/components/icon/Icon.svelte';
|
|
||||||
import ThemeSelector from '$lib/widget/ThemeSelector.svelte';
|
import ThemeSelector from '$lib/widget/ThemeSelector.svelte';
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="w-full h-18 flex justify-between items-center px-4 bg-base-300 flex-shrink-0">
|
<header class="w-full h-18 flex justify-between items-center px-4 bg-base-300 flex-shrink-0">
|
||||||
<div>
|
<div>
|
||||||
<button
|
<!-- <button-->
|
||||||
class="btn btn-square btn-ghost"
|
<!-- class="btn btn-square btn-ghost"-->
|
||||||
aria-label="Toggle Sidebar"
|
<!-- aria-label="Toggle Sidebar"-->
|
||||||
>
|
<!-- onclick={sidebarState.toggleSidebar}-->
|
||||||
<Icon id="menu" size="24" />
|
<!-- >-->
|
||||||
</button>
|
<!-- <Icon id="menu" size="24" />-->
|
||||||
|
<!-- </button>-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="flex justify-center items-center gap-4 select-none">
|
<div class="flex justify-center items-center gap-4 select-none">
|
||||||
<ThemeSelector/>
|
<ThemeSelector/>
|
||||||
|
|
||||||
@@ -25,11 +28,11 @@
|
|||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="rounded-full bg-primary h-8 w-8 p-4 flex items-center justify-center text-primary-content font-bold"
|
class="rounded-full cursor-pointer shadow-base-content bg-base-100/50 p-0.5 flex items-center justify-center text-primary-content font-bold"
|
||||||
>
|
>
|
||||||
{#if page.data.user.avatar}
|
{#if page.data.user.avatar}
|
||||||
<img
|
<img
|
||||||
class="w-8 h-8 rounded-full"
|
class="w-8 h-8 rounded-full z-10"
|
||||||
src="{page.data.user.avatar}"
|
src="{page.data.user.avatar}"
|
||||||
alt="Avatar"
|
alt="Avatar"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,272 +1,522 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
|
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
|
|
||||||
import { fly, fade } from 'svelte/transition';
|
import { fly, fade } from 'svelte/transition';
|
||||||
|
|
||||||
import Icon from '$lib/components/icon/Icon.svelte';
|
import Icon from '$lib/components/icon/Icon.svelte';
|
||||||
|
|
||||||
import type { NavItem, ProcessedNavItem } from '$lib/types/layout';
|
import type { NavItem, ProcessedNavItem } from '$lib/types/layout';
|
||||||
|
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
|
|
||||||
import { TOAST_KEY, type ToastState } from '$lib/stores/toast.svelte.ts';
|
import { TOAST_KEY, type ToastState } from '$lib/stores/toast.svelte.ts';
|
||||||
|
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
|
|
||||||
|
import { SIDEBAR_KEY, SidebarState } from '$lib/stores/sidebar.svelte.ts';
|
||||||
|
|
||||||
|
const sidebarState = getContext<SidebarState>(SIDEBAR_KEY);
|
||||||
|
|
||||||
|
|
||||||
// 1. 模拟数据:包含三层结构
|
// 1. 模拟数据:包含三层结构
|
||||||
|
|
||||||
const rawNavItems: NavItem[] = [
|
const rawNavItems: NavItem[] = [
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
||||||
id: 'dashboard',
|
id: 'dashboard',
|
||||||
|
|
||||||
label: '仪表盘',
|
label: '仪表盘',
|
||||||
|
|
||||||
icon: 'home',
|
icon: 'home',
|
||||||
|
|
||||||
href: '/app/dashboard'
|
href: '/app/dashboard'
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'statistics',
|
|
||||||
label: '数据看板',
|
|
||||||
icon: 'data',
|
|
||||||
href: '/app/statistics'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'settings',
|
|
||||||
label: '系统设置',
|
|
||||||
icon: 'settings',
|
|
||||||
href: '/app/settings',
|
|
||||||
subItems: [
|
|
||||||
{
|
|
||||||
id: 'auth',
|
|
||||||
label: '认证管理',
|
|
||||||
href: '/app/settings/auth',
|
|
||||||
icon: 'auth',
|
|
||||||
subItems: [
|
|
||||||
{
|
|
||||||
id: 'users',
|
|
||||||
label: '用户管理',
|
|
||||||
href: '/app/settings/auth/users'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'roles',
|
|
||||||
label: '角色权限',
|
|
||||||
href: '/app/settings/auth/roles'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'permissions',
|
|
||||||
label: '权限管理',
|
|
||||||
href: '/app/settings/auth/permissions'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: 'advanced',
|
|
||||||
label: '高级设置',
|
id: 'statistics',
|
||||||
href: '/app/settings/advanced',
|
|
||||||
subItems: [
|
label: '数据看板',
|
||||||
{
|
|
||||||
id: 'logs',
|
icon: 'data',
|
||||||
label: '安全日志',
|
|
||||||
href: '/app/settings/advanced/logs'
|
href: '/app/statistics'
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
||||||
|
id: 'settings',
|
||||||
|
|
||||||
|
label: '系统设置',
|
||||||
|
|
||||||
|
icon: 'settings',
|
||||||
|
|
||||||
|
href: '/app/settings',
|
||||||
|
|
||||||
|
subItems: [
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
id: 'auth',
|
||||||
|
|
||||||
|
label: '认证管理',
|
||||||
|
|
||||||
|
href: '/app/settings/auth',
|
||||||
|
|
||||||
|
icon: 'auth',
|
||||||
|
|
||||||
|
subItems: [
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
id: 'users',
|
||||||
|
|
||||||
|
label: '用户管理',
|
||||||
|
|
||||||
|
href: '/app/settings/auth/users'
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
id: 'roles',
|
||||||
|
|
||||||
|
label: '角色权限',
|
||||||
|
|
||||||
|
href: '/app/settings/auth/roles'
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
id: 'permissions',
|
||||||
|
|
||||||
|
label: '权限管理',
|
||||||
|
|
||||||
|
href: '/app/settings/auth/permissions'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
id: 'advanced',
|
||||||
|
|
||||||
|
label: '高级设置',
|
||||||
|
|
||||||
|
href: '/app/settings/advanced',
|
||||||
|
|
||||||
|
subItems: [
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
id: 'logs',
|
||||||
|
|
||||||
|
label: '安全日志',
|
||||||
|
|
||||||
|
href: '/app/settings/advanced/logs'
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
id: 'backup',
|
id: 'backup',
|
||||||
|
|
||||||
label: '备份恢复',
|
label: '备份恢复',
|
||||||
|
|
||||||
href: '/app/settings/advanced/backup'
|
href: '/app/settings/advanced/backup'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
||||||
* 递归计算高亮状态 (强类型版本)
|
* 递归计算高亮状态 (强类型版本)
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function processNavItems(items: NavItem[], currentPath: string): ProcessedNavItem[] {
|
function processNavItems(items: NavItem[], currentPath: string): ProcessedNavItem[] {
|
||||||
|
|
||||||
return items.map((item) => {
|
return items.map((item) => {
|
||||||
|
|
||||||
const isSelfActive =
|
const isSelfActive =
|
||||||
|
|
||||||
item.href === '/' ? currentPath === '/' : currentPath.startsWith(item.href);
|
item.href === '/' ? currentPath === '/' : currentPath.startsWith(item.href);
|
||||||
|
|
||||||
|
|
||||||
let processedSubItems: ProcessedNavItem[] | undefined = undefined;
|
let processedSubItems: ProcessedNavItem[] | undefined = undefined;
|
||||||
|
|
||||||
let isChildActive = false;
|
let isChildActive = false;
|
||||||
|
|
||||||
|
|
||||||
if (item.subItems) {
|
if (item.subItems) {
|
||||||
// 递归调用
|
|
||||||
|
// 递归调用
|
||||||
|
|
||||||
processedSubItems = processNavItems(item.subItems, currentPath);
|
processedSubItems = processNavItems(item.subItems, currentPath);
|
||||||
// 检查子项激活状态
|
|
||||||
|
// 检查子项激活状态
|
||||||
|
|
||||||
isChildActive = processedSubItems.some((sub) => sub.isActive || sub.isChildActive);
|
isChildActive = processedSubItems.some((sub) => sub.isActive || sub.isChildActive);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
||||||
...item,
|
...item,
|
||||||
|
|
||||||
subItems: processedSubItems, // 这里类型现在是 ProcessedNavItem[]
|
subItems: processedSubItems, // 这里类型现在是 ProcessedNavItem[]
|
||||||
|
|
||||||
isActive: isSelfActive,
|
isActive: isSelfActive,
|
||||||
|
|
||||||
isChildActive: isChildActive
|
isChildActive: isChildActive
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 使用 $derived 动态计算,类型自动推断为 ProcessedNavItem[]
|
// 使用 $derived 动态计算,类型自动推断为 ProcessedNavItem[]
|
||||||
|
|
||||||
let navItems = $derived(processNavItems(rawNavItems, page.url.pathname));
|
let navItems = $derived(processNavItems(rawNavItems, page.url.pathname));
|
||||||
|
|
||||||
|
|
||||||
// 获取 Toast 以便提示用户
|
// 获取 Toast 以便提示用户
|
||||||
|
|
||||||
const toast = getContext<ToastState>(TOAST_KEY);
|
const toast = getContext<ToastState>(TOAST_KEY);
|
||||||
|
|
||||||
|
|
||||||
// 处理提交结果的回调
|
// 处理提交结果的回调
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
|
|
||||||
toast.info('正在退出登录...');
|
toast.info('正在退出登录...');
|
||||||
|
|
||||||
return async ({ result, update }) => {
|
return async ({ result, update }) => {
|
||||||
|
|
||||||
// result.type 可能是 'redirect', 'success', 'failure'
|
|
||||||
|
// result.type 可能是 'redirect', 'success', 'failure'
|
||||||
|
|
||||||
if (result.type === 'redirect') {
|
if (result.type === 'redirect') {
|
||||||
|
|
||||||
toast.success('您已安全退出');
|
toast.success('您已安全退出');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// update() 会触发默认行为(也就是执行 redirect 跳转)
|
|
||||||
|
// update() 会触发默认行为(也就是执行 redirect 跳转)
|
||||||
|
|
||||||
await update();
|
await update();
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
let logoutForm: HTMLFormElement;
|
let logoutForm: HTMLFormElement;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<!-- 定义递归 Snippet,显式指定类型 -->
|
<!-- 定义递归 Snippet,显式指定类型 -->
|
||||||
|
|
||||||
{#snippet menuItem(item: ProcessedNavItem)}
|
{#snippet menuItem(item: ProcessedNavItem)}
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
|
|
||||||
{#if item.subItems && item.subItems.length > 0}
|
{#if item.subItems && item.subItems.length > 0}
|
||||||
|
|
||||||
<details open={item.isChildActive}>
|
<details open={item.isChildActive}>
|
||||||
|
|
||||||
<summary class="group {item.isActive ? 'text-primary font-medium' : ''}">
|
<summary class="group {item.isActive ? 'text-primary font-medium' : ''}">
|
||||||
|
|
||||||
{#if item.icon}
|
{#if item.icon}
|
||||||
|
|
||||||
<Icon id={item.icon} size="20" />
|
<Icon id={item.icon} size="20" />
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<span class="truncate">{item.label}</span>
|
<span class="truncate">{item.label}</span>
|
||||||
|
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
|
|
||||||
{#each item.subItems as subItem (subItem.id)}
|
{#each item.subItems as subItem (subItem.id)}
|
||||||
|
|
||||||
<!-- 递归渲染子项 -->
|
<!-- 递归渲染子项 -->
|
||||||
|
|
||||||
{@render menuItem(subItem)}
|
{@render menuItem(subItem)}
|
||||||
|
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
{:else}
|
{:else}
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
|
||||||
href={resolve(item.href)}
|
href={resolve(item.href)}
|
||||||
|
|
||||||
class="group {item.isActive ? 'active font-medium' : ''}"
|
class="group {item.isActive ? 'active font-medium' : ''}"
|
||||||
|
|
||||||
>
|
>
|
||||||
|
|
||||||
{#if item.icon}
|
{#if item.icon}
|
||||||
|
|
||||||
<Icon id={item.icon} size="20" />
|
<Icon id={item.icon} size="20" />
|
||||||
|
|
||||||
{:else}
|
{:else}
|
||||||
|
|
||||||
<!-- 无图标时的占位符,保持对齐 -->
|
<!-- 无图标时的占位符,保持对齐 -->
|
||||||
|
|
||||||
<span class="w-5 text-center text-xs opacity-50">•</span>
|
<span class="w-5 text-center text-xs opacity-50">•</span>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<span class="truncate">{item.label}</span>
|
<span class="truncate">{item.label}</span>
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
||||||
role="button"
|
role="button"
|
||||||
|
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
|
||||||
class="fixed inset-0 z-20 cursor-pointer bg-black/50 backdrop-blur-sm md:hidden"
|
class="fixed inset-0 z-20 cursor-pointer bg-black/50 backdrop-blur-sm md:hidden"
|
||||||
|
|
||||||
transition:fade={{ duration: 200 }}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<aside
|
transition:fade={{ duration: 200 }}
|
||||||
|
|
||||||
|
></div>
|
||||||
|
|
||||||
|
|
||||||
|
<aside
|
||||||
|
|
||||||
in:fly={{ duration: 200, x: -100 }}
|
in:fly={{ duration: 200, x: -100 }}
|
||||||
|
|
||||||
out:fly={{ duration: 200, x: -100 }}
|
out:fly={{ duration: 200, x: -100 }}
|
||||||
|
|
||||||
class="bg-base-200 border-base-100/70 fixed z-30 flex h-full w-64 flex-shrink-0 flex-col border-r md:relative"
|
class="bg-base-200 border-base-100/70 fixed z-30 flex h-full w-64 flex-shrink-0 flex-col border-r md:relative"
|
||||||
>
|
|
||||||
|
>
|
||||||
|
|
||||||
<div class="h-18 flex flex-shrink-0 items-center p-4">
|
<div class="h-18 flex flex-shrink-0 items-center p-4">
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
|
||||||
href={resolve('/app/dashboard')}
|
href={resolve('/app/dashboard')}
|
||||||
|
|
||||||
class="flex items-center gap-3"
|
class="flex items-center gap-3"
|
||||||
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<Icon id="logo" size="32" className="flex-shrink-0 rounded-box" />
|
<Icon id="logo" size="32" className="flex-shrink-0 rounded-box" />
|
||||||
|
|
||||||
<p class="truncate font-serif text-lg font-bold">IT DTMS</p>
|
<p class="truncate font-serif text-lg font-bold">IT DTMS</p>
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="custom-scrollbar flex-1 overflow-y-auto">
|
<div class="custom-scrollbar flex-1 overflow-y-auto">
|
||||||
|
|
||||||
<ul class="menu menu-vertical w-full gap-1 px-2">
|
<ul class="menu menu-vertical w-full gap-1 px-2">
|
||||||
|
|
||||||
{#each navItems as item (item.id)}
|
{#each navItems as item (item.id)}
|
||||||
|
|
||||||
<!-- 初始渲染调用 -->
|
<!-- 初始渲染调用 -->
|
||||||
|
|
||||||
{@render menuItem(item)}
|
{@render menuItem(item)}
|
||||||
|
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
<!-- 状态: {sidebarState.isSidebarExpanded ? '展开' : '收起'}-->
|
||||||
|
<!-- <button-->
|
||||||
|
<!-- onclick="{sidebarState.toggleSidebar}"-->
|
||||||
|
<!-- class="btn btn-square btn-ghost">-->
|
||||||
|
<!-- 123-->
|
||||||
|
<!-- </button>-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{#if page.data.user}
|
{#if page.data.user}
|
||||||
|
|
||||||
<div class="border-base-content/10 bg-base-200/50 flex-shrink-0 border-t p-3">
|
<div class="border-base-content/10 bg-base-200/50 flex-shrink-0 border-t p-3">
|
||||||
|
|
||||||
<div class="dropdown dropdown-top dropdown-end w-full">
|
<div class="dropdown dropdown-top dropdown-end w-full">
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
|
||||||
role="button"
|
role="button"
|
||||||
|
|
||||||
class="hover:bg-base-300 flex w-full cursor-pointer items-center gap-3 rounded-lg p-2 transition-colors"
|
class="hover:bg-base-300 flex w-full cursor-pointer items-center gap-3 rounded-lg p-2 transition-colors"
|
||||||
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="avatar placeholder">
|
<div class="avatar placeholder">
|
||||||
|
|
||||||
<div class="bg-neutral text-neutral-content w-10 rounded-full">
|
<div class="bg-neutral text-neutral-content w-10 rounded-full">
|
||||||
|
|
||||||
<img src={page.data.user.avatar} alt="avatar" />
|
<img src={page.data.user.avatar} alt="avatar" />
|
||||||
|
|
||||||
<span class="text-xs">User</span>
|
<span class="text-xs">User</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="flex min-w-0 flex-1 flex-col">
|
<div class="flex min-w-0 flex-1 flex-col">
|
||||||
|
|
||||||
<span class="truncate text-sm font-bold">{page.data.user.nickname}</span>
|
<span class="truncate text-sm font-bold">{page.data.user.nickname}</span>
|
||||||
|
|
||||||
<span class="text-base-content/60 truncate text-xs"
|
<span class="text-base-content/60 truncate text-xs"
|
||||||
|
|
||||||
>@{page.data.user.username}</span
|
>@{page.data.user.username}</span
|
||||||
|
|
||||||
>
|
>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<Icon id="chevron-up-down" size="16" className="opacity-50" />
|
<Icon id="chevron-up-down" size="16" className="opacity-50" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<ul
|
<ul
|
||||||
|
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
|
||||||
class="dropdown-content z-[1] menu rounded-box border-base-content/10 mb-2 w-60 border bg-base-100 p-2 shadow-lg"
|
class="dropdown-content z-[1] menu rounded-box border-base-content/10 mb-2 w-60 border bg-base-100 p-2 shadow-lg"
|
||||||
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<li class="menu-title px-4 py-2">我的账户</li>
|
<li class="menu-title px-4 py-2">我的账户</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="/app/profile"><Icon id="user-profile" size="16" /> 个人资料</a>
|
|
||||||
|
<a href="/app/user">
|
||||||
|
<Icon id="user-profile" size="16" />
|
||||||
|
个人资料</a>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="/app/settings"><Icon id="settings" size="16" /> 设置</a>
|
|
||||||
|
<a href="/app/settings">
|
||||||
|
<Icon id="settings" size="16" />
|
||||||
|
设置</a>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<div class="divider my-1"></div>
|
<div class="divider my-1"></div>
|
||||||
|
|
||||||
|
|
||||||
<li class="">
|
<li class="">
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
||||||
class="text-error w-full text-left flex items-center gap-2"
|
class="text-error w-full text-left flex items-center gap-2"
|
||||||
on:click={() => logoutForm.requestSubmit()}
|
|
||||||
|
onclick={() => logoutForm.requestSubmit()}
|
||||||
|
|
||||||
>
|
>
|
||||||
<Icon id="sign-out" size="16" /> 退出登录
|
|
||||||
|
<Icon id="sign-out" size="16" />
|
||||||
|
退出登录
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
|
|
||||||
action="/auth/logout"
|
action="/auth/logout"
|
||||||
|
|
||||||
method="POST"
|
method="POST"
|
||||||
|
|
||||||
use:enhance={handleLogout}
|
use:enhance={handleLogout}
|
||||||
|
|
||||||
bind:this={logoutForm}
|
bind:this={logoutForm}
|
||||||
|
|
||||||
hidden
|
hidden
|
||||||
|
|
||||||
>
|
>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
</aside>
|
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
/* 保持原有样式 */
|
/* 保持原有样式 */
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar {
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
|
||||||
width: 5px;
|
width: 5px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-track {
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
|
||||||
background-color: rgba(156, 163, 175, 0.3);
|
background-color: rgba(156, 163, 175, 0.3);
|
||||||
|
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
20
src/lib/stores/sidebar.svelte.ts
Normal file
20
src/lib/stores/sidebar.svelte.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export class SidebarState {
|
||||||
|
isSidebarExpanded = $state(true);
|
||||||
|
|
||||||
|
constructor(initialIsSidebarExpanded = true) {
|
||||||
|
this.isSidebarExpanded = initialIsSidebarExpanded;
|
||||||
|
}
|
||||||
|
toggleSidebar = ()=> {
|
||||||
|
this.isSidebarExpanded = !this.isSidebarExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeSidebar() {
|
||||||
|
this.isSidebarExpanded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
openSidebar() {
|
||||||
|
this.isSidebarExpanded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SIDEBAR_KEY = Symbol('SIDEBAR');
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
export interface UserProfile{
|
export interface UserProfile{
|
||||||
id: string;
|
id: number;
|
||||||
username : string;
|
username : string;
|
||||||
nickname : string;
|
nickname : string;
|
||||||
roles : string[];
|
roles : {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
avatar? : string;
|
avatar? : string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,12 @@
|
|||||||
import type { DaisyUIThemeID } from '$lib/types/theme.ts';
|
import type { DaisyUIThemeID } from '$lib/types/theme.ts';
|
||||||
import { COOKIE_THEME_KEY } from '$lib/components/constants/cookiesConstants.ts';
|
import { COOKIE_THEME_KEY } from '$lib/components/constants/cookiesConstants.ts';
|
||||||
import { TOAST_KEY, ToastState } from '$lib/stores/toast.svelte.ts';
|
import { TOAST_KEY, ToastState } from '$lib/stores/toast.svelte.ts';
|
||||||
|
import { SIDEBAR_KEY, SidebarState } from '$lib/stores/sidebar.svelte.ts';
|
||||||
let { data ,children} = $props();
|
let { data ,children} = $props();
|
||||||
|
|
||||||
setContext(THEME_KEY, new ThemeState(data.theme as DaisyUIThemeID ?? 'dark'))
|
setContext(THEME_KEY, new ThemeState(data.theme as DaisyUIThemeID ?? 'dark'))
|
||||||
setContext(TOAST_KEY,new ToastState())
|
setContext(TOAST_KEY,new ToastState())
|
||||||
|
setContext(SIDEBAR_KEY,new SidebarState())
|
||||||
const themeState = getContext<ThemeState>(THEME_KEY);
|
const themeState = getContext<ThemeState>(THEME_KEY);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { userService } from '$lib/api/services/userService.ts';
|
import { userService } from '$lib/api/services/userService.ts';
|
||||||
|
import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
export const load:PageServerLoad = async ({ locals }) => {
|
export const load:PageServerLoad = async ({ cookies }) => {
|
||||||
|
|
||||||
const allUsers = await userService.getAllUsers({ page: 1, size: 10 });
|
const token = cookies.get(COOKIE_TOKEN_KEY);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw redirect(302, '/auth/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const allUsers = await userService.getAllUsers({ page: 1, size: 10 ,token:token});
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,41 +1,80 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DataTable from '$lib/components/DataTable.svelte';
|
import { resolve } from '$app/paths';
|
||||||
import type { BaseRecord, TableColumn } from '$lib/types/dataTable.ts';
|
|
||||||
import type { PageData } from './$types'; // SvelteKit 自动生成的类型
|
const {data} = $props();
|
||||||
|
|
||||||
// 从 load 函数获取的数据
|
|
||||||
export let data: PageData;
|
|
||||||
console.log(data);
|
console.log(data);
|
||||||
// 1. 定义具体的业务接口
|
|
||||||
interface Role {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserRecord extends BaseRecord {
|
|
||||||
username: string;
|
|
||||||
nickname: string;
|
|
||||||
roles: Role[];
|
|
||||||
email?: string; // 可选字段
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 定义列配置
|
|
||||||
// 注意:这里显式声明了 TableColumn<UserRecord>[]
|
|
||||||
// 好处:如果我在 key 里写 "mobile",TS 会报错,因为 UserRecord 里没有 mobile。
|
|
||||||
const columns: TableColumn<UserRecord>[] = [
|
|
||||||
{ key: 'id', label: 'ID', width: '50px' },
|
|
||||||
{ key: 'nickname', label: '用户昵称' }, // 用 nickname 作为 key
|
|
||||||
{ key: 'roles', label: '角色列表' }, // key 必须是 'roles'
|
|
||||||
{ key: 'username', label: '登录账号' }
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
function handleEdit(e: CustomEvent<UserRecord>) {
|
|
||||||
// e.detail 自动被推断为 UserRecord 类型,不是 any
|
const {current,pages,size,total,records} = data.userList;
|
||||||
console.log(e.detail.username);
|
const rowTitles = ['ID','用户名','昵称','头像','用户组']
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center bg-base-100">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">用户列表</h1>
|
||||||
|
</div>
|
||||||
|
<div class="breadcrumbs text-sm">
|
||||||
|
<ul>
|
||||||
|
<li><a href={resolve('/app/dashboard')} >仪表盘</a></li>
|
||||||
|
<li><a href={resolve('/app/settings')} >系统设置</a></li>
|
||||||
|
<li><a href={resolve('/app/settings/auth')} >认证管理</a></li>
|
||||||
|
<li><a href={resolve('/app/settings/auth/users')} >用户管理</a></li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table">
|
||||||
|
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" class="checkbox" />
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
{#each rowTitles as title,index(index)}
|
||||||
|
<th>{title}</th>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each records as record(record.id)}
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" class="checkbox" />
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
<td>{record.id}</td>
|
||||||
|
<td>{record.username}</td>
|
||||||
|
<td>{record.nickname}</td>
|
||||||
|
<td><img class="w-8 h-8 rounded-box bg-primary-content/10 border-0" src="{record.avatar}" alt=" "></td>
|
||||||
|
<td class="flex gap-2">
|
||||||
|
{#each record.roles as role(role.id)}
|
||||||
|
|
||||||
|
<span class="badge {role.id === 1 ? 'badge-primary' : 'badge-secondary'}">{role.name}</span>
|
||||||
|
|
||||||
|
{/each}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
|
||||||
|
</th>
|
||||||
|
{#each rowTitles as title,index(index)}
|
||||||
|
<th>{title}</th>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
16
src/routes/app/user/+page.server.ts
Normal file
16
src/routes/app/user/+page.server.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { userService } from '$lib/api/services/userService.ts';
|
||||||
|
|
||||||
|
export async function load({cookies}) {
|
||||||
|
const token = cookies.get(COOKIE_TOKEN_KEY);
|
||||||
|
if (!token) {
|
||||||
|
throw redirect(302, '/auth/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = await userService.getUserProfile(token);
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/routes/app/user/+page.svelte
Normal file
9
src/routes/app/user/+page.svelte
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
这里展示个人信息
|
||||||
|
{JSON.stringify(data.profile)}
|
||||||
|
</div>
|
||||||
8
src/routes/app/user/[id]/+page.server.ts
Normal file
8
src/routes/app/user/[id]/+page.server.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export function load({ params }) {
|
||||||
|
|
||||||
|
console.log('params:', params);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
13
src/routes/app/user/[id]/+page.svelte
Normal file
13
src/routes/app/user/[id]/+page.svelte
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import {page} from '$app/state';
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<div>
|
||||||
|
{JSON.stringify(data)}
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{JSON.stringify(page.params)}
|
||||||
|
</div>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { TOAST_KEY, ToastState } from '$lib/stores/toast.svelte.ts';
|
import { TOAST_KEY, ToastState } from '$lib/stores/toast.svelte.ts';
|
||||||
|
|
||||||
|
|
||||||
const toast = getContext<ToastState>(TOAST_KEY);
|
const toast = getContext<ToastState>(TOAST_KEY);
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user