- 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
148 lines
4.5 KiB
Svelte
148 lines
4.5 KiB
Svelte
<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> |