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 @@ + + + \ 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 @@ -
-
\ 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