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:
148
src/lib/components/DataTable.svelte
Normal file
148
src/lib/components/DataTable.svelte
Normal 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>
|
||||
Reference in New Issue
Block a user