feat(item): add delete functionality
This commit introduces the ability to delete items. A new `ItemDeleteModal` component provides a confirmation step before removal. - Adds a 'Delete' option to the action menu in the items table. - Refactors the `useItems` composable to ensure reactivity with `useLocalStorage` by creating new array references on add/remove operations. - Renames `AddModal` to `EditModal` to better align with its future role in both adding and editing items.
This commit is contained in:
53
app/components/item/DeleteModal.vue
Normal file
53
app/components/item/DeleteModal.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<UModal
|
||||
v-model:open="open"
|
||||
:title="`删除 ${item?.name}?`"
|
||||
:description="`确定吗?此操作不可被还原。`"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<template #body>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton
|
||||
label="Cancel"
|
||||
color="neutral"
|
||||
variant="subtle"
|
||||
@click="open = false"
|
||||
/>
|
||||
<UButton
|
||||
label="Delete"
|
||||
color="error"
|
||||
variant="solid"
|
||||
loading-auto
|
||||
@click="onSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
item?: Item | null;
|
||||
}>(),
|
||||
{
|
||||
item: null,
|
||||
}
|
||||
);
|
||||
|
||||
const { removeItem } = useItemsStore();
|
||||
|
||||
const open = defineModel<boolean>("open", { default: false });
|
||||
|
||||
async function onSubmit() {
|
||||
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
if (!props.item || props.item.id == null) {
|
||||
return;
|
||||
}
|
||||
removeItem(props.item.id);
|
||||
open.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useLocalStorage, createSharedComposable } from "@vueuse/core";
|
||||
|
||||
// 类型定义
|
||||
export type Item = {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -12,9 +11,10 @@ export type Item = {
|
||||
};
|
||||
|
||||
const _useItems = () => {
|
||||
const items = useLocalStorage<Item[]>("item-database", [], {
|
||||
const stored = useLocalStorage<Item[]>("item-database", [], {
|
||||
serializer: {
|
||||
read: (v) => {
|
||||
if (!v) return [];
|
||||
const parsed = JSON.parse(v);
|
||||
return parsed.map((item: any) => ({
|
||||
...item,
|
||||
@@ -26,34 +26,37 @@ const _useItems = () => {
|
||||
},
|
||||
});
|
||||
|
||||
// 直接使用 stored(Ref<Item[]>)。模板会自动解包,其他逻辑也简单。
|
||||
const items = stored; // Ref<Item[]>
|
||||
|
||||
// 统计项使用 items.value
|
||||
const stats = {
|
||||
totalItems: computed(() => items.value.length),
|
||||
totalItems: computed(() => (items.value ?? []).length),
|
||||
addedThisMonth: computed(() => {
|
||||
const now = new Date();
|
||||
const thisMonth = now.getMonth();
|
||||
const thisYear = now.getFullYear();
|
||||
|
||||
let count = 0;
|
||||
for (const item of items.value) {
|
||||
const date = item.createdAt;
|
||||
if (date.getMonth() === thisMonth && date.getFullYear() === thisYear) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
return (items.value ?? []).filter(
|
||||
(item) =>
|
||||
item.createdAt.getMonth() === thisMonth &&
|
||||
item.createdAt.getFullYear() === thisYear
|
||||
).length;
|
||||
}),
|
||||
latestId: computed(
|
||||
() => (items.value[items.value.length - 1]?.id ?? 0) + 1
|
||||
() => (items.value?.[items.value.length - 1]?.id ?? 0) + 1
|
||||
),
|
||||
};
|
||||
|
||||
const isNameAvailable = (name: string) =>
|
||||
!items.value.some((item) => item.name === name);
|
||||
!(items.value ?? []).some((item) => item.name === name);
|
||||
|
||||
const addItem = (item: Item) => items.value.push(item);
|
||||
const addItem = (item: Item) => {
|
||||
items.value = [...(items.value ?? []), item]; // <-- 替换数组引用
|
||||
};
|
||||
|
||||
const removeItem = (id: number) =>
|
||||
(items.value = items.value.filter((i) => i.id !== id));
|
||||
const removeItem = (id: number) => {
|
||||
items.value = (items.value ?? []).filter((i) => i.id !== id); // <-- 替换数组引用
|
||||
};
|
||||
|
||||
return { items, stats, isNameAvailable, addItem, removeItem };
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ const items = computed<NavigationMenuItem[]>(() => [
|
||||
{
|
||||
label: "物品管理",
|
||||
to: "/",
|
||||
icon: "marketeq:home-alt-3",
|
||||
icon: "marketeq:box",
|
||||
},
|
||||
{
|
||||
label: "导出配置",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<UPageHero
|
||||
<!-- <UPageHero
|
||||
title="智能物品管理系统"
|
||||
description="专业的物品数据库管理工具,支持图片记录、CSV导出和历史价格追踪"
|
||||
headline="New release"
|
||||
/>
|
||||
/> -->
|
||||
<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">
|
||||
@@ -46,7 +46,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ItemAddModal />
|
||||
<ItemEditModal />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -97,6 +97,11 @@
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
|
||||
<ItemDeleteModal
|
||||
v-model:open="deleteModal.open"
|
||||
:item="deleteModal.selectedItem"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -104,15 +109,23 @@
|
||||
<script lang="ts" setup>
|
||||
import type { TableColumn, DropdownMenuItem } from "@nuxt/ui";
|
||||
import type { Row } from "@tanstack/vue-table";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
const UBadge = resolveComponent("UBadge");
|
||||
const UAvatar = resolveComponent("UAvatar");
|
||||
const toast = useToast();
|
||||
const { copy } = useClipboard();
|
||||
|
||||
const { items, stats } = useItemsStore();
|
||||
|
||||
const deleteModal = reactive<{
|
||||
open: boolean;
|
||||
selectedItem: Item | null;
|
||||
}>({
|
||||
open: false,
|
||||
selectedItem: null,
|
||||
});
|
||||
|
||||
const globalFilter = ref<string>("");
|
||||
|
||||
// 统计
|
||||
const statistics = [
|
||||
{
|
||||
@@ -141,8 +154,6 @@ const statistics = [
|
||||
},
|
||||
];
|
||||
|
||||
const globalFilter = ref<string>("");
|
||||
|
||||
const itemColumns: TableColumn<Item>[] = [
|
||||
{
|
||||
accessorKey: "id",
|
||||
@@ -171,16 +182,28 @@ function getRowItems(row: Row<Item>): DropdownMenuItem[] {
|
||||
color: "primary",
|
||||
icon: "lucide:pencil-line",
|
||||
onSelect() {
|
||||
copy(row.original.id.toString());
|
||||
toast.add({
|
||||
title: "Payment ID copied to clipboard!",
|
||||
color: "success",
|
||||
icon: "i-lucide-circle-check",
|
||||
title: `Editing ${row.original.name}...`,
|
||||
color: "warning",
|
||||
icon: "lucide:pencil-line",
|
||||
});
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ label: "删除", color: "error", icon: "lucide:trash-2" },
|
||||
{
|
||||
label: "删除",
|
||||
color: "error",
|
||||
icon: "lucide:trash-2",
|
||||
onSelect() {
|
||||
deleteModal.selectedItem = row.original;
|
||||
deleteModal.open = true; // 👈 打开 Modal!
|
||||
toast.add({
|
||||
title: `Deleting ${row.original.name}...`,
|
||||
color: "error",
|
||||
icon: "lucide:trash-2",
|
||||
});
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user