refactor(settings): 重构用户管理和设备管理页面

- 调整用户管理页面角色数据获取方法,使用 getRolesOptions 替代 getAllRoles
- 更新用户表格组件接收的角色数据属性名及类型
- 修改设备管理页面路由路径,从 /device/list 调整为 /devices
- 移除调试用 console.log 输出语句
- 添加选项类型 Options 接口定义
- 优化侧边栏导航结构与交互逻辑,支持父级菜单带链接可点击
- 引入日志模块用于 API 请求与响应记录
- 升级依赖包配置,移除 peer 标记
- 微调样式类名增强布局效果和用户体验
This commit is contained in:
Chaos
2025-11-29 09:02:00 +08:00
parent 2caa8f26a3
commit 0a0e6df66b
16 changed files with 170 additions and 142 deletions

13
package-lock.json generated
View File

@@ -1473,7 +1473,6 @@
"integrity": "sha512-/rnwfSWS3qwUSzvHynUTORF9xSJi7PCR9yXkxUOnRrNqyKmCmh3FPHH+E9BbgqxXfTevGXBqgnlh9kMb+9T5XA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@sveltejs/acorn-typescript": "^1.0.5",
@@ -1513,7 +1512,6 @@
"integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
"debug": "^4.4.1",
@@ -1846,7 +1844,6 @@
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -1897,7 +1894,6 @@
"integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.47.0",
"@typescript-eslint/types": "8.47.0",
@@ -2116,7 +2112,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2493,7 +2488,6 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3614,7 +3608,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -3642,7 +3635,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -3776,7 +3768,6 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -3793,7 +3784,6 @@
"integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"prettier": "^3.0.0",
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
@@ -4520,7 +4510,6 @@
"integrity": "sha512-d1R+3pFa39LXoHCsxHmV//D2pSFZlEMlnxCVQ54TlrQv+4o5pewJO0/Pc5MUp+j71PJrOrPJHTvREZJHn+ymDQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -4718,7 +4707,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -4788,7 +4776,6 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",

View File

@@ -2,6 +2,7 @@
import type { HttpMethod, JsonObject, JsonValue } from '$lib/types/http.ts';
import type { ApiResult } from '$lib/types/api.ts';
import { log } from '$lib/log.ts';
interface RequestOptions extends Omit<RequestInit, 'method' | 'body'> {
@@ -13,8 +14,6 @@ const API_BASE_URL = import.meta.env.VITE_PUBLIC_API_URL || 'http://localhost:18
const normalizeHeaders = (headers?: HeadersInit):Record<string, string> =>{
const result:Record<string,string> = {};
console.log('normalizeHeaders', headers);
if (!headers){
return result;
}
@@ -36,7 +35,7 @@ const normalizeHeaders = (headers?: HeadersInit):Record<string, string> =>{
})
}
console.log('normalizeHeaders result:', result);
return result;
}
export class HttpError extends Error {
@@ -48,7 +47,6 @@ export class HttpError extends Error {
this.name = 'HttpError';
this.status = status;
this.details = details;
// 保持正确的原型链
if (Error.captureStackTrace) {
Error.captureStackTrace(this, HttpError);
@@ -63,10 +61,10 @@ const httpRequest = async <T>(
options: RequestOptions = {}
): Promise<ApiResult<T>> => {
const fullUrl = `${API_BASE_URL}${url}`;
log.info('API Request:', method, fullUrl)
const { body , headers, ...rest } = options;
const requestHeaders: Record<string, string> = normalizeHeaders(headers);
let requestBody: BodyInit | undefined;
@@ -85,7 +83,7 @@ const httpRequest = async <T>(
try {
log.debug('API Request Body:', requestBody)
const response = await fetch(fullUrl, {
method,
headers: requestHeaders,
@@ -94,24 +92,27 @@ const httpRequest = async <T>(
body: canHaveBody ? requestBody : undefined,
...rest
});
console.log('response', response);
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})`;
log.warn(message)
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>;
const res = await response.json();
log.info('API Response:', res)
return (res) as ApiResult<T>;
}
return { code: 200, msg: 'OK', data: null } ; // 这里的 as any 是为了兼容 T 可能是 null 的情况

View File

@@ -1,9 +1,10 @@
import { api } from '$lib/api/httpClient.ts';
import type { RoleResponse } from '$lib/types/user.ts';
import type { Options } from '$lib/types/api.ts';
export const roleService = {
getAllRoles: async (token:string) => {
const response = await api.get<RoleResponse[]>('/role/', {headers: {Authorization: `${token}`}});
getRolesOptions: async (token:string) => {
const response = await api.get<Options[]>('/roles/options', {headers: {Authorization: `${token}`}});
if (response.code != 200 || !response.data){
throw new Error(response.msg);
}

View File

@@ -4,7 +4,7 @@ import type { PageResult } from '$lib/types/dataTable.ts';
export const userService = {
getUserProfile: async (token:string) => {
const response = await api.get<UserProfile>('/user/profile', {headers: {Authorization: `${token}`}});
const response = await api.get<UserProfile>('/users/me', {headers: {Authorization: `${token}`}});
if (response.code != 200 || !response.data){
throw new Error(response.msg);
}
@@ -15,7 +15,7 @@ export const userService = {
formData.append('pageNum', page.toString());
formData.append('pageSize', size.toString());
const response = await api.get<PageResult<UserProfile[]>>(
'/user/all',
'/users',
{
body: formData,
headers: {Authorization: `${token}`}

View File

@@ -6,7 +6,7 @@
</script>
<header class="w-full h-18 flex justify-between items-center px-4 bg-base-300 flex-shrink-0">
<header class="w-full h-18 flex justify-between items-center px-4 bg-base-300 flex-shrink-0 z-10">
<div>
<!-- <button-->
<!-- class="btn btn-square btn-ghost"-->
@@ -32,7 +32,7 @@
>
{#if page.data.user.avatar}
<img
class="w-8 h-8 rounded-full z-10"
class="w-8 h-8 rounded-full "
src="{page.data.user.avatar}"
alt="Avatar"
/>

View File

@@ -6,12 +6,9 @@
import Icon from '$lib/components/icon/Icon.svelte';
import type { NavItem, ProcessedNavItem } from '$lib/types/layout';
import { TOAST_KEY, type ToastState } from '$lib/stores/toast.svelte.ts';
// import { SIDEBAR_KEY, SidebarState } from '$lib/stores/sidebar.svelte.ts';
import { enhance } from '$app/forms';
// const sidebarState = getContext<SidebarState>(SIDEBAR_KEY);
// 1. 模拟数据:包含三层结构
// 1. 模拟数据:现在给父级 "device" 也加上了 href
const rawNavItems: NavItem[] = [
{
id: 'dashboard',
@@ -29,7 +26,7 @@
id: 'settings',
label: '系统设置',
icon: 'settings',
href: '/app/settings',
href: '/app/settings', // 父级带链接
subItems: [
{
id: 'auth',
@@ -54,21 +51,17 @@
}
]
},
{
id: "device",
label: '设备管理',
icon: 'laptop-settings',
subItems: [
{
id: 'device',
label: '设备管理',
href: '/app/settings/device/list'
},
icon: 'laptop-settings',
href: '/app/settings/devices', // 【修改点】父级现在有链接了,指向列表页
subItems: [
{
id: 'type',
id: 'device-type',
label: '类型管理',
href: '/app/settings/device/type'
},
href: '/app/settings/devices/type'
}
]
},
{
@@ -93,64 +86,50 @@
];
/**
* 递归计算高亮状态 (强类型版本)
* 递归计算高亮状态 (逻辑修复版)
* 修复了当 href 为 undefined 时的潜在报错,并增强了激活判断
*/
function processNavItems(items: NavItem[], currentPath: string): ProcessedNavItem[] {
return items.map((item) => {
const isSelfActive =
item.href === '/' ? currentPath === '/' : currentPath.startsWith(item.href);
// 安全获取 href
const href = item.href || '';
// 判断自身是否激活
// 如果 href 存在,且 (是根路径全等 OR 当前路径以 href 开头)
const isSelfActive = href
? (href === '/' ? currentPath === '/' : currentPath.startsWith(href))
: false;
let processedSubItems: ProcessedNavItem[] | undefined = undefined;
let isChildActive = false;
if (item.subItems) {
// 递归调用
processedSubItems = processNavItems(item.subItems, currentPath);
// 检查子项激活状态
// 只要有一个子项激活,或者子项的子项激活,父级就算 ChildActive
isChildActive = processedSubItems.some((sub) => sub.isActive || sub.isChildActive);
}
return {
...item,
subItems: processedSubItems, // 这里类型现在是 ProcessedNavItem[]
subItems: processedSubItems,
isActive: isSelfActive,
isChildActive: isChildActive
};
});
}
// 使用 $derived 动态计算,类型自动推断为 ProcessedNavItem[]
// 使用 $derived 动态计算
let navItems = $derived(processNavItems(rawNavItems, page.url.pathname));
// 获取 Toast 以便提示用户
// Toast & Logout 逻辑
const toast = getContext<ToastState>(TOAST_KEY);
// 处理提交结果的回调
const handleLogout = () => {
toast.info('正在退出登录...');
return async ({ result, update }) => {
// result.type 可能是 'redirect', 'success', 'failure'
if (result.type === 'redirect') {
toast.success('您已安全退出');
}
// update() 会触发默认行为(也就是执行 redirect 跳转)
await update();
};
};
@@ -158,44 +137,64 @@
let logoutForm: HTMLFormElement;
</script>
<!-- 定义递归 Snippet显式指定类型 -->
<!--
定义递归 Snippet
【修改点】:在 summary 内部增加了 conditional rendering支持父级点击跳转
-->
{#snippet menuItem(item: ProcessedNavItem)}
<li>
{#if item.subItems && item.subItems.length > 0}
<details open={item.isChildActive}>
<summary class="group {item.isActive ? 'font-medium text-primary' : ''}">
<!--
open 属性控制:
如果是自身激活(点了父级链接) 或者 子项激活(点了子级),都保持展开状态
-->
<details open={item.isActive || item.isChildActive}>
<summary class="group {item.isActive ? 'text-primary' : ''}">
{#if item.icon}
<Icon id={item.icon} size="20" />
{/if}
<!-- 【核心修改】:父级如果有 href渲染为链接 -->
{#if item.href}
<!--
onclick stopPropagation 是关键:
阻止冒泡,防止点击链接时触发 <details> 的 toggle 行为。
这样点击文字是跳转,点击右侧箭头/空白是展开折叠。
-->
<a
href={resolve(item.href)}
class="flex-1 truncate hover:text-primary-focus transition-colors"
onclick={(e) => e.stopPropagation()}
>
{item.label}
</a>
{:else}
<!-- 没有 href纯文本点击整行触发展开折叠 -->
<span class="truncate">{item.label}</span>
{/if}
</summary>
<ul>
{#each item.subItems as subItem (subItem.id)}
<!-- 递归渲染子项 -->
{@render menuItem(subItem)}
{/each}
</ul>
</details>
{:else}
<a href={resolve(item.href)} class="group {item.isActive ? 'active font-medium' : ''}">
<!-- 无子菜单的普通叶子节点 -->
<a href={resolve(item.href || '#')} class="group {item.isActive ? 'active font-medium' : ''}">
{#if item.icon}
<Icon id={item.icon} size="20" />
{:else}
<!-- 无图标时的占位符,保持对齐 -->
<span class="w-5 text-center text-xs opacity-50"></span>
{/if}
<span class="truncate">{item.label}</span>
</a>
{/if}
</li>
{/snippet}
<!-- Mobile Backdrop -->
<div
class="fixed inset-0 z-20 cursor-pointer bg-black/50 backdrop-blur-sm md:hidden"
role="button"
@@ -203,6 +202,7 @@
transition:fade={{ duration: 200 }}
></div>
<!-- Sidebar Container -->
<aside
class="fixed z-30 flex h-full w-64 flex-shrink-0 flex-col border-r border-base-100/70 bg-base-200 md:relative"
in:fly={{ duration: 200, x: -100 }}
@@ -211,7 +211,6 @@
<div class="flex h-18 flex-shrink-0 items-center p-4">
<a class="flex items-center gap-3" href={resolve('/app/dashboard')}>
<Icon className="flex-shrink-0 rounded-box" id="logo" size="32" />
<p class="truncate font-serif text-lg font-bold">IT DTMS</p>
</a>
</div>
@@ -219,19 +218,12 @@
<div class="custom-scrollbar flex-1 overflow-y-auto">
<ul class="menu menu-vertical w-full gap-1 px-2">
{#each navItems as item (item.id)}
<!-- 初始渲染调用 -->
{@render menuItem(item)}
{/each}
</ul>
<!-- 状态: {sidebarState.isSidebarExpanded ? '展开' : '收起'}-->
<!-- <button-->
<!-- onclick="{sidebarState.toggleSidebar}"-->
<!-- class="btn btn-square btn-ghost">-->
<!-- 123-->
<!-- </button>-->
</div>
<!-- User Profile Section -->
{#if page.data.user}
<div class="flex-shrink-0 border-t border-base-content/10 bg-base-200/50 p-3">
<div class="dropdown dropdown-end dropdown-top w-full">
@@ -243,14 +235,12 @@
<div class="placeholder avatar">
<div class="w-10 rounded-full bg-neutral text-neutral-content">
<img src={page.data.user.avatar} alt="avatar" />
<span class="text-xs">User</span>
</div>
</div>
<div class="flex min-w-0 flex-1 flex-col">
<span class="truncate text-sm font-bold">{page.data.user.nickname}</span>
<span class="truncate text-xs text-base-content/60">@{page.data.user.username}</span>
</div>
@@ -262,23 +252,19 @@
class="dropdown-content menu z-[1] mb-2 w-60 rounded-box border border-base-content/10 bg-base-100 p-2 shadow-lg"
>
<li class="menu-title px-4 py-2">我的账户</li>
<li>
<a href="/app/user">
<Icon id="user-profile" size="16" />
个人资料</a
>
</li>
<li>
<a href="/app/settings">
<Icon id="settings" size="16" />
设置</a
>
</li>
<div class="divider my-1"></div>
<li class="">
<button
class="flex w-full items-center gap-2 text-left text-error"
@@ -288,7 +274,6 @@
退出登录
</button>
</li>
<form
action="/auth/logout"
method="POST"
@@ -303,19 +288,14 @@
</aside>
<style>
/* 保持原有样式 */
.custom-scrollbar::-webkit-scrollbar {
width: 5px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.3);
border-radius: 20px;
}
</style>

View File

@@ -2,13 +2,13 @@
import type { PageResult } from '$lib/types/dataTable.ts';
import type { RoleResponse, UserProfile } from '$lib/types/user.ts';
import { resolve } from '$app/paths';
import type { UserProfile } from '$lib/types/user.ts';
import Icon from '$lib/components/icon/Icon.svelte';
import type { Options } from '$lib/types/api.ts';
let { users , roles } = $props<{
let { users , rolesOptions } = $props<{
users: PageResult<UserProfile[]>,
roles: RoleResponse[]
rolesOptions: Options[]
}>();
@@ -32,9 +32,6 @@
<div>
{#if users.total > 0}
<div class="">
<div class="overflow-x-auto rounded-box shadow bg-base-100 mt-1 ">
<div class="flex items-center justify-between px-4 pt-4 pb-2">
<div class="flex gap-4">
@@ -54,11 +51,11 @@
<input type="search" required placeholder="Search" />
<button class="btn btn-xs btn-primary">搜索</button>
</label>
{#if roles}
{#if rolesOptions}
<div class="filter w-64">
<input class="btn filter-reset " type="radio" value='' name="metaframeworks" aria-label="All" onchange={handleRoleChange} />
{#each roles as role(role.id)}
<input class="btn " type="radio" name="metaframeworks" aria-label="{role.name}" value={role.id} onchange={handleRoleChange} />
{#each rolesOptions as role(role.value)}
<input class="btn" type="radio" name="metaframeworks" aria-label="{role.label}" value={role.value} onchange={handleRoleChange} />
{/each}
</div>
{/if}

43
src/lib/log.ts Normal file
View File

@@ -0,0 +1,43 @@
import { browser } from '$app/environment';
type LogArgs = unknown[];
const getTime = () => new Date().toLocaleTimeString('zh-CN', { hour12: false });
function print(level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR', message: string, args: LogArgs) {
if (browser) {
const styles = {
DEBUG: 'color: #999; font-weight: bold;',
INFO: 'color: #2196f3; font-weight: bold;',
WARN: 'color: #ff9800; font-weight: bold;',
ERROR: 'color: #f44336; font-weight: bold;',
};
console.log(`%c[${level}] ${message}`, styles[level], ...args);
} else {
const colors = {
DEBUG: '\x1b[90m',
INFO: '\x1b[34m',
WARN: '\x1b[33m',
ERROR: '\x1b[31m',
};
const reset = '\x1b[0m';
console.log(
`${colors[level]}[${getTime()}] [${level}] ${message}${reset}`,
...args
);
}
}
export const log = {
debug: (message: string, ...args: LogArgs) => print('DEBUG', message, args),
info: (message: string, ...args: LogArgs) => print('INFO', message, args),
warn: (message: string, ...args: LogArgs) => print('WARN', message, args),
error: (message: string, ...args: LogArgs) => print('ERROR', message, args),
};

View File

@@ -48,3 +48,8 @@ export interface CreateDeviceRequest {
status: number;
remark: string;
}
export interface Options {
label: string;
value: string;
}

View File

@@ -9,7 +9,7 @@
<AppSidebar />
<div class="flex-1 flex flex-col min-w-0 overflow-hidden">
<div class="flex-1 flex flex-col min-w-0 overflow-hidden relative">
<AppHeader />

View File

@@ -0,0 +1,3 @@
<div>
123
</div>

View File

@@ -13,7 +13,7 @@ export const load:PageServerLoad = async ({ cookies }) => {
}
const getRoles = async() => {
return await roleService.getAllRoles(token);
return await roleService.getRolesOptions(token);
}
@@ -26,7 +26,7 @@ export const load:PageServerLoad = async ({ cookies }) => {
streamed:{
userList: getUserList(),
roles: getRoles(),
rolesOptions: getRoles(),
}
};
};

View File

@@ -5,24 +5,34 @@ import { resolve } from '$app/paths';
const {data} = $props();
</script>
<div class="flex justify-between items-center ">
<div class="flex justify-between items-center select-none">
<p class="font-bold">用户管理</p>
<div class="breadcrumbs ">
<div class="breadcrumbs textarea-md text-base-content/70 ">
<ul>
<li><a href={resolve('/app/dashboard')}>仪表盘</a></li>
<li><a href={resolve('/app/settings')}>系统设置</a></li>
<li><a href={resolve('/app/settings/auth')}>认证管理</a></li>
<li>系统设置</li>
<li>认证管理</li>
<li><a href={resolve('/app/settings/auth/users')}>用户管理</a></li>
</ul>
</div>
</div>
{#await data.streamed.userList}
加载中
<div class=" flex flex-col items-center justify-center absolute top-0 left-0 bottom-0 right-0 backdrop-blur-2xl z-0">
<div class="loading w-28 h-28">
</div>
<p class="text-base-content mt-4">加载中...</p>
</div>
{:then result}
{#await data.streamed.roles}
加载中
{:then roles}
<UserTable users={result} roles={roles}/>
{#await data.streamed.rolesOptions}
<div class=" flex flex-col items-center justify-center absolute top-0 left-0 bottom-0 right-0 backdrop-blur-2xl z-0">
<div class="loading w-28 h-28">
</div>
<p class="text-base-content mt-4">加载中...</p>
</div>
{:then rolesOptions}
<UserTable users={result} rolesOptions={rolesOptions}/>
{:catch err}
<p>出错了: {err.message}</p>
{/await}

View File

@@ -12,7 +12,8 @@ export const load:PageServerLoad = async ({ cookies }) => {
}
const result = await deviceService.getAllDevices({ page: 1, size: 10 ,token:token});
console.log('result', result);
return {

View File

@@ -3,7 +3,7 @@ import { resolve } from '$app/paths';
const {data} = $props();
console.log( data.streamed.deviceList);
console.log( data.streamed.list);
</script>
<div class="flex justify-between items-center ">
@@ -12,12 +12,12 @@ console.log( data.streamed.deviceList);
<ul>
<li><a href={resolve('/app/dashboard')}>仪表盘</a></li>
<li><a href={resolve('/app/settings')}>系统设置</a></li>
<li><a href={resolve('/app/settings/device/list')}>设备管理</a></li>
<li><a href={resolve('/app/settings/devices')}>设备管理</a></li>
</ul>
</div>
</div>
{#await data.streamed.deviceList}
{#await data.streamed.list}
<p class="text-center">正在加载设备列表...</p>
<p class="text-center">请稍后...</p>
{:then result}