feat(auth): implement login and user management features

- Added server-side login action with form handling and cookie storage
- Implemented user authentication service with token management
- Created user list page with data fetching from userService
- Developed reusable DataTable component with selection and pagination
- Enhanced AppSidebar with nested navigation and active state tracking
- Updated icon definitions and sprite symbols for UI consistency
- Improved HTTP client to properly handle request bodies for different methods
- Refactored auth store to manage authentication state and cookies
- Added strict typing for navigation items and table columns
- Removed obsolete code and simplified authentication flow
This commit is contained in:
Chaos
2025-11-24 17:11:41 +08:00
parent 3515faa814
commit ed542f108c
16 changed files with 472 additions and 203 deletions

View File

@@ -0,0 +1,148 @@
<script lang="ts" generics="T extends import('$lib/types/dataTable').BaseRecord">
// --- Props ---
import type { PageResult, TableColumn } from '$lib/types/dataTable.ts';
import { createEventDispatcher } from 'svelte';
export let data: PageResult<T>;
// 这里的 columns 被严格约束,传入错误的 key 会报错
export let columns: TableColumn<T>[];
export let loading: boolean = false;
// --- State ---
let selectedIds: Set<number | string> = new Set();
// 响应式计算
$: allSelected = data.records.length > 0 && data.records.every(item => selectedIds.has(item.id));
$: indeterminate = data.records.some(item => selectedIds.has(item.id)) && !allSelected;
// 定义事件,为了严格起见,我们明确 Payload 类型
const dispatch = createEventDispatcher<{
pageChange: number;
delete: T;
edit: T;
batchDelete: (number | string)[];
}>();
// --- Logic ---
function toggleAll() {
if (allSelected) {
data.records.forEach(item => selectedIds.delete(item.id));
} else {
data.records.forEach(item => selectedIds.add(item.id));
}
selectedIds = selectedIds;
}
function toggleOne(id: number | string) {
if (selectedIds.has(id)) {
selectedIds.delete(id);
} else {
selectedIds.add(id);
}
selectedIds = selectedIds;
}
function handleBatchDelete() {
dispatch('batchDelete', Array.from(selectedIds));
selectedIds = new Set();
}
</script>
<div class="bg-base-100 rounded-box shadow-md w-full border border-base-200">
<div class="p-4 border-b border-base-200 flex justify-between items-center bg-base-100 rounded-t-box">
<div class="flex gap-2 items-center">
{#if selectedIds.size > 0}
<div class="badge badge-neutral">已选 {selectedIds.size}</div>
<button class="btn btn-error btn-sm text-white" on:click={handleBatchDelete}>
批量删除
</button>
{:else}
<slot name="toolbar"></slot>
{/if}
</div>
<div><slot name="toolbar-right"></slot></div>
</div>
<div class="overflow-x-auto">
<table class="table w-full">
<thead class="bg-base-200/50">
<tr>
<th class="w-12">
<label>
<input type="checkbox" class="checkbox checkbox-sm"
checked={allSelected}
indeterminate={indeterminate}
on:change={toggleAll} />
</label>
</th>
{#each columns as col(col.key)}
<th class="{col.align === 'right' ? 'text-right' : col.align === 'center' ? 'text-center' : 'text-left'} font-semibold">
{col.label}
</th>
{/each}
<th class="text-right">操作</th>
</tr>
</thead>
<tbody>
{#if loading}
{#each Array(5) as _}
<tr><td colspan={columns.length + 2} class="skeleton h-12 w-full rounded-none opacity-50"></td></tr>
{/each}
{:else if data.records.length === 0}
<tr>
<td colspan={columns.length + 2} class="text-center py-10 text-base-content/50">
暂无数据
</td>
</tr>
{:else}
{#each data.records as row (row.id)}
<tr class="hover group {selectedIds.has(row.id) ? 'bg-base-200/30' : ''}">
<td>
<label>
<input type="checkbox" class="checkbox checkbox-sm"
checked={selectedIds.has(row.id)}
on:change={() => toggleOne(row.id)} />
</label>
</td>
{#each columns as col}
<td class="{col.align === 'right' ? 'text-right' : col.align === 'center' ? 'text-center' : 'text-left'}">
<slot name="cell" row={row} key={col.key} value={row[col.key]}>
{String(row[col.key] ?? '-')}
</slot>
</td>
{/each}
<td class="text-right">
<div class="join opacity-0 group-hover:opacity-100 transition-opacity">
<button class="btn btn-xs btn-ghost" on:click={() => dispatch('edit', row)}>编辑</button>
<button class="btn btn-xs btn-ghost text-error" on:click={() => dispatch('delete', row)}>删除</button>
</div>
</td>
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
{#if data.total > 0}
<div class="p-4 flex justify-between items-center border-t border-base-200">
<span class="text-sm opacity-60">{data.current} / {data.pages}</span>
<div class="join">
<button class="join-item btn btn-sm" disabled={data.current === 1}
on:click={() => dispatch('pageChange', data.current - 1)}>«</button>
<button class="join-item btn btn-sm pointer-events-none bg-base-100">
{data.current}
</button>
<button class="join-item btn btn-sm" disabled={data.current === data.pages}
on:click={() => dispatch('pageChange', data.current + 1)}>»</button>
</div>
</div>
{/if}
</div>