diff --git a/package-lock.json b/package-lock.json
index 942581c..0c6b5cd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index c87d039..ea34e7b 100644
--- a/package.json
+++ b/package.json
@@ -34,5 +34,8 @@
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.1",
"vite": "^7.1.10"
+ },
+ "dependencies": {
+ "daisyui": "^5.5.5"
}
}
diff --git a/src/app.html b/src/app.html
index f273cc5..4939117 100644
--- a/src/app.html
+++ b/src/app.html
@@ -5,7 +5,7 @@
%sveltekit.head%
-
+
%sveltekit.body%
diff --git a/src/lib/api/httpClient.ts b/src/lib/api/httpClient.ts
new file mode 100644
index 0000000..92ea48e
--- /dev/null
+++ b/src/lib/api/httpClient.ts
@@ -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 {
+ 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 =>{
+ const result:Record = {};
+
+ 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 (
+ url:string,
+ method: HttpMethod,
+ options: RequestOptions = {}
+):Promise> =>{
+ const fullUrl = `${API_BASE_URL}${url}`;
+ const { body, headers, ...rest} = options;
+
+ const requestHeaders: Record = 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;
+ }
+
+ return {code:200, msg:'OK', data:null} ;
+
+ }catch (error){
+ console.error(`API Request Failed to ${fullUrl}:`, error);
+ throw error;
+ }
+
+}
+
+
+export const api = {
+ get: (url: string, options?: RequestOptions) => httpRequest(url, 'GET', options),
+ post: (url: string, body: JsonObject, options?: RequestOptions) => httpRequest(url, 'POST', { ...options, body }),
+ put: (url: string, body: JsonObject, options?: RequestOptions) => httpRequest(url, 'PUT', { ...options, body }),
+ delete: (url: string, options?: RequestOptions) => httpRequest(url, 'DELETE', options),
+ patch: (url: string, body: JsonObject, options?: RequestOptions) => httpRequest(url, 'PATCH', { ...options, body }),
+};
\ No newline at end of file
diff --git a/src/lib/api/services/authService.ts b/src/lib/api/services/authService.ts
new file mode 100644
index 0000000..78c9609
--- /dev/null
+++ b/src/lib/api/services/authService.ts
@@ -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 => {
+ const response = await api.post('/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);
+ }
+}
\ No newline at end of file
diff --git a/src/lib/api/services/userService.ts b/src/lib/api/services/userService.ts
new file mode 100644
index 0000000..dbc34af
--- /dev/null
+++ b/src/lib/api/services/userService.ts
@@ -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('/user/profile');
+ if (response.code != 200 || !response.data){
+ throw new Error(response.msg);
+ }
+ return response.data;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/stores/authStore.ts b/src/lib/stores/authStore.ts
new file mode 100644
index 0000000..fc80f2c
--- /dev/null
+++ b/src/lib/stores/authStore.ts
@@ -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({
+ token: initialToken,
+ tokenHead: initialTokenHead,
+ isAuthenticated: initialToken !== null
+})
+
+export const authStore = {
+ subscribe: authStatusStore.subscribe,
+ set: authStatusStore.set,
+ update: authStatusStore.update,
+ clear: () => {
+ authStatusStore.set(initialAuthStore);
+ },
+};
\ No newline at end of file
diff --git a/src/lib/stores/themeStore.ts b/src/lib/stores/themeStore.ts
new file mode 100644
index 0000000..e10030e
--- /dev/null
+++ b/src/lib/stores/themeStore.ts
@@ -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(initialTheme);
+
+export const themeStore = {
+ subscribe: themeStatusStore.subscribe,
+ set: (theme: DaisyUIThemeID) => {
+ if (browser){
+ localStorage.setItem('theme', theme);
+ }
+ themeStatusStore.set(theme);
+ },
+};
+
diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts
index 295ebb5..7e7452d 100644
--- a/src/lib/stores/userStore.ts
+++ b/src/lib/stores/userStore.ts
@@ -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 = writable( {
+ 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
+};
\ No newline at end of file
diff --git a/src/lib/types/api.ts b/src/lib/types/api.ts
new file mode 100644
index 0000000..c9caed6
--- /dev/null
+++ b/src/lib/types/api.ts
@@ -0,0 +1,5 @@
+export interface ApiResult {
+ code: number,
+ msg: string,
+ data: T | null;
+}
\ No newline at end of file
diff --git a/src/lib/types/auth.ts b/src/lib/types/auth.ts
new file mode 100644
index 0000000..e12d952
--- /dev/null
+++ b/src/lib/types/auth.ts
@@ -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;
+}
\ No newline at end of file
diff --git a/src/lib/types/http.ts b/src/lib/types/http.ts
new file mode 100644
index 0000000..1a89b88
--- /dev/null
+++ b/src/lib/types/http.ts
@@ -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;
diff --git a/src/lib/types/theme.ts b/src/lib/types/theme.ts
new file mode 100644
index 0000000..10d5a9b
--- /dev/null
+++ b/src/lib/types/theme.ts
@@ -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: '',
+ },
+ {
+ name: '深色',
+ value: 'dark',
+ previewHTML: '',
+ },
+ {
+ name: '纸杯蛋糕',
+ value: 'cupcake',
+ previewHTML: '',
+ },
+ {
+ name: '大黄蜂',
+ value: 'bumblebee',
+ previewHTML: '',
+ },
+ {
+ name: '祖母绿',
+ value: 'emerald',
+ previewHTML: '',
+ },
+ {
+ name: '企业',
+ value: 'corporate',
+ previewHTML: '',
+ },
+ {
+ name: '合成波',
+ value: 'synthwave',
+ previewHTML: '',
+ },
+ {
+ name: '复古',
+ value: 'retro',
+ previewHTML: '',
+ },
+ {
+ name: '赛博朋克',
+ value: 'cyberpunk',
+ previewHTML: '',
+ },
+ {
+ name: '情人节',
+ value: 'valentine',
+ previewHTML: '',
+ },
+ {
+ name: '万圣节',
+ value: 'halloween',
+ previewHTML: '',
+ },
+ {
+ name: '花园',
+ value: 'garden',
+ previewHTML: '',
+ },
+ {
+ name: '森林',
+ value: 'forest',
+ previewHTML: '',
+ },
+ {
+ name: '水色',
+ value: 'aqua',
+ previewHTML: '',
+ },
+ {
+ name: '低保真',
+ value: 'lofi',
+ previewHTML: '',
+ },
+ {
+ name: '马卡龙色',
+ value: 'pastel',
+ previewHTML: '',
+ },
+ {
+ name: '梦幻',
+ value: 'fantasy',
+ previewHTML: '',
+ },
+ {
+ name: '线框',
+ value: 'wireframe',
+ previewHTML: '',
+ },
+ {
+ name: '纯黑',
+ value: 'black',
+ previewHTML: '',
+ },
+ {
+ name: '奢华',
+ value: 'luxury',
+ previewHTML: '',
+ },
+ {
+ name: '德古拉',
+ value: 'dracula',
+ previewHTML: '',
+ },
+ {
+ name: '印刷色',
+ value: 'cmyk',
+ previewHTML: '',
+ },
+ {
+ name: '秋季',
+ value: 'autumn',
+ previewHTML: '',
+ },
+ {
+ name: '商业',
+ value: 'business',
+ previewHTML: '',
+ },
+ {
+ name: '酸性',
+ value: 'acid',
+ previewHTML: '',
+ },
+ {
+ name: '柠檬水',
+ value: 'lemonade',
+ previewHTML: '',
+ },
+ {
+ name: '夜晚',
+ value: 'night',
+ previewHTML: '',
+ },
+ {
+ name: '咖啡',
+ value: 'coffee',
+ previewHTML: '',
+ },
+ {
+ name: '冬季',
+ value: 'winter',
+ previewHTML: '',
+ },
+ {
+ name: '暗淡',
+ value: 'dim',
+ previewHTML: '',
+ },
+ {
+ name: '极光',
+ value: 'nord',
+ previewHTML: '',
+ },
+ {
+ name: '日落',
+ value: 'sunset',
+ previewHTML: '',
+ },
+ {
+ name: '焦糖拿铁',
+ value: 'caramellatte',
+ previewHTML: '',
+ },
+ {
+ name: '深渊',
+ value: 'abyss',
+ previewHTML: '',
+ },
+ {
+ name: '丝绸',
+ value: 'silk',
+ previewHTML: '',
+ },
+];
\ No newline at end of file
diff --git a/src/lib/types/user.ts b/src/lib/types/user.ts
new file mode 100644
index 0000000..7d247a6
--- /dev/null
+++ b/src/lib/types/user.ts
@@ -0,0 +1,8 @@
+export interface UserProfile{
+ id: string;
+ name : string;
+ nickname : string;
+ roles : string[];
+}
+
+
diff --git a/src/lib/utils/auth.ts b/src/lib/utils/auth.ts
new file mode 100644
index 0000000..73c1745
--- /dev/null
+++ b/src/lib/utils/auth.ts
@@ -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))
+};
\ No newline at end of file
diff --git a/src/lib/widget/ThemePreview.svelte b/src/lib/widget/ThemePreview.svelte
new file mode 100644
index 0000000..4e0305a
--- /dev/null
+++ b/src/lib/widget/ThemePreview.svelte
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/widget/ThemeSelector.svelte b/src/lib/widget/ThemeSelector.svelte
new file mode 100644
index 0000000..e4596fb
--- /dev/null
+++ b/src/lib/widget/ThemeSelector.svelte
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+ {#each DAISYUI_THEME_OPTIONS as theme (theme.value)}
+
+
+ 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' : ''}"
+ >
+
+
{theme.name}
+
+
+ {/each}
+
+
\ No newline at end of file
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index b0cd4fb..e47ab22 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -1,7 +1,7 @@
@@ -9,4 +9,6 @@
-{@render children()}
+
+ {@render children()}
+
diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte
new file mode 100644
index 0000000..483c8d6
--- /dev/null
+++ b/src/routes/admin/+page.svelte
@@ -0,0 +1,18 @@
+
+
+
+ {#if isAdmin}
+
+ 是管理员
+
+ {:else }
+
+ 没有权限
+
+ {/if}
+
\ No newline at end of file
diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts
new file mode 100644
index 0000000..18e9c92
--- /dev/null
+++ b/src/routes/api/auth/login/+server.ts
@@ -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'
+ }
+ }
+ );
+}
\ No newline at end of file
diff --git a/src/routes/app/dashboard/+page.svelte b/src/routes/app/dashboard/+page.svelte
index 341144c..185c059 100644
--- a/src/routes/app/dashboard/+page.svelte
+++ b/src/routes/app/dashboard/+page.svelte
@@ -1,13 +1,23 @@
-
-
+
@@ -24,14 +34,36 @@
1
1
1
-
+ {$userStore.name}
+ {$userStore.id}
+ {$userStore.nickname}
\ No newline at end of file
diff --git a/src/routes/auth/login/+page.svelte b/src/routes/auth/login/+page.svelte
index c5e4f70..914851a 100644
--- a/src/routes/auth/login/+page.svelte
+++ b/src/routes/auth/login/+page.svelte
@@ -1,4 +1,43 @@
+
\ No newline at end of file
diff --git a/src/routes/layout.css b/src/routes/layout.css
index d4b5078..762110a 100644
--- a/src/routes/layout.css
+++ b/src/routes/layout.css
@@ -1 +1,4 @@
@import 'tailwindcss';
+@plugin "daisyui"{
+ themes: all;
+}
\ No newline at end of file