Files
bid-setup.tootaio.com/app/pages/index.vue
xiaomai 8cc389630e fix(ui): ensure table updates correctly on data changes
The UTable component was not reliably updating when items were added, edited, or removed due to a reactivity issue. This commit resolves the problem by forcing the table to re-render when its data source
changes.

- A `watch` with `{ deep: true }` is now used to monitor the `items` array for any changes.
- A `tableKey` is incremented on each change and passed as a `:key` to `UTable`, triggering a re-mount.

Additionally, data handling has been made more robust:
- The localStorage serializer now gracefully handles parsing errors and ensures data integrity for dates and tags.
- Added null-safe access for tags in the table template to prevent rendering errors.
2025-10-14 14:09:36 +08:00

265 lines
6.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 统计 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div
v-for="stat in statistics"
:key="stat.label"
class="bg-white rounded-xl p-6 shadow-sm border border-gray-100"
>
<div class="flex items-center">
<div
:class="`p-3 rounded-lg flex bg-${stat.color}-100 items-center`"
>
<Icon
class="w-6 h-6"
:name="stat.icon"
:style="`color: ${stat.color}`"
/>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">{{ stat.label }}</p>
<p class="text-2xl font-bold text-gray-900">{{ stat.value }}</p>
</div>
</div>
</div>
</div>
<!-- 列表 -->
<div class="flex flex-col flex-1 w-full">
<div
class="flex justify-between items-center gap-2 overflow-x-auto px-4 py-3.5 border-b border-accented"
>
<div>
<UInput
v-model="globalFilter"
class="max-w-sm"
placeholder="Search..."
icon="lucide:search"
/>
</div>
<div>
<ItemEditModal ref="itemEditModal" />
</div>
</div>
<!-- UTable 一个 keytableKey用于在需要时强制重挂载 -->
<UTable
:key="tableKey"
v-model:global-filter="globalFilter"
:data="itemsList"
:columns="itemColumns"
>
<template #select-cell="{ row }">
<UCheckbox
:model-value="row.getIsSelected()"
@update:model-value="(value: boolean | 'indeterminate') => row.toggleSelected(!!value)"
aria-label="Select row"
/>
</template>
<template #name-cell="{ row }">
<div class="flex items-center gap-3">
<UAvatar
:src="row.original.imageUrl"
size="lg"
:alt="row.original.name"
/>
<div>
<p class="font-medium text-highlighted">
{{ row.original.name }}
</p>
</div>
</div>
</template>
<!-- tags 空值兜底,并加 key 避免重复 key 警告 -->
<template #tags-cell="{ row }">
<div class="flex gap-2">
<UBadge
v-for="(tag, idx) in row.original.tags ?? []"
:key="`${row.original.id}-tag-${idx}`"
variant="subtle"
>
{{ tag }}
</UBadge>
</div>
</template>
<template #actions-cell="{ row }">
<div class="text-right">
<UDropdownMenu
:items="getRowItems(row)"
:content="{ align: 'end' }"
aria-label="Actions dropdown"
>
<UButton
icon="i-lucide-ellipsis-vertical"
color="neutral"
variant="ghost"
class="ml-auto"
aria-label="Actions dropdown"
/>
</UDropdownMenu>
</div>
</template>
</UTable>
</div>
<ItemDeleteModal
v-model:open="deleteModal.open"
:item="deleteModal.selectedItem"
/>
</main>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, reactive, watch } from "vue";
import type { TableColumn, DropdownMenuItem } from "@nuxt/ui";
import type { Row } from "@tanstack/vue-table";
const UCheckbox = resolveComponent("UCheckbox");
const UBadge = resolveComponent("UBadge");
const UAvatar = resolveComponent("UAvatar");
const toast = useToast();
const { items, stats } = useItemsStore();
const itemEditModal = ref<any>(null);
const deleteModal = reactive<{ open: boolean; selectedItem: Item | null }>({
open: false,
selectedItem: null,
});
const globalFilter = ref<string>("");
// --- computed 包装,保证传给 UTable 的始终是响应式且值随 items 变化
const itemsList = computed(() => items.value ?? []);
// --- 强制表格重挂载的 key当 items 改变时递增)
const tableKey = ref(0);
// 深度监听 items数组变化每次 items 内容或引用变化都让 tableKey++,从而触发 UTable 重新挂载
watch(
items,
() => {
tableKey.value += 1;
// Console debug运行时可在浏览器控制台查看
console.debug(
"[items watch] tableKey ->",
tableKey.value,
"items.length ->",
(items.value ?? []).length
);
},
{ deep: true }
);
// 统计
const statistics = [
{
icon: "lucide:box",
label: "总物品数",
value: stats.totalItems,
color: "blue",
},
{
icon: "lucide:package-plus",
label: "本月新增",
value: stats.addedThisMonth,
color: "green",
},
{
icon: "lucide:history",
label: "历史记录",
value: computed(() => 0),
color: "orange",
},
{
icon: "lucide:image",
label: "图片总数",
value: computed(() => 0),
color: "purple",
},
];
const itemColumns: TableColumn<Item>[] = [
{
id: "select",
header: ({ table }) =>
h(UCheckbox, {
modelValue: table.getIsSomePageRowsSelected()
? "indeterminate"
: table.getIsAllPageRowsSelected(),
"onUpdate:modelValue": (value: boolean | "indeterminate") =>
table.toggleAllPageRowsSelected(!!value),
ariaLabel: "Select all",
}),
},
{
accessorKey: "id",
header: "#",
cell: ({ row }) => `#${row.getValue("id")}`,
},
{
accessorKey: "name",
header: "标品名称",
},
{
accessorKey: "tags",
header: "标签",
},
{
id: "actions",
header: "", // 不需要标题
},
];
function getRowItems(row: Row<Item>): DropdownMenuItem[] {
return [
{ type: "label", label: "操作" },
{
label: "编辑",
color: "primary",
icon: "lucide:pencil-line",
onSelect() {
itemEditModal.value?.openModal(row.original);
toast.add({
title: `编辑 ${row.original.name}`,
description: "已打开编辑模态框",
color: "warning",
icon: "lucide:pencil-line",
});
},
},
{ type: "separator" },
{
label: "删除",
color: "error",
icon: "lucide:trash-2",
onSelect() {
deleteModal.selectedItem = row.original;
deleteModal.open = true;
toast.add({
title: `Deleting ${row.original.name}...`,
color: "error",
icon: "lucide:trash-2",
});
},
},
] as const;
}
// Define Shortcut
defineShortcuts({
meta_i: () => {
console.log("Shortcut works!");
},
});
</script>
<style></style>