feat(应用): 实现带可折叠侧边栏的仪表盘布局
添加从根路径到/app/dashboard的服务器端重定向 创建包含主内容容器的应用布局组件 实现带响应式侧边栏和顶栏的仪表盘页面 添加带平滑过渡效果的侧边栏切换功能 设置基础用户状态管理仓库 使用对等依赖标志更新package-lock.json中的依赖项 从锁定文件中移除冗余的picomatch条目 添加带路径解析导入的登录页面组件
This commit is contained in:
12
package-lock.json
generated
12
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -34,5 +34,8 @@
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.1",
|
||||
"vite": "^7.1.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"daisyui": "^5.5.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<body data-sveltekit-preload-data="hover" >
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
132
src/lib/api/httpClient.ts
Normal file
132
src/lib/api/httpClient.ts
Normal 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 }),
|
||||
};
|
||||
48
src/lib/api/services/authService.ts
Normal file
48
src/lib/api/services/authService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
12
src/lib/api/services/userService.ts
Normal file
12
src/lib/api/services/userService.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
38
src/lib/stores/authStore.ts
Normal file
38
src/lib/stores/authStore.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
24
src/lib/stores/themeStore.ts
Normal file
24
src/lib/stores/themeStore.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
5
src/lib/types/api.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface ApiResult<T> {
|
||||
code: number,
|
||||
msg: string,
|
||||
data: T | null;
|
||||
}
|
||||
11
src/lib/types/auth.ts
Normal file
11
src/lib/types/auth.ts
Normal 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
7
src/lib/types/http.ts
Normal 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
192
src/lib/types/theme.ts
Normal 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
8
src/lib/types/user.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface UserProfile{
|
||||
id: string;
|
||||
name : string;
|
||||
nickname : string;
|
||||
roles : string[];
|
||||
}
|
||||
|
||||
|
||||
11
src/lib/utils/auth.ts
Normal file
11
src/lib/utils/auth.ts
Normal 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))
|
||||
};
|
||||
13
src/lib/widget/ThemePreview.svelte
Normal file
13
src/lib/widget/ThemePreview.svelte
Normal 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>
|
||||
41
src/lib/widget/ThemeSelector.svelte
Normal file
41
src/lib/widget/ThemeSelector.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
{@render children()}
|
||||
<div data-theme={$themeStore} class="text-base-content">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
18
src/routes/admin/+page.svelte
Normal file
18
src/routes/admin/+page.svelte
Normal 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>
|
||||
41
src/routes/api/auth/login/+server.ts
Normal file
41
src/routes/api/auth/login/+server.ts
Normal 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
@@ -1,4 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
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>
|
||||
@@ -1 +1,4 @@
|
||||
@import 'tailwindcss';
|
||||
@plugin "daisyui"{
|
||||
themes: all;
|
||||
}
|
||||
Reference in New Issue
Block a user