Files
bid-setup.tootaio.com/app/components/itemManage/Table.vue
xiaomai 3909f614d2 feat(ui): implement dashboard layout and refactor pages
This commit replaces the previous simple layout with a comprehensive dashboard interface using Nuxt UI Pro components.

- Introduced a new default layout with `UDashboardSidebar` and `UDashboardPanel`.
- Refactored the main page (`index.vue`) by splitting it into `ItemManageStatistics` and `ItemManageTable` components.
- Added new computed stats for monthly growth and total images, and displayed them in new statistic cards.
- Reorganized components from `item/` to a new `itemManage/` directory.
- Added custom primary and secondary theme colors.
2025-10-21 14:16:27 +08:00

216 lines
5.8 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 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>
<ItemManageEditModal ref="itemEditModal" />
</div>
</div>
<!-- UTable 一个 keytableKey用于在需要时强制重挂载 -->
<UTable
:key="tableKey"
sticky
v-model:global-filter="globalFilter"
:data="itemsList"
:columns="itemColumns"
class="max-h-[640px]"
>
<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">
<strong>{{ row.original.brand }}</strong>
{{ 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>
<ItemManageDeleteModal
v-model:open="deleteModal.open"
:item="deleteModal.selectedItem"
/>
</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 } = 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 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")}`,
},
// 新增一个 brand 列 —— 让它参与全局搜索但不实际渲染内容brand 已在 name-cell 中显示)
{
accessorKey: "brand",
header: "", // 留空,避免影响布局
// 明确允许全局过滤(通常是默认 true但写上更明确
enableGlobalFilter: true,
// 不在表格中重复显示 brand因为你在 name-cell 已经显示了)
cell: () => null,
},
{
accessorKey: "name",
header: "标品名称",
},
{
accessorKey: "tags",
header: "标签",
accessorFn: (row: Item) => (row.tags ?? []).join(" "), // <-- 关键:把数组转成字符串
enableGlobalFilter: true,
// 注意:你仍然使用 template #tags-cell 来渲染标签外观badgeslot 会优先显示
},
{
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;
}
</script>
<style></style>