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";
|
import { useLocalStorage, createSharedComposable } from "@vueuse/core";
|
||||||
|
|
||||||
// 类型定义
|
|
||||||
export type Item = {
|
export type Item = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -12,9 +11,10 @@ export type Item = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const _useItems = () => {
|
const _useItems = () => {
|
||||||
const items = useLocalStorage<Item[]>("item-database", [], {
|
const stored = useLocalStorage<Item[]>("item-database", [], {
|
||||||
serializer: {
|
serializer: {
|
||||||
read: (v) => {
|
read: (v) => {
|
||||||
|
if (!v) return [];
|
||||||
const parsed = JSON.parse(v);
|
const parsed = JSON.parse(v);
|
||||||
return parsed.map((item: any) => ({
|
return parsed.map((item: any) => ({
|
||||||
...item,
|
...item,
|
||||||
@@ -26,34 +26,37 @@ const _useItems = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 直接使用 stored(Ref<Item[]>)。模板会自动解包,其他逻辑也简单。
|
||||||
|
const items = stored; // Ref<Item[]>
|
||||||
|
|
||||||
|
// 统计项使用 items.value
|
||||||
const stats = {
|
const stats = {
|
||||||
totalItems: computed(() => items.value.length),
|
totalItems: computed(() => (items.value ?? []).length),
|
||||||
addedThisMonth: computed(() => {
|
addedThisMonth: computed(() => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const thisMonth = now.getMonth();
|
const thisMonth = now.getMonth();
|
||||||
const thisYear = now.getFullYear();
|
const thisYear = now.getFullYear();
|
||||||
|
return (items.value ?? []).filter(
|
||||||
let count = 0;
|
(item) =>
|
||||||
for (const item of items.value) {
|
item.createdAt.getMonth() === thisMonth &&
|
||||||
const date = item.createdAt;
|
item.createdAt.getFullYear() === thisYear
|
||||||
if (date.getMonth() === thisMonth && date.getFullYear() === thisYear) {
|
).length;
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}),
|
}),
|
||||||
latestId: computed(
|
latestId: computed(
|
||||||
() => (items.value[items.value.length - 1]?.id ?? 0) + 1
|
() => (items.value?.[items.value.length - 1]?.id ?? 0) + 1
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const isNameAvailable = (name: string) =>
|
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) =>
|
const removeItem = (id: number) => {
|
||||||
(items.value = items.value.filter((i) => i.id !== id));
|
items.value = (items.value ?? []).filter((i) => i.id !== id); // <-- 替换数组引用
|
||||||
|
};
|
||||||
|
|
||||||
return { items, stats, isNameAvailable, addItem, removeItem };
|
return { items, stats, isNameAvailable, addItem, removeItem };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const items = computed<NavigationMenuItem[]>(() => [
|
|||||||
{
|
{
|
||||||
label: "物品管理",
|
label: "物品管理",
|
||||||
to: "/",
|
to: "/",
|
||||||
icon: "marketeq:home-alt-3",
|
icon: "marketeq:box",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "导出配置",
|
label: "导出配置",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<UPageHero
|
<!-- <UPageHero
|
||||||
title="智能物品管理系统"
|
title="智能物品管理系统"
|
||||||
description="专业的物品数据库管理工具,支持图片记录、CSV导出和历史价格追踪"
|
description="专业的物品数据库管理工具,支持图片记录、CSV导出和历史价格追踪"
|
||||||
headline="New release"
|
headline="New release"
|
||||||
/>
|
/> -->
|
||||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<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 class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ItemAddModal />
|
<ItemEditModal />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -97,6 +97,11 @@
|
|||||||
</template>
|
</template>
|
||||||
</UTable>
|
</UTable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ItemDeleteModal
|
||||||
|
v-model:open="deleteModal.open"
|
||||||
|
:item="deleteModal.selectedItem"
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -104,15 +109,23 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { TableColumn, DropdownMenuItem } from "@nuxt/ui";
|
import type { TableColumn, DropdownMenuItem } from "@nuxt/ui";
|
||||||
import type { Row } from "@tanstack/vue-table";
|
import type { Row } from "@tanstack/vue-table";
|
||||||
import { useClipboard } from "@vueuse/core";
|
|
||||||
|
|
||||||
const UBadge = resolveComponent("UBadge");
|
const UBadge = resolveComponent("UBadge");
|
||||||
const UAvatar = resolveComponent("UAvatar");
|
const UAvatar = resolveComponent("UAvatar");
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { copy } = useClipboard();
|
|
||||||
|
|
||||||
const { items, stats } = useItemsStore();
|
const { items, stats } = useItemsStore();
|
||||||
|
|
||||||
|
const deleteModal = reactive<{
|
||||||
|
open: boolean;
|
||||||
|
selectedItem: Item | null;
|
||||||
|
}>({
|
||||||
|
open: false,
|
||||||
|
selectedItem: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const globalFilter = ref<string>("");
|
||||||
|
|
||||||
// 统计
|
// 统计
|
||||||
const statistics = [
|
const statistics = [
|
||||||
{
|
{
|
||||||
@@ -141,8 +154,6 @@ const statistics = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const globalFilter = ref<string>("");
|
|
||||||
|
|
||||||
const itemColumns: TableColumn<Item>[] = [
|
const itemColumns: TableColumn<Item>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "id",
|
accessorKey: "id",
|
||||||
@@ -171,16 +182,28 @@ function getRowItems(row: Row<Item>): DropdownMenuItem[] {
|
|||||||
color: "primary",
|
color: "primary",
|
||||||
icon: "lucide:pencil-line",
|
icon: "lucide:pencil-line",
|
||||||
onSelect() {
|
onSelect() {
|
||||||
copy(row.original.id.toString());
|
|
||||||
toast.add({
|
toast.add({
|
||||||
title: "Payment ID copied to clipboard!",
|
title: `Editing ${row.original.name}...`,
|
||||||
color: "success",
|
color: "warning",
|
||||||
icon: "i-lucide-circle-check",
|
icon: "lucide:pencil-line",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ type: "separator" },
|
{ 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;
|
] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user