feat(应用): 实现带可折叠侧边栏的仪表盘布局

添加从根路径到/app/dashboard的服务器端重定向
创建包含主内容容器的应用布局组件
实现带响应式侧边栏和顶栏的仪表盘页面
添加带平滑过渡效果的侧边栏切换功能
设置基础用户状态管理仓库
使用对等依赖标志更新package-lock.json中的依赖项
从锁定文件中移除冗余的picomatch条目
添加带路径解析导入的登录页面组件
This commit is contained in:
Chaos
2025-11-21 17:29:11 +08:00
parent 86030df690
commit cdd14b3c85
23 changed files with 737 additions and 13 deletions

12
package-lock.json generated
View File

@@ -7,6 +7,9 @@
"": {
"name": "chaos-it",
"version": "0.0.1",
"dependencies": {
"daisyui": "^5.5.5"
},
"devDependencies": {
"@eslint/compat": "^1.4.0",
"@eslint/js": "^9.38.0",
@@ -2008,6 +2011,15 @@
"node": ">=4"
}
},
"node_modules/daisyui": {
"version": "5.5.5",
"resolved": "https://registry.npmmirror.com/daisyui/-/daisyui-5.5.5.tgz",
"integrity": "sha512-ekvI93ZkWIJoCOtDl0D2QMxnWvTejk9V5nWBqRv+7t0xjiBXqAK5U6o6JE2RPvlIC3EqwNyUoIZSdHX9MZK3nw==",
"license": "MIT",
"funding": {
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",

View File

@@ -34,5 +34,8 @@
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.1",
"vite": "^7.1.10"
},
"dependencies": {
"daisyui": "^5.5.5"
}
}

132
src/lib/api/httpClient.ts Normal file
View File

@@ -0,0 +1,132 @@
// src/lib/api/httpClient.ts
import { browser } from '$app/environment';
import type { HttpMethod, JsonObject, JsonValue } from '$lib/types/http.ts';
import { authStore } from '$lib/stores/authStore.ts';
import type { ApiResult } from '$lib/types/api.ts';
interface RequestOptions extends Omit<RequestInit, 'method' | 'body'> {
body?: JsonObject | FormData;
}
const API_BASE_URL = import.meta.env.VITE_PUBLIC_API_URL || 'http://localhost:18888/api';
let currentToken: string | null = null;
let currentTokenHead: string | null = null;
if (browser) {
// 只有在浏览器环境下才订阅,防止 SSR 内存泄漏
authStore.subscribe(state => {
currentToken = state.token;
currentTokenHead = state.tokenHead;
});
}
const normalizeHeaders = (headers?: HeadersInit):Record<string, string> =>{
const result:Record<string,string> = {};
if (!headers){
return result;
}
if (headers instanceof Headers){
headers.forEach((value, key) => {
result[key.toLowerCase()] = value;
});
}else if (Array.isArray(headers)){
headers.forEach(([key, value]) => {
result[key.toLowerCase()] = value;
})
}else {
Object.keys(headers).forEach(key => {
result[key.toLowerCase()] = headers[key.toLowerCase()] as string;
})
}
return result;
}
export class HttpError extends Error {
public status: number;
public details: JsonValue | string;
constructor(message: string, status: number, details: JsonValue | string) {
super(message);
this.name = 'HttpError';
this.status = status;
this.details = details;
// 保持正确的原型链
if (Error.captureStackTrace) {
Error.captureStackTrace(this, HttpError);
}
}
}
const httpRequest= async <T>(
url:string,
method: HttpMethod,
options: RequestOptions = {}
):Promise<ApiResult<T>> =>{
const fullUrl = `${API_BASE_URL}${url}`;
const { body, headers, ...rest} = options;
const requestHeaders: Record<string, string> = normalizeHeaders(headers);
let requestBody:BodyInit | undefined;
if (body instanceof FormData){
requestBody = body;
}else if (body){
requestHeaders['content-type'] = 'application/json';
requestBody = JSON.stringify(body);
}
if (currentToken && currentTokenHead) {
requestHeaders['authorization'] = `${currentTokenHead} ${currentToken}`;
}
try {
const response = await fetch(fullUrl,{
method,
headers: requestHeaders,
body: requestBody,
...rest
})
if (!response.ok) {
let errorDetail;
try {
errorDetail = await response.json()
}catch (e){
console.error('Error parsing JSON:', e);
errorDetail = await response.text()
}
const message = `HTTP Error ${response.status} (${response.statusText})`;
throw new HttpError(message, response.status, errorDetail);
}
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.includes('application/json')){
return (await response.json() ) as ApiResult<T>;
}
return {code:200, msg:'OK', data:null} ;
}catch (error){
console.error(`API Request Failed to ${fullUrl}:`, error);
throw error;
}
}
export const api = {
get: <T>(url: string, options?: RequestOptions) => httpRequest<T>(url, 'GET', options),
post: <T>(url: string, body: JsonObject, options?: RequestOptions) => httpRequest<T>(url, 'POST', { ...options, body }),
put: <T>(url: string, body: JsonObject, options?: RequestOptions) => httpRequest<T>(url, 'PUT', { ...options, body }),
delete: <T>(url: string, options?: RequestOptions) => httpRequest<T>(url, 'DELETE', options),
patch: <T>(url: string, body: JsonObject, options?: RequestOptions) => httpRequest<T>(url, 'PATCH', { ...options, body }),
};

View File

@@ -0,0 +1,48 @@
import {api} from '$lib/api/httpClient.ts'
import type { AuthResponse, LoginPayload } from '$lib/types/auth.ts';
import { browser } from '$app/environment';
import { authStore } from '$lib/stores/authStore.ts';
import { userService } from '$lib/api/services/userService.ts';
import { userStore } from '$lib/stores/userStore.ts';
export const authService = {
login: async (payload: LoginPayload): Promise<AuthResponse> => {
const response = await api.post<AuthResponse>('/auth/login', payload);
if (response.code != 200 || !response.data){
throw new Error(response.msg);
}
if (browser){
authService._setToken(response.data.token, response.data.tokenHead)
}
const userProfile = await userService.getUserProfile();
if (browser){
userStore.set(userProfile)
}
return response.data;
},
logout: async () => {
if (browser){
authStore.clear();
userStore.clear();
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_token_head');
return true;
}else {
return false;
}
},
_setToken: (token:string ,tokenHead: string)=> {
authStore.set({ token, tokenHead, isAuthenticated: true });
localStorage.setItem('auth_token', token);
localStorage.setItem('auth_token_head', tokenHead);
}
}

View File

@@ -0,0 +1,12 @@
import { api } from '$lib/api/httpClient.ts';
import type { UserProfile } from '$lib/types/user.ts';
export const userService = {
getUserProfile: async () => {
const response = await api.get<UserProfile>('/user/profile');
if (response.code != 200 || !response.data){
throw new Error(response.msg);
}
return response.data;
}
}

View File

@@ -0,0 +1,38 @@
import {writable} from 'svelte/store';
import { browser } from '$app/environment';
export interface AuthStore {
token: string | null;
tokenHead: string | null;
isAuthenticated: boolean;
}
let initialToken: string | null = null;
let initialTokenHead: string | null = null;
if (browser) {
initialToken = localStorage.getItem('auth_token');
initialTokenHead = localStorage.getItem('auth_token_head');
}
const initialAuthStore: AuthStore = {
token: initialToken,
tokenHead: initialTokenHead,
isAuthenticated: initialToken !== null
}
const authStatusStore = writable<AuthStore>({
token: initialToken,
tokenHead: initialTokenHead,
isAuthenticated: initialToken !== null
})
export const authStore = {
subscribe: authStatusStore.subscribe,
set: authStatusStore.set,
update: authStatusStore.update,
clear: () => {
authStatusStore.set(initialAuthStore);
},
};

View File

@@ -0,0 +1,24 @@
import { writable } from 'svelte/store';
import type { DaisyUIThemeID } from '$lib/types/theme.ts';
import { browser } from '$app/environment';
let initialTheme: DaisyUIThemeID = 'light';
if (browser){
initialTheme = localStorage.getItem('theme') as DaisyUIThemeID || 'light';
}
const themeStatusStore = writable<DaisyUIThemeID>(initialTheme);
export const themeStore = {
subscribe: themeStatusStore.subscribe,
set: (theme: DaisyUIThemeID) => {
if (browser){
localStorage.setItem('theme', theme);
}
themeStatusStore.set(theme);
},
};

View File

@@ -1,5 +1,37 @@
import {writable} from 'svelte/store';
import { type Writable, writable} from 'svelte/store';
import type { UserProfile } from '$lib/types/user.ts';
export const user = writable( null);
export const userStateStore:Writable<UserProfile > = writable<UserProfile>( {
id: '',
name: '',
nickname: '',
roles: [],
});
const initialUserProfile: UserProfile = {
id: '',
name: '',
nickname: '',
roles: [],
};
const clearUserProfile = () => {
userStore.set(initialUserProfile);
};
export const userStore = {
// 导出 subscribe 方法供组件订阅
subscribe: userStateStore.subscribe,
// 导出 set 方法
set: userStateStore.set,
// 导出 update 方法
update: userStateStore.update,
// 导出清晰的 'clear' 方法
clear: clearUserProfile
};

5
src/lib/types/api.ts Normal file
View File

@@ -0,0 +1,5 @@
export interface ApiResult<T> {
code: number,
msg: string,
data: T | null;
}

11
src/lib/types/auth.ts Normal file
View File

@@ -0,0 +1,11 @@
import type { JsonObject } from '$lib/types/http.ts';
export interface LoginPayload extends JsonObject {
username: string;
password: string;
}
export interface AuthResponse {
token: string;
tokenHead: string;
}

7
src/lib/types/http.ts Normal file
View File

@@ -0,0 +1,7 @@
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
export type JsonPrimitive = string | number | boolean | null;
export interface JsonObject {
[key:string] : JsonValue;
}
export type JsonArray = JsonValue[];
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;

192
src/lib/types/theme.ts Normal file
View File

@@ -0,0 +1,192 @@
export type DaisyUIThemeID =
| 'light' | 'dark' | 'cupcake' | 'bumblebee' | 'emerald' | 'corporate'
| 'synthwave' | 'retro' | 'cyberpunk' | 'valentine' | 'halloween' | 'garden'
| 'forest' | 'aqua' | 'lofi' | 'pastel' | 'fantasy' | 'wireframe' | 'black'
| 'luxury' | 'dracula' | 'cmyk' | 'autumn' | 'business' | 'acid' | 'lemonade'
| 'night' | 'coffee' | 'winter' | 'dim' | 'nord' | 'sunset' | 'caramellatte'
| 'abyss' | 'silk';
export interface ThemeOption {
name: string;
value: DaisyUIThemeID;
previewHTML: string;
}
export const DAISYUI_THEME_OPTIONS: ThemeOption[] = [
{
name: '浅色',
value: 'light',
previewHTML: '<div data-theme="light" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '深色',
value: 'dark',
previewHTML: '<div data-theme="dark" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '纸杯蛋糕',
value: 'cupcake',
previewHTML: '<div data-theme="cupcake" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '大黄蜂',
value: 'bumblebee',
previewHTML: '<div data-theme="bumblebee" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '祖母绿',
value: 'emerald',
previewHTML: '<div data-theme="emerald" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '企业',
value: 'corporate',
previewHTML: '<div data-theme="corporate" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '合成波',
value: 'synthwave',
previewHTML: '<div data-theme="synthwave" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '复古',
value: 'retro',
previewHTML: '<div data-theme="retro" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '赛博朋克',
value: 'cyberpunk',
previewHTML: '<div data-theme="cyberpunk" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '情人节',
value: 'valentine',
previewHTML: '<div data-theme="valentine" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '万圣节',
value: 'halloween',
previewHTML: '<div data-theme="halloween" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '花园',
value: 'garden',
previewHTML: '<div data-theme="garden" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '森林',
value: 'forest',
previewHTML: '<div data-theme="forest" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '水色',
value: 'aqua',
previewHTML: '<div data-theme="aqua" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '低保真',
value: 'lofi',
previewHTML: '<div data-theme="lofi" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '马卡龙色',
value: 'pastel',
previewHTML: '<div data-theme="pastel" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '梦幻',
value: 'fantasy',
previewHTML: '<div data-theme="fantasy" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '线框',
value: 'wireframe',
previewHTML: '<div data-theme="wireframe" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '纯黑',
value: 'black',
previewHTML: '<div data-theme="black" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '奢华',
value: 'luxury',
previewHTML: '<div data-theme="luxury" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '德古拉',
value: 'dracula',
previewHTML: '<div data-theme="dracula" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '印刷色',
value: 'cmyk',
previewHTML: '<div data-theme="cmyk" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '秋季',
value: 'autumn',
previewHTML: '<div data-theme="autumn" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '商业',
value: 'business',
previewHTML: '<div data-theme="business" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '酸性',
value: 'acid',
previewHTML: '<div data-theme="acid" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '柠檬水',
value: 'lemonade',
previewHTML: '<div data-theme="lemonade" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '夜晚',
value: 'night',
previewHTML: '<div data-theme="night" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '咖啡',
value: 'coffee',
previewHTML: '<div data-theme="coffee" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '冬季',
value: 'winter',
previewHTML: '<div data-theme="winter" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '暗淡',
value: 'dim',
previewHTML: '<div data-theme="dim" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '极光',
value: 'nord',
previewHTML: '<div data-theme="nord" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '日落',
value: 'sunset',
previewHTML: '<div data-theme="sunset" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '焦糖拿铁',
value: 'caramellatte',
previewHTML: '<div data-theme="caramellatte" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '深渊',
value: 'abyss',
previewHTML: '<div data-theme="abyss" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
{
name: '丝绸',
value: 'silk',
previewHTML: '<div data-theme="silk" class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm"><div class="bg-base-content size-1 rounded-full"></div> <div class="bg-primary size-1 rounded-full"></div> <div class="bg-secondary size-1 rounded-full"></div> <div class="bg-accent size-1 rounded-full"></div></div>',
},
];

8
src/lib/types/user.ts Normal file
View File

@@ -0,0 +1,8 @@
export interface UserProfile{
id: string;
name : string;
nickname : string;
roles : string[];
}

11
src/lib/utils/auth.ts Normal file
View File

@@ -0,0 +1,11 @@
import {user} from '$lib/stores/userStore';
import {get} from 'svelte/store';
export const hasRole = (role: string[]) => {
const userProfile = get(user);
if (!userProfile){
return false;
}
return role.some(r => userProfile.roles.includes(r))
};

View File

@@ -0,0 +1,13 @@
<script lang="ts">
/**
* @type {string} themeId - 要预览的主题的英文 ID (e.g., 'light', 'dark')
*/
export let themeId: string;
</script>
<div data-theme={themeId} class="bg-base-300 w-8 h-8 rounded-xl relative border border-accent-content/10">
<span class="w-1.5 h-1.5 bg-success absolute top-2 left-2 rounded-full"> </span>
<span class="w-1.5 h-1.5 bg-secondary absolute right-2 top-2 rounded-full"> </span>
<span class="w-1.5 h-1.5 bg-warning absolute bottom-2 left-2 rounded-full"> </span>
<span class="w-1.5 h-1.5 bg-error absolute bottom-2 right-2 rounded-full"> </span>
</div>

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import { themeStore } from '$lib/stores/themeStore.ts';
import { DAISYUI_THEME_OPTIONS, type DaisyUIThemeID } from '$lib/types/theme.ts';
import ThemePreview from '$lib/widget/ThemePreview.svelte';
// ... 逻辑保持不变 ...
const handleThemeChange = (themeValue: DaisyUIThemeID) => {
themeStore.set(themeValue);
};
</script>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="rounded hover:bg-base-100 active:bg-base-200 p-2 overflow-hidden flex items-center gap-2">
<ThemePreview themeId={$themeStore} />
<svg width="12px" height="12px" class="mt-px text-base-content size-2 fill-current opacity-60 sm:inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z"></path></svg>
</div>
<ul class="dropdown-content shadow bg-base-200 border border-base-content/10 z-[1] rounded-box w-64 max-h-80 overflow-x-hidden overflow-y-auto flex flex-col ">
{#each DAISYUI_THEME_OPTIONS as theme (theme.value)}
<li class="text-base-content w-full ">
<div
role="button"
tabindex="0"
on:click={() => handleThemeChange(theme.value)}
on:keydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleThemeChange(theme.value);
}
}}
class="gap-3 w-full flex hover:bg-base-300 active:bg-primary p-2 items-center {theme.value === $themeStore ? 'active' : ''}"
>
<ThemePreview themeId={theme.value} />
<div class=" ">{theme.name}</div>
</div>
</li>
{/each}
</ul>
</div>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import './layout.css';
import favicon from '$lib/assets/favicon.svg';
import { themeStore } from '$lib/stores/themeStore.ts';
let { children } = $props();
</script>
@@ -9,4 +9,6 @@
<link rel="icon" href={favicon} />
</svelte:head>
<div data-theme={$themeStore} class="text-base-content">
{@render children()}
</div>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import { hasRole } from '$lib/utils/auth.ts';
const isAdmin = hasRole(['admin'])
</script>
<div class="h-screen w-screen">
{#if isAdmin}
<div>
是管理员
</div>
{:else }
<div>
没有权限
</div>
{/if}
</div>

View File

@@ -0,0 +1,41 @@
/**
* 处理 GET 请求
* @returns {Response}
*/
export async function GET() {
// 假设从数据库或服务中获取用户数据
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];
return new Response(
JSON.stringify(users),
{
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
);
}
export async function POST({ request }) {
const newUser = await request.json();
// 实际应用中:将 newUser 存入数据库
console.log('新用户数据:', newUser);
return new Response(
JSON.stringify({ message: 'User created successfully', user: newUser }),
{
status: 201, // 201 Created
headers: {
'Content-Type': 'application/json'
}
}
);
}

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,43 @@
<script lang="ts">
import { authService } from '$lib/api/services/authService.ts';
import type { LoginPayload } from '$lib/types/auth.ts';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { get } from 'svelte/store';
import { authStore } from '$lib/stores/authStore.ts';
const loginPayload:LoginPayload = {
username: '',
password: ''
}
const handleSubmit = async (e: Event) => {
e.preventDefault();
try{
await authService.login(loginPayload);
if(get(authStore).isAuthenticated){
await goto(resolve('/'));
}
}catch (e){
console.error(e);
}
}
const handleChange = (e: Event) => {
const target = e.target as HTMLInputElement;
loginPayload[target.name] = target.value;
}
</script>
<div class="h-screen w-screen">
<form id="loginForm">
<div>
<label class="" for="username" > username </label>
<input type="text" name="username" on:change={handleChange} placeholder="username">
</div>
<div>
<label class="" for="password" > password </label>
<input type="password" name="password" on:change={handleChange} placeholder="password">
</div>
<button class="" type="submit" on:click="{handleSubmit}" > 登录</button>
</form>
</div>

View File

@@ -1 +1,4 @@
@import 'tailwindcss';
@plugin "daisyui"{
themes: all;
}