From 3556d5e8c1cdb12d5098597ffdc1aaebf863d5e3 Mon Sep 17 00:00:00 2001 From: ZhangAo Date: Fri, 3 Jul 2026 17:32:11 +0800 Subject: [PATCH] 20260703173210 --- code/app/CLOTHING_OUTFIT_DESIGN.md | 5 + code/app/app-go/db/clothing.sql | 31 +- code/app/app-go/db/clothing_add_owner.sql | 12 + code/app/app-go/db/clothing_merge_tag.sql | 37 ++ .../src/app/clothes/(tabs)/home/index.tsx | 11 +- .../src/app/clothes/(tabs)/manage/index.tsx | 29 +- .../src/app/clothes/(tabs)/outfit/index.tsx | 17 +- .../src/app/clothes/detail/[id]/index.tsx | 16 +- .../app/app-rn/src/app/clothes/edit/index.tsx | 85 ++- .../app/clothes/outfit/detail/[id]/index.tsx | 15 +- .../src/app/clothes/outfit/edit/index.tsx | 86 ++- .../components/clothing/ClothesGridItem.tsx | 10 +- code/app/app-rn/src/store/clothing.ts | 89 ++- code/app/app-rn/src/types/clothing.ts | 17 +- code/app/app-rn/test.html | 536 ++++++++++++++++++ 15 files changed, 853 insertions(+), 143 deletions(-) create mode 100644 code/app/app-go/db/clothing_add_owner.sql create mode 100644 code/app/app-go/db/clothing_merge_tag.sql create mode 100644 code/app/app-rn/test.html diff --git a/code/app/CLOTHING_OUTFIT_DESIGN.md b/code/app/CLOTHING_OUTFIT_DESIGN.md index a07f126d..7942ae13 100644 --- a/code/app/CLOTHING_OUTFIT_DESIGN.md +++ b/code/app/CLOTHING_OUTFIT_DESIGN.md @@ -80,6 +80,7 @@ - 数据保存优先使用现有公共接口,例如 `data/loadData`、`data/saveData`。 - 尽量都走通用接口;只有通用接口明显无法保证体验或一致性时,再补特殊接口。 - 能在前端完成的能力优先放前端,减少后端特殊接口。 +- 衣服和搭配支持「归属者」字段(`owner_id`,引用 `b_family_member.user_id`)。归属者仅作为标记,让家人知道这件衣服/搭配是谁的;数据仍为家庭共享,不做个人隔离,也不按归属者过滤可见性。 - 删除搭配时,只删除搭配和搭配明细,不删除衣服。 - 删除衣服时,如果衣服没有被搭配引用,直接删除或软删除该衣服。 - 删除衣服时,如果衣服已被搭配引用,需要提示用户是否同时删除相关搭配: @@ -135,6 +136,7 @@ - name - image_file_key - category_id +- owner_id - brand - remark - status @@ -150,6 +152,7 @@ - `image_file_key`:走现有 S3 规则,上传到 `family_id/年/月/日/...`。 - `image_file_key` 必填。 - `status`:建议 1 启用,0 停用。 +- `owner_id`:归属者,引用 `b_family_member.user_id`,可空。仅作为标记,让家人知道这件衣服是谁的,**不改变家庭共享语义,不做个人衣橱隔离**。 - 颜色、季节这类检索信息不做结构化字段,由标签承载。 ### 搭配 b_clothing_outfit @@ -162,6 +165,7 @@ - cover_file_key - layout_json - category_id +- owner_id - remark - status - sort_number @@ -177,6 +181,7 @@ - 缩略图由前端生成后通过 S3 上传,保存时写入 `cover_file_key`。 - 如果缩略图生成失败,可以降级使用搭配内第一件衣服图片作为封面。 - `layout_json` 保存画布级配置,例如画布宽高比例、背景、版本号等。 +- `owner_id`:归属者,引用 `b_family_member.user_id`,可空。仅作为标记,让家人知道这套搭配是谁的,**不改变家庭共享语义,不做个人衣橱隔离**。 ### 搭配明细 b_clothing_outfit_clothes diff --git a/code/app/app-go/db/clothing.sql b/code/app/app-go/db/clothing.sql index f5d239b2..e90fe771 100644 --- a/code/app/app-go/db/clothing.sql +++ b/code/app/app-go/db/clothing.sql @@ -1,7 +1,6 @@ drop table if exists b_clothing_tag_relation; drop table if exists b_clothing_outfit_clothes; drop table if exists b_clothing_tag; -drop table if exists b_clothing_tag_group; drop table if exists b_clothing_category; drop table if exists b_clothing_outfit; drop table if exists b_clothing_clothes; @@ -12,6 +11,7 @@ create table b_clothing_clothes ( name varchar(64) not null, image_file_key varchar(512) not null, category_id bigint, + owner_id bigint, brand varchar(64) default '', remark varchar(512) default '', sort_number int not null default 0, @@ -29,6 +29,7 @@ create table b_clothing_outfit ( cover_file_key varchar(512) default '', layout_json jsonb, category_id bigint, + owner_id bigint, remark varchar(512) default '', sort_number int not null default 0, status smallint not null default 1, @@ -68,23 +69,10 @@ create table b_clothing_category ( update_by bigint ); -create table b_clothing_tag_group ( - id bigint primary key, - family_id bigint not null, - name varchar(32) not null, - type smallint not null, - sort_number int not null default 0, - status smallint not null default 1, - create_time timestamp, - update_time timestamp, - create_by bigint, - update_by bigint -); - create table b_clothing_tag ( id bigint primary key, family_id bigint not null, - tag_group_id bigint not null, + parent_id bigint not null default 0, name varchar(32) not null, type smallint not null, color varchar(7) default '', @@ -111,9 +99,15 @@ create table b_clothing_tag_relation ( create index idx_b_clothing_clothes_family_status_category on b_clothing_clothes (family_id, status, category_id, sort_number, id); +create index idx_b_clothing_clothes_family_owner + on b_clothing_clothes (family_id, owner_id, id); + create index idx_b_clothing_outfit_family_status_category on b_clothing_outfit (family_id, status, category_id, sort_number, id); +create index idx_b_clothing_outfit_family_owner + on b_clothing_outfit (family_id, owner_id, id); + create index idx_b_clothing_outfit_clothes_outfit on b_clothing_outfit_clothes (outfit_id, z_index, sort_number, id); @@ -123,11 +117,8 @@ create index idx_b_clothing_outfit_clothes_clothes create index idx_b_clothing_category_family_type on b_clothing_category (family_id, status, type, sort_number, id); -create index idx_b_clothing_tag_group_family_type - on b_clothing_tag_group (family_id, status, type, sort_number, id); - -create index idx_b_clothing_tag_family_group - on b_clothing_tag (family_id, status, type, tag_group_id, sort_number, id); +create index idx_b_clothing_tag_family_parent + on b_clothing_tag (family_id, status, type, parent_id, sort_number, id); create index idx_b_clothing_tag_relation_target on b_clothing_tag_relation (family_id, target_type, target_id, tag_id); diff --git a/code/app/app-go/db/clothing_add_owner.sql b/code/app/app-go/db/clothing_add_owner.sql new file mode 100644 index 00000000..3addba39 --- /dev/null +++ b/code/app/app-go/db/clothing_add_owner.sql @@ -0,0 +1,12 @@ +-- 增量迁移:给衣服和搭配表添加归属者字段 +-- 说明:owner_id 引用 b_family_member.user_id;可空;仅为归属标记, +-- 数据仍为家庭共享,不做隔离。 + +alter table b_clothing_clothes add column if not exists owner_id bigint; +alter table b_clothing_outfit add column if not exists owner_id bigint; + +create index if not exists idx_b_clothing_clothes_family_owner + on b_clothing_clothes (family_id, owner_id, id); + +create index if not exists idx_b_clothing_outfit_family_owner + on b_clothing_outfit (family_id, owner_id, id); diff --git a/code/app/app-go/db/clothing_merge_tag.sql b/code/app/app-go/db/clothing_merge_tag.sql new file mode 100644 index 00000000..10d8bb82 --- /dev/null +++ b/code/app/app-go/db/clothing_merge_tag.sql @@ -0,0 +1,37 @@ +-- 增量迁移:合并 b_clothing_tag_group 与 b_clothing_tag 为单表(加 parent_id) +-- 说明:parent_id = 0 表示标签组(父行);parent_id != 0 表示标签(子行), +-- 指向所属组的 id。type(0=衣服/1=搭配)保留在所有行上。 +-- 应用方式:go run ./cmd/apply_sql app-go/db/clothing_merge_tag.sql + +-- 1. 新增 parent_id 列 +alter table b_clothing_tag add column if not exists parent_id bigint not null default 0; + +-- 2. 把旧 tag_group_id 搬到 parent_id(仅当列仍存在时执行) +do $$ +begin + if exists ( + select 1 from information_schema.columns + where table_name = 'b_clothing_tag' and column_name = 'tag_group_id' + ) then + update b_clothing_tag set parent_id = tag_group_id where parent_id = 0; + end if; +end $$; + +-- 3. 把组数据搬进 b_clothing_tag(parent_id = 0) +insert into b_clothing_tag + (id, family_id, name, type, color, parent_id, sort_number, status, + create_time, update_time, create_by, update_by) +select id, family_id, name, type, '', 0, sort_number, status, + create_time, update_time, create_by, update_by +from b_clothing_tag_group +on conflict (id) do nothing; + +-- 4. 清理旧结构 +alter table b_clothing_tag drop column if exists tag_group_id; +drop index if exists idx_b_clothing_tag_family_group; +drop index if exists idx_b_clothing_tag_group_family_type; +drop table if exists b_clothing_tag_group; + +-- 5. 新索引 +create index if not exists idx_b_clothing_tag_family_parent + on b_clothing_tag (family_id, status, type, parent_id, sort_number, id); diff --git a/code/app/app-rn/src/app/clothes/(tabs)/home/index.tsx b/code/app/app-rn/src/app/clothes/(tabs)/home/index.tsx index c5e91fa7..395e2a93 100644 --- a/code/app/app-rn/src/app/clothes/(tabs)/home/index.tsx +++ b/code/app/app-rn/src/app/clothes/(tabs)/home/index.tsx @@ -31,9 +31,10 @@ export default function ClothingHome() { const fetchOutfits = useClothingStore((s) => s.fetchOutfits); const fetchOutfitClothes = useClothingStore((s) => s.fetchOutfitClothes); const fetchCategories = useClothingStore((s) => s.fetchCategories); - const fetchTagGroups = useClothingStore((s) => s.fetchTagGroups); const fetchTags = useClothingStore((s) => s.fetchTags); const fetchTagRelations = useClothingStore((s) => s.fetchTagRelations); + const fetchMembers = useClothingStore((s) => s.fetchMembers); + const getOwnerName = useClothingStore((s) => s.getOwnerName); const deleteClothesBundle = useClothingStore((s) => s.deleteClothesBundle); const { loading, refreshing, runInitial, runRefresh } = useListLoading(); const { isLoading, startLoading, stopLoading } = useLoading(); @@ -72,8 +73,8 @@ export default function ClothingHome() { const loadBaseData = useCallback(async () => { if (!familyId) return; - await Promise.all([fetchClothes(familyId), fetchOutfits(familyId), fetchOutfitClothes(familyId), fetchCategories(familyId, ClothingTargetType.Clothes), fetchTagGroups(familyId, ClothingTargetType.Clothes), fetchTags(familyId, ClothingTargetType.Clothes), fetchTagRelations(familyId)]); - }, [familyId, fetchCategories, fetchClothes, fetchOutfitClothes, fetchOutfits, fetchTagGroups, fetchTagRelations, fetchTags]); + await Promise.all([fetchClothes(familyId), fetchOutfits(familyId), fetchOutfitClothes(familyId), fetchCategories(familyId, ClothingTargetType.Clothes), fetchTags(familyId, ClothingTargetType.Clothes), fetchTagRelations(familyId), fetchMembers(familyId)]); + }, [familyId, fetchCategories, fetchClothes, fetchOutfitClothes, fetchOutfits, fetchTagRelations, fetchTags, fetchMembers]); useEffect(() => { if (familyId) runInitial(loadBaseData); }, [familyId]); useFocusEffect(useCallback(() => { if (familyId) loadBaseData(); }, [familyId, loadBaseData])); @@ -149,9 +150,9 @@ export default function ClothingHome() { { setActiveItem(null); router.push(`/clothes/detail/${item.id}` as any); }} onLongPress={(e) => { setMenuPos({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY }); setActiveItem(item); }}> - + - ), [renderCategoryName]); + ), [getOwnerName, renderCategoryName]); return ( diff --git a/code/app/app-rn/src/app/clothes/(tabs)/manage/index.tsx b/code/app/app-rn/src/app/clothes/(tabs)/manage/index.tsx index cb897729..edfaf7e4 100644 --- a/code/app/app-rn/src/app/clothes/(tabs)/manage/index.tsx +++ b/code/app/app-rn/src/app/clothes/(tabs)/manage/index.tsx @@ -7,27 +7,24 @@ import LayoutView from "@/components/layout/view"; import { useListLoading, useLoading } from "@/hooks/useLoading"; import { LoadingOverlay } from "@/components/LoadingOverlay"; import { useClothingStore, useFamilyStore } from "@/store"; -import { ClothingCategory, ClothingTag, ClothingTagGroup, ClothingTargetType } from "@/types/clothing"; +import { ClothingCategory, ClothingTag, ClothingTargetType } from "@/types/clothing"; const COLORS = ["#EF4444","#F97316","#F59E0B","#84CC16","#22C55E","#14B8A6","#06B6D4","#3B82F6","#6366F1","#8B5CF6","#A855F7","#D946EF","#EC4899","#78716C"]; export default function ClothingManage() { const familyId = useFamilyStore((s) => s.getFamilyId()); const categories = useClothingStore((s) => s.categories); - const tagGroups = useClothingStore((s) => s.tagGroups); const tags = useClothingStore((s) => s.tags); const fetchCategories = useClothingStore((s) => s.fetchCategories); - const fetchTagGroups = useClothingStore((s) => s.fetchTagGroups); const fetchTags = useClothingStore((s) => s.fetchTags); const saveCategoryChanges = useClothingStore((s) => s.saveCategoryChanges); - const saveTagGroupChanges = useClothingStore((s) => s.saveTagGroupChanges); const saveTagChanges = useClothingStore((s) => s.saveTagChanges); const { refreshing, runInitial, runRefresh } = useListLoading(); const { isLoading, startLoading, stopLoading } = useLoading(); const [tab, setTab] = useState(ClothingTargetType.Clothes); const curCategories = useMemo(() => categories.filter((c) => c.type === tab), [categories, tab]); - const curTagGroups = useMemo(() => tagGroups.filter((g) => g.type === tab), [tagGroups, tab]); + const curTagGroups = useMemo(() => tags.filter((t) => t.parent_id === 0 && t.type === tab), [tags, tab]); // 分类 const [catForm, setCatForm] = useState(""); @@ -37,9 +34,9 @@ export default function ClothingManage() { // 标签分组 const [groupForm, setGroupForm] = useState(""); - const [editingGroup, setEditingGroup] = useState(null); + const [editingGroup, setEditingGroup] = useState(null); const [showGroupSheet, setShowGroupSheet] = useState(false); - const [groupDelete, setGroupDelete] = useState(null); + const [groupDelete, setGroupDelete] = useState(null); // 标签 const [tagForm, setTagForm] = useState({ name: "", color: "" }); @@ -50,8 +47,8 @@ export default function ClothingManage() { const loadData = useCallback(async () => { if (!familyId) return; - await Promise.all([fetchCategories(familyId), fetchTagGroups(familyId), fetchTags(familyId)]); - }, [familyId, fetchCategories, fetchTagGroups, fetchTags]); + await Promise.all([fetchCategories(familyId), fetchTags(familyId)]); + }, [familyId, fetchCategories, fetchTags]); useEffect(() => { if (familyId) runInitial(loadData); }, [familyId]); // 分类 @@ -68,24 +65,24 @@ export default function ClothingManage() { }, [catDelete, loadData, saveCategoryChanges, startLoading, stopLoading]); // 标签分组 - const openGroup = useCallback((g?: ClothingTagGroup) => { setGroupForm(g?.name ?? ""); setEditingGroup(g ?? null); setShowGroupSheet(true); }, []); + const openGroup = useCallback((g?: ClothingTag) => { setGroupForm(g?.name ?? ""); setEditingGroup(g ?? null); setShowGroupSheet(true); }, []); const saveGroup = useCallback(async () => { if (!familyId || !groupForm.trim()) return; startLoading("保存中..."); try { - editingGroup ? await saveTagGroupChanges({ updates: [{ id: editingGroup.id, name: groupForm.trim() }] }) : await saveTagGroupChanges({ inserts: [{ name: groupForm.trim(), type: tab, family_id: familyId }] }); + editingGroup ? await saveTagChanges({ updates: [{ id: editingGroup.id, name: groupForm.trim() }] }) : await saveTagChanges({ inserts: [{ name: groupForm.trim(), type: tab, family_id: familyId, parent_id: 0, color: "" }] }); setShowGroupSheet(false); await loadData(); stopLoading({ type: "success", message: "已保存" }); } catch { stopLoading({ type: "error", message: "保存失败" }); } - }, [editingGroup, familyId, groupForm, loadData, saveTagGroupChanges, startLoading, stopLoading, tab]); + }, [editingGroup, familyId, groupForm, loadData, saveTagChanges, startLoading, stopLoading, tab]); const delGroup = useCallback(async () => { if (!groupDelete) return; startLoading("删除中..."); - try { await saveTagChanges({ deletes: tags.filter((t) => t.tag_group_id === groupDelete.id).map((t) => ({ id: t.id })) }); await saveTagGroupChanges({ deletes: [{ id: groupDelete.id }] }); setGroupDelete(null); await loadData(); stopLoading({ type: "success", message: "已删除" }); } catch { stopLoading({ type: "error", message: "删除失败" }); } - }, [groupDelete, loadData, saveTagChanges, saveTagGroupChanges, startLoading, stopLoading, tags]); + try { const childIds = tags.filter((t) => t.parent_id === groupDelete.id).map((t) => ({ id: t.id })); await saveTagChanges({ deletes: [{ id: groupDelete.id }, ...childIds] }); setGroupDelete(null); await loadData(); stopLoading({ type: "success", message: "已删除" }); } catch { stopLoading({ type: "error", message: "删除失败" }); } + }, [groupDelete, loadData, saveTagChanges, startLoading, stopLoading, tags]); // 标签 const openTag = useCallback((groupId: number, t?: ClothingTag) => { setActiveGroupId(groupId); setTagForm({ name: t?.name ?? "", color: t?.color ?? "" }); setEditingTag(t ?? null); setShowTagSheet(true); }, []); const saveTag = useCallback(async () => { if (!familyId || !activeGroupId || !tagForm.name.trim()) return; startLoading("保存中..."); try { - editingTag ? await saveTagChanges({ updates: [{ id: editingTag.id, name: tagForm.name.trim(), color: tagForm.color }] }) : await saveTagChanges({ inserts: [{ name: tagForm.name.trim(), color: tagForm.color, tag_group_id: activeGroupId, type: tab, family_id: familyId }] }); + editingTag ? await saveTagChanges({ updates: [{ id: editingTag.id, name: tagForm.name.trim(), color: tagForm.color }] }) : await saveTagChanges({ inserts: [{ name: tagForm.name.trim(), color: tagForm.color, parent_id: activeGroupId, type: tab, family_id: familyId }] }); setShowTagSheet(false); await loadData(); stopLoading({ type: "success", message: "已保存" }); } catch { stopLoading({ type: "error", message: "保存失败" }); } }, [activeGroupId, editingTag, familyId, loadData, saveTagChanges, startLoading, stopLoading, tab, tagForm]); @@ -93,7 +90,7 @@ export default function ClothingManage() { try { await saveTagChanges({ deletes: [{ id: tagDelete.id }] }); setTagDelete(null); await loadData(); stopLoading({ type: "success", message: "已删除" }); } catch { stopLoading({ type: "error", message: "删除失败" }); } }, [loadData, saveTagChanges, startLoading, stopLoading, tagDelete]); - const getTagsByGroup = useCallback((groupId: number) => tags.filter((t) => t.tag_group_id === groupId), [tags]); + const getTagsByGroup = useCallback((groupId: number) => tags.filter((t) => t.parent_id === groupId), [tags]); return ( diff --git a/code/app/app-rn/src/app/clothes/(tabs)/outfit/index.tsx b/code/app/app-rn/src/app/clothes/(tabs)/outfit/index.tsx index e63bba31..a0881f15 100644 --- a/code/app/app-rn/src/app/clothes/(tabs)/outfit/index.tsx +++ b/code/app/app-rn/src/app/clothes/(tabs)/outfit/index.tsx @@ -4,7 +4,7 @@ import { router, useFocusEffect } from "expo-router"; import { Button, Dialog, Label, TextField } from "heroui-native"; import BottomSheet from "@/components/BottomSheet"; import ContextMenu from "@/components/ContextMenu"; -import { FilterIcon, PencilIcon, PlusIcon, RotateCcwIcon, Trash2Icon } from "lucide-react-native"; +import { FilterIcon, PencilIcon, PlusIcon, RotateCcwIcon, Trash2Icon, UserIcon } from "lucide-react-native"; import LayoutView from "@/components/layout/view"; import EmptyState from "@/components/EmptyStatus"; import ClothingImage from "@/components/clothing/ClothingImage"; @@ -28,6 +28,8 @@ export default function ClothingOutfitList() { const fetchTags = useClothingStore((s) => s.fetchTags); const fetchTagRelations = useClothingStore((s) => s.fetchTagRelations); const fetchClothes = useClothingStore((s) => s.fetchClothes); + const fetchMembers = useClothingStore((s) => s.fetchMembers); + const getOwnerName = useClothingStore((s) => s.getOwnerName); const saveOutfitBundle = useClothingStore((s) => s.saveOutfitBundle); const { loading, refreshing, runInitial, runRefresh } = useListLoading(); const { isLoading, startLoading, stopLoading } = useLoading(); @@ -46,8 +48,8 @@ export default function ClothingOutfitList() { const loadData = useCallback(async () => { if (!familyId) return; - await Promise.all([fetchOutfits(familyId), fetchClothes(familyId), fetchOutfitClothes(familyId), fetchCategories(familyId, ClothingTargetType.Outfit), fetchTags(familyId, ClothingTargetType.Outfit), fetchTagRelations(familyId, ClothingTargetType.Outfit)]); - }, [familyId, fetchClothes, fetchOutfitClothes, fetchOutfits, fetchCategories, fetchTags, fetchTagRelations]); + await Promise.all([fetchOutfits(familyId), fetchClothes(familyId), fetchOutfitClothes(familyId), fetchCategories(familyId, ClothingTargetType.Outfit), fetchTags(familyId, ClothingTargetType.Outfit), fetchTagRelations(familyId, ClothingTargetType.Outfit), fetchMembers(familyId)]); + }, [familyId, fetchClothes, fetchOutfitClothes, fetchOutfits, fetchCategories, fetchTags, fetchTagRelations, fetchMembers]); useEffect(() => { if (familyId) runInitial(loadData); }, [familyId]); useFocusEffect(useCallback(() => { if (familyId) loadData(); }, [familyId, loadData])); @@ -117,6 +119,7 @@ export default function ClothingOutfitList() { const renderGridItem = useCallback((item: any) => { const catName = item.category_id ? categoriesById.get(item.category_id)?.name : null; + const ownerName = getOwnerName(item.owner_id); return ( router.push(`/clothes/outfit/detail/${item.id}` as any)} @@ -127,13 +130,19 @@ export default function ClothingOutfitList() { {getClothesCount(item)} 件 + {!!ownerName && ( + + + {ownerName} + + )} {item.name} {catName && {catName}} ); - }, [categoriesById, getClothesCount, getCoverKey]); + }, [categoriesById, getClothesCount, getCoverKey, getOwnerName]); return ( diff --git a/code/app/app-rn/src/app/clothes/detail/[id]/index.tsx b/code/app/app-rn/src/app/clothes/detail/[id]/index.tsx index c4b5f711..b06b1a83 100644 --- a/code/app/app-rn/src/app/clothes/detail/[id]/index.tsx +++ b/code/app/app-rn/src/app/clothes/detail/[id]/index.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Text, View } from "react-native"; import { Stack, useFocusEffect, useLocalSearchParams, useRouter } from "expo-router"; import { Button, Dialog, useThemeColor } from "heroui-native"; -import { PencilIcon, RotateCcwIcon, Trash2Icon } from "lucide-react-native"; +import { PencilIcon, RotateCcwIcon, Trash2Icon, UserIcon } from "lucide-react-native"; import PageLayout from "@/layouts/PageLayout"; import LayoutView from "@/components/layout/view"; import ClothingImage from "@/components/clothing/ClothingImage"; @@ -28,9 +28,10 @@ export default function ClothingDetail() { const fetchOutfits = useClothingStore((s) => s.fetchOutfits); const fetchOutfitClothes = useClothingStore((s) => s.fetchOutfitClothes); const fetchCategories = useClothingStore((s) => s.fetchCategories); - const fetchTagGroups = useClothingStore((s) => s.fetchTagGroups); const fetchTags = useClothingStore((s) => s.fetchTags); const fetchTagRelations = useClothingStore((s) => s.fetchTagRelations); + const fetchMembers = useClothingStore((s) => s.fetchMembers); + const getOwnerName = useClothingStore((s) => s.getOwnerName); const deleteClothesBundle = useClothingStore((s) => s.deleteClothesBundle); const { isLoading, startLoading, stopLoading } = useLoading(); const notifyModuleOperation = useModuleChatNotifier(); @@ -39,6 +40,7 @@ export default function ClothingDetail() { const item = useMemo(() => clothes.find((c) => c.id === clothesId) ?? null, [clothesId, clothes]); const categoryName = useMemo(() => { if (!item?.category_id) return "未分类"; return categories.find((c) => c.id === item.category_id)?.name ?? "未分类"; }, [categories, item?.category_id]); + const ownerName = useMemo(() => getOwnerName(item?.owner_id), [getOwnerName, item?.owner_id]); const selectedTags = useMemo(() => { const ids = new Set(tagRelations.filter((r) => r.target_type === ClothingTargetType.Clothes && r.target_id === clothesId).map((r) => r.tag_id)); return tags.filter((t) => ids.has(t.id)); @@ -46,9 +48,9 @@ export default function ClothingDetail() { const loadData = useCallback(async () => { if (!familyId || !clothesId) return; setPageLoading(true); - try { await Promise.all([fetchClothes(familyId), fetchOutfits(familyId), fetchOutfitClothes(familyId), fetchCategories(familyId, ClothingTargetType.Clothes), fetchTagGroups(familyId, ClothingTargetType.Clothes), fetchTags(familyId, ClothingTargetType.Clothes), fetchTagRelations(familyId, ClothingTargetType.Clothes, clothesId)]); } + try { await Promise.all([fetchClothes(familyId), fetchOutfits(familyId), fetchOutfitClothes(familyId), fetchCategories(familyId, ClothingTargetType.Clothes), fetchTags(familyId, ClothingTargetType.Clothes), fetchTagRelations(familyId, ClothingTargetType.Clothes, clothesId), fetchMembers(familyId)]); } finally { setPageLoading(false); } - }, [familyId, clothesId, fetchCategories, fetchClothes, fetchOutfitClothes, fetchOutfits, fetchTagGroups, fetchTagRelations, fetchTags]); + }, [familyId, clothesId, fetchCategories, fetchClothes, fetchOutfitClothes, fetchOutfits, fetchTagRelations, fetchTags, fetchMembers]); useEffect(() => { loadData(); }, [loadData]); useFocusEffect(useCallback(() => { if (familyId && clothesId) loadData(); }, [familyId, loadData, clothesId])); @@ -89,6 +91,12 @@ export default function ClothingDetail() { {/* 分类 + 标签数 - 横向排列,不套卡片 */} {categoryName} + {!!ownerName && ( + + + {ownerName} + + )} {selectedTags.length > 0 && ( {selectedTags.map((tag) => ( diff --git a/code/app/app-rn/src/app/clothes/edit/index.tsx b/code/app/app-rn/src/app/clothes/edit/index.tsx index 78c23717..7a90eaef 100644 --- a/code/app/app-rn/src/app/clothes/edit/index.tsx +++ b/code/app/app-rn/src/app/clothes/edit/index.tsx @@ -16,13 +16,12 @@ import { LoadingOverlay } from "@/components/LoadingOverlay"; import { useLoading } from "@/hooks/useLoading"; import { useModuleChatNotifier } from "@/hooks/useModuleChatNotifier"; import { getUniqueIdApi } from "@/request/api"; -import { useClothingStore, useFamilyStore } from "@/store"; +import { useClothingStore, useFamilyStore, useUserStore, type ClothingFamilyMember } from "@/store"; import { ClothingCategory, ClothingClothes, ClothingStatus, ClothingTag, - ClothingTagGroup, ClothingTargetType, } from "@/types/clothing"; import { deleteS3FileQuietly } from "@/utils/file"; @@ -31,6 +30,7 @@ interface ClothesFormState { name: string; imageFileKey: string; categoryId: number | null; + ownerId: number | null; brand: string; remark: string; sortNumber?: number; @@ -40,6 +40,7 @@ const emptyForm: ClothesFormState = { name: "", imageFileKey: "", categoryId: null, + ownerId: null, brand: "", remark: "", }; @@ -49,6 +50,7 @@ function createEditForm(item: ClothingClothes): ClothesFormState { name: item.name, imageFileKey: item.image_file_key, categoryId: item.category_id ?? null, + ownerId: item.owner_id ?? null, brand: item.brand ?? "", remark: item.remark ?? "", sortNumber: item.sort_number, @@ -65,16 +67,17 @@ export default function ClothingEdit() { const editId = id ? Number(id) : 0; const isEdit = editId > 0; const familyId = useFamilyStore((s) => s.getFamilyId()); + const currentUserId = useUserStore((s) => s.getUserId()); const clothes = useClothingStore((s) => s.clothes); const categories = useClothingStore((s) => s.categories); - const tagGroups = useClothingStore((s) => s.tagGroups); const tags = useClothingStore((s) => s.tags); const tagRelations = useClothingStore((s) => s.tagRelations); + const members = useClothingStore((s) => s.members); const fetchClothes = useClothingStore((s) => s.fetchClothes); const fetchCategories = useClothingStore((s) => s.fetchCategories); - const fetchTagGroups = useClothingStore((s) => s.fetchTagGroups); const fetchTags = useClothingStore((s) => s.fetchTags); const fetchTagRelations = useClothingStore((s) => s.fetchTagRelations); + const fetchMembers = useClothingStore((s) => s.fetchMembers); const saveClothesBundle = useClothingStore((s) => s.saveClothesBundle); const uploadClothesImage = useClothingStore((s) => s.uploadClothesImage); const { isLoading, startLoading, stopLoading } = useLoading(); @@ -91,21 +94,21 @@ export default function ClothingEdit() { ); const clothesTagGroups = useMemo( - () => tagGroups.filter((item) => item.type === ClothingTargetType.Clothes), - [tagGroups], + () => tags.filter((item) => item.type === ClothingTargetType.Clothes && item.parent_id === 0), + [tags], ); const clothesTags = useMemo( - () => tags.filter((item) => item.type === ClothingTargetType.Clothes), + () => tags.filter((item) => item.type === ClothingTargetType.Clothes && item.parent_id !== 0), [tags], ); const tagsByGroupId = useMemo(() => { const map = new Map(); clothesTags.forEach((tag) => { - const groupTags = map.get(tag.tag_group_id) ?? []; + const groupTags = map.get(tag.parent_id) ?? []; groupTags.push(tag); - map.set(tag.tag_group_id, groupTags); + map.set(tag.parent_id, groupTags); }); return map; }, [clothesTags]); @@ -123,9 +126,9 @@ export default function ClothingEdit() { await Promise.all([ fetchClothes(familyId), fetchCategories(familyId, ClothingTargetType.Clothes), - fetchTagGroups(familyId, ClothingTargetType.Clothes), fetchTags(familyId, ClothingTargetType.Clothes), fetchTagRelations(familyId, ClothingTargetType.Clothes, isEdit ? editId : undefined), + fetchMembers(familyId), ]); if (isEdit) { @@ -144,16 +147,20 @@ export default function ClothingEdit() { ) .map((row) => row.tag_id), ); + } else { + // 新增时默认归属者为当前登录用户 + setForm({ ...emptyForm, ownerId: currentUserId || null }); } } finally { setPageLoading(false); } }, [ + currentUserId, editId, familyId, fetchCategories, fetchClothes, - fetchTagGroups, + fetchMembers, fetchTagRelations, fetchTags, isEdit, @@ -248,6 +255,7 @@ export default function ClothingEdit() { name, image_file_key: form.imageFileKey, category_id: form.categoryId, + owner_id: form.ownerId, brand: form.brand.trim(), remark: form.remark.trim(), sort_number: form.sortNumber ?? clothes.length, @@ -336,6 +344,32 @@ export default function ClothingEdit() { [form.categoryId], ); + const renderMemberChip = useCallback( + (member: ClothingFamilyMember) => { + const selected = form.ownerId === member.user_id; + + return ( + + setForm((prev) => ({ + ...prev, + ownerId: prev.ownerId === member.user_id ? null : member.user_id, + })) + } + > + + {member.nickname || `用户${member.user_id}`} + + + ); + }, + [form.ownerId], + ); + const renderTagChip = useCallback( (item: ClothingTag) => { const selected = selectedTagIds.includes(item.id); @@ -359,7 +393,7 @@ export default function ClothingEdit() { ); const renderTagGroup = useCallback( - (group: ClothingTagGroup) => { + (group: ClothingTag) => { const groupTags = tagsByGroupId.get(group.id) ?? []; if (groupTags.length === 0) return null; @@ -448,6 +482,33 @@ export default function ClothingEdit() { {clothesCategories.map(renderCategoryChip)} + {/* 归属者 */} + {members.length > 0 && ( + <> + 归属者 + + 衣服仍为家庭共享,仅用于让家人知道这件衣服是谁的 + + + setForm((prev) => ({ ...prev, ownerId: null }))} + > + + 不指定 + + + {members.map(renderMemberChip)} + + + )} + {/* 标签 */} {clothesTagGroups.length > 0 && ( <> diff --git a/code/app/app-rn/src/app/clothes/outfit/detail/[id]/index.tsx b/code/app/app-rn/src/app/clothes/outfit/detail/[id]/index.tsx index 215ff11a..feb31a39 100644 --- a/code/app/app-rn/src/app/clothes/outfit/detail/[id]/index.tsx +++ b/code/app/app-rn/src/app/clothes/outfit/detail/[id]/index.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Text, TouchableOpacity, View } from "react-native"; import { Stack, useFocusEffect, useLocalSearchParams, useRouter } from "expo-router"; import { Button, Dialog, useThemeColor } from "heroui-native"; -import { PencilIcon, RotateCcwIcon, Trash2Icon } from "lucide-react-native"; +import { PencilIcon, RotateCcwIcon, Trash2Icon, UserIcon } from "lucide-react-native"; import PageLayout from "@/layouts/PageLayout"; import LayoutView from "@/components/layout/view"; import ClothingImage from "@/components/clothing/ClothingImage"; @@ -31,6 +31,8 @@ export default function OutfitDetail() { const fetchTags = useClothingStore((s) => s.fetchTags); const fetchTagRelations = useClothingStore((s) => s.fetchTagRelations); const fetchClothes = useClothingStore((s) => s.fetchClothes); + const fetchMembers = useClothingStore((s) => s.fetchMembers); + const getOwnerName = useClothingStore((s) => s.getOwnerName); const saveOutfitBundle = useClothingStore((s) => s.saveOutfitBundle); const { isLoading, startLoading, stopLoading } = useLoading(); const notifyModuleOperation = useModuleChatNotifier(); @@ -39,15 +41,16 @@ export default function OutfitDetail() { const item = useMemo(() => outfits.find((o) => o.id === outfitId) ?? null, [outfitId, outfits]); const categoryName = useMemo(() => { if (!item?.category_id) return "未分类"; return categories.find((c) => c.id === item.category_id)?.name ?? "未分类"; }, [categories, item?.category_id]); + const ownerName = useMemo(() => getOwnerName(item?.owner_id), [getOwnerName, item?.owner_id]); const details = useMemo(() => outfitClothes.filter((r) => r.outfit_id === outfitId).sort((a, b) => a.z_index - b.z_index || a.sort_number - b.sort_number).map((r) => ({ ...r, clothes: clothes.find((c) => c.id === r.clothes_id) })).filter((r) => r.clothes), [clothes, outfitClothes, outfitId]); const selectedTags = useMemo(() => { const ids = new Set(tagRelations.filter((r) => r.target_type === ClothingTargetType.Outfit && r.target_id === outfitId).map((r) => r.tag_id)); return tags.filter((t) => ids.has(t.id)); }, [outfitId, tagRelations, tags]); const getCoverKey = useCallback(() => { if (!item) return ""; if (item.cover_file_key) return item.cover_file_key; return details[0]?.clothes?.image_file_key ?? ""; }, [details, item]); const loadData = useCallback(async () => { if (!familyId || !outfitId) return; setPageLoading(true); - try { await Promise.all([fetchOutfits(familyId), fetchClothes(familyId), fetchOutfitClothes(familyId), fetchCategories(familyId, ClothingTargetType.Outfit), fetchTags(familyId, ClothingTargetType.Outfit), fetchTagRelations(familyId, ClothingTargetType.Outfit, outfitId)]); } + try { await Promise.all([fetchOutfits(familyId), fetchClothes(familyId), fetchOutfitClothes(familyId), fetchCategories(familyId, ClothingTargetType.Outfit), fetchTags(familyId, ClothingTargetType.Outfit), fetchTagRelations(familyId, ClothingTargetType.Outfit, outfitId), fetchMembers(familyId)]); } finally { setPageLoading(false); } - }, [familyId, outfitId, fetchCategories, fetchClothes, fetchOutfitClothes, fetchOutfits, fetchTags, fetchTagRelations]); + }, [familyId, outfitId, fetchCategories, fetchClothes, fetchOutfitClothes, fetchOutfits, fetchTags, fetchTagRelations, fetchMembers]); useEffect(() => { loadData(); }, [loadData]); useFocusEffect(useCallback(() => { if (familyId && outfitId) loadData(); }, [familyId, loadData, outfitId])); @@ -85,6 +88,12 @@ export default function OutfitDetail() { {categoryName} {details.length} 件衣服 + {!!ownerName && ( + + + {ownerName} + + )} {selectedTags.map((tag) => ( {tag.name} diff --git a/code/app/app-rn/src/app/clothes/outfit/edit/index.tsx b/code/app/app-rn/src/app/clothes/outfit/edit/index.tsx index 5de8f661..d1d5f387 100644 --- a/code/app/app-rn/src/app/clothes/outfit/edit/index.tsx +++ b/code/app/app-rn/src/app/clothes/outfit/edit/index.tsx @@ -23,13 +23,12 @@ import { LoadingOverlay } from "@/components/LoadingOverlay"; import { useLoading } from "@/hooks/useLoading"; import { useModuleChatNotifier } from "@/hooks/useModuleChatNotifier"; import { getUniqueIdApi } from "@/request/api"; -import { useClothingStore, useFamilyStore } from "@/store"; +import { useClothingStore, useFamilyStore, useUserStore, type ClothingFamilyMember } from "@/store"; import { ClothingCategory, ClothingClothes, ClothingStatus, ClothingTag, - ClothingTagGroup, ClothingTargetType, } from "@/types/clothing"; import { deleteS3FileQuietly } from "@/utils/file"; @@ -146,19 +145,20 @@ export default function ClothingOutfitEdit() { const editId = id ? Number(id) : 0; const isEdit = editId > 0; const familyId = useFamilyStore((s) => s.getFamilyId()); + const currentUserId = useUserStore((s) => s.getUserId()); const accentColor = useThemeColor("accent"); const clothes = useClothingStore((s) => s.clothes); const categories = useClothingStore((s) => s.categories); - const tagGroups = useClothingStore((s) => s.tagGroups); const tags = useClothingStore((s) => s.tags); const tagRelations = useClothingStore((s) => s.tagRelations); + const members = useClothingStore((s) => s.members); const fetchClothes = useClothingStore((s) => s.fetchClothes); const fetchOutfits = useClothingStore((s) => s.fetchOutfits); const fetchOutfitClothes = useClothingStore((s) => s.fetchOutfitClothes); const fetchCategories = useClothingStore((s) => s.fetchCategories); - const fetchTagGroups = useClothingStore((s) => s.fetchTagGroups); const fetchTags = useClothingStore((s) => s.fetchTags); const fetchTagRelations = useClothingStore((s) => s.fetchTagRelations); + const fetchMembers = useClothingStore((s) => s.fetchMembers); const saveOutfitBundle = useClothingStore((s) => s.saveOutfitBundle); const uploadOutfitCover = useClothingStore((s) => s.uploadOutfitCover); const { isLoading, startLoading, stopLoading } = useLoading(); @@ -167,6 +167,7 @@ export default function ClothingOutfitEdit() { const canvasRef = useRef(null); const [name, setName] = useState(""); const [categoryId, setCategoryId] = useState(null); + const [ownerId, setOwnerId] = useState(null); const [remark, setRemark] = useState(""); const [selectedTagIds, setSelectedTagIds] = useState([]); const [canvasSize, setCanvasSize] = useState(0); @@ -211,19 +212,19 @@ export default function ClothingOutfitEdit() { [categories], ); const outfitTagGroups = useMemo( - () => tagGroups.filter((item) => item.type === ClothingTargetType.Outfit), - [tagGroups], + () => tags.filter((item) => item.type === ClothingTargetType.Outfit && item.parent_id === 0), + [tags], ); const outfitTags = useMemo( - () => tags.filter((item) => item.type === ClothingTargetType.Outfit), + () => tags.filter((item) => item.type === ClothingTargetType.Outfit && item.parent_id !== 0), [tags], ); const tagsByGroupId = useMemo(() => { const map = new Map(); outfitTags.forEach((tag) => { - const groupTags = map.get(tag.tag_group_id) ?? []; + const groupTags = map.get(tag.parent_id) ?? []; groupTags.push(tag); - map.set(tag.tag_group_id, groupTags); + map.set(tag.parent_id, groupTags); }); return map; }, [outfitTags]); @@ -242,9 +243,9 @@ export default function ClothingOutfitEdit() { fetchOutfits(familyId), fetchOutfitClothes(familyId), fetchCategories(familyId, ClothingTargetType.Outfit), - fetchTagGroups(familyId, ClothingTargetType.Outfit), fetchTags(familyId, ClothingTargetType.Outfit), fetchTagRelations(familyId, ClothingTargetType.Outfit, isEdit ? editId : undefined), + fetchMembers(familyId), ]); if (isEdit) { @@ -262,6 +263,7 @@ export default function ClothingOutfitEdit() { setName(outfit.name); setCategoryId(outfit.category_id ?? null); + setOwnerId(outfit.owner_id ?? null); setRemark(outfit.remark ?? ""); setSelectedTagIds( state.tagRelations @@ -284,18 +286,22 @@ export default function ClothingOutfitEdit() { })), ); setSelectedUid(details[0] ? `detail_${details[0].id}` : null); + } else { + // 新增时默认归属者为当前登录用户 + setOwnerId(currentUserId || null); } } finally { setPageLoading(false); } }, [ + currentUserId, editId, familyId, fetchCategories, fetchClothes, + fetchMembers, fetchOutfitClothes, fetchOutfits, - fetchTagGroups, fetchTagRelations, fetchTags, isEdit, @@ -417,6 +423,27 @@ export default function ClothingOutfitEdit() { [categoryId], ); + const renderMemberChip = useCallback( + (member: ClothingFamilyMember) => { + const selected = ownerId === member.user_id; + + return ( + setOwnerId((prev) => (prev === member.user_id ? null : member.user_id))} + > + + {member.nickname || `用户${member.user_id}`} + + + ); + }, + [ownerId], + ); + const renderTagChip = useCallback( (item: ClothingTag) => { const selected = selectedTagIds.includes(item.id); @@ -440,7 +467,7 @@ export default function ClothingOutfitEdit() { ); const renderTagGroup = useCallback( - (group: ClothingTagGroup) => { + (group: ClothingTag) => { const groupTags = tagsByGroupId.get(group.id) ?? []; if (groupTags.length === 0) return null; @@ -531,6 +558,7 @@ export default function ClothingOutfitEdit() { background: "light", }, category_id: categoryId, + owner_id: ownerId, remark: remark.trim(), sort_number: currentOutfit?.sort_number ?? state.outfits.length, status: ClothingStatus.Enabled, @@ -637,6 +665,7 @@ export default function ClothingOutfitEdit() { items, name, notifyModuleOperation, + ownerId, remark, router, saveOutfitBundle, @@ -712,6 +741,31 @@ export default function ClothingOutfitEdit() { {outfitCategories.map(renderCategoryChip)} + {/* 归属者 */} + {members.length > 0 && ( + <> + 归属者 + + 搭配仍为家庭共享,仅用于让家人知道这套搭配是谁的 + + + setOwnerId(null)} + > + + 不指定 + + + {members.map(renderMemberChip)} + + + )} + {/* 标签 */} {outfitTagGroups.length > 0 && ( <> @@ -797,11 +851,11 @@ export default function ClothingOutfitEdit() { )} {/* 标签按分组 */} - {tags.filter((t) => t.type === ClothingTargetType.Clothes).length > 0 && (() => { - const ctGroups = tagGroups.filter((g) => g.type === ClothingTargetType.Clothes); - const ctTags = tags.filter((t) => t.type === ClothingTargetType.Clothes); + {tags.filter((t) => t.type === ClothingTargetType.Clothes && t.parent_id !== 0).length > 0 && (() => { + const ctGroups = tags.filter((g) => g.type === ClothingTargetType.Clothes && g.parent_id === 0); + const ctTags = tags.filter((t) => t.type === ClothingTargetType.Clothes && t.parent_id !== 0); return ctGroups.map((g) => { - const gTags = ctTags.filter((t) => t.tag_group_id === g.id); + const gTags = ctTags.filter((t) => t.parent_id === g.id); if (gTags.length === 0) return null; return ( diff --git a/code/app/app-rn/src/components/clothing/ClothesGridItem.tsx b/code/app/app-rn/src/components/clothing/ClothesGridItem.tsx index 4d8bd739..9d1d1d90 100644 --- a/code/app/app-rn/src/components/clothing/ClothesGridItem.tsx +++ b/code/app/app-rn/src/components/clothing/ClothesGridItem.tsx @@ -1,14 +1,16 @@ import { Text, View } from "react-native"; +import { UserIcon } from "lucide-react-native"; import ClothingImage from "./ClothingImage"; import { ClothingClothes } from "@/types/clothing"; interface ClothesGridItemProps { item: ClothingClothes; categoryName?: string; + ownerName?: string; aspectRatio?: number; } -export default function ClothesGridItem({ item, categoryName, aspectRatio = 1 }: ClothesGridItemProps) { +export default function ClothesGridItem({ item, categoryName, ownerName, aspectRatio = 1 }: ClothesGridItemProps) { return ( @@ -18,6 +20,12 @@ export default function ClothesGridItem({ item, categoryName, aspectRatio = 1 }: {categoryName} )} + {!!ownerName && ( + + + {ownerName} + + )} {item.name} diff --git a/code/app/app-rn/src/store/clothing.ts b/code/app/app-rn/src/store/clothing.ts index 3c5d9bfd..2b3bec17 100644 --- a/code/app/app-rn/src/store/clothing.ts +++ b/code/app/app-rn/src/store/clothing.ts @@ -8,7 +8,6 @@ import { ClothingSaveChanges, ClothingStatus, ClothingTag, - ClothingTagGroup, ClothingTagRelation, ClothingTargetType, } from "@/types/clothing"; @@ -18,19 +17,25 @@ const TABLES = { outfit: "b_clothing_outfit", outfitClothes: "b_clothing_outfit_clothes", category: "b_clothing_category", - tagGroup: "b_clothing_tag_group", tag: "b_clothing_tag", tagRelation: "b_clothing_tag_relation", + familyMember: "b_family_member", } as const; +export interface ClothingFamilyMember { + user_id: number; + nickname: string; + avatar?: string; +} + interface ClothingState { clothes: ClothingClothes[]; outfits: ClothingOutfit[]; outfitClothes: ClothingOutfitClothes[]; categories: ClothingCategory[]; - tagGroups: ClothingTagGroup[]; tags: ClothingTag[]; tagRelations: ClothingTagRelation[]; + members: ClothingFamilyMember[]; clear: () => void; @@ -39,13 +44,14 @@ interface ClothingState { fetchOutfits: (familyId: number) => Promise; fetchOutfitClothes: (familyId: number, outfitId?: number) => Promise; fetchCategories: (familyId: number, type?: ClothingTargetType) => Promise; - fetchTagGroups: (familyId: number, type?: ClothingTargetType) => Promise; - fetchTags: (familyId: number, type?: ClothingTargetType, tagGroupId?: number) => Promise; + fetchTags: (familyId: number, type?: ClothingTargetType, parentId?: number) => Promise; fetchTagRelations: ( familyId: number, targetType?: ClothingTargetType, targetId?: number, ) => Promise; + fetchMembers: (familyId: number) => Promise; + getOwnerName: (ownerId?: number | null) => string; saveClothesChanges: (changes: ClothingSaveChanges) => Promise; saveOutfitChanges: (changes: ClothingSaveChanges) => Promise; @@ -53,16 +59,12 @@ interface ClothingState { changes: ClothingSaveChanges, ) => Promise; saveCategoryChanges: (changes: ClothingSaveChanges) => Promise; - saveTagGroupChanges: (changes: ClothingSaveChanges) => Promise; saveTagChanges: (changes: ClothingSaveChanges) => Promise; saveTagRelationChanges: (changes: ClothingSaveChanges) => Promise; saveMetadataBundle: (payload: { categoryInserts?: Partial[]; categoryUpdates?: Partial[]; categoryDeletes?: Partial[]; - tagGroupInserts?: Partial[]; - tagGroupUpdates?: Partial[]; - tagGroupDeletes?: Partial[]; tagInserts?: Partial[]; tagUpdates?: Partial[]; tagDeletes?: Partial[]; @@ -121,9 +123,9 @@ export const useClothingStore = create((set, get) => ({ outfits: [], outfitClothes: [], categories: [], - tagGroups: [], tags: [], tagRelations: [], + members: [], clear: () => set({ @@ -131,9 +133,9 @@ export const useClothingStore = create((set, get) => ({ outfits: [], outfitClothes: [], categories: [], - tagGroups: [], tags: [], tagRelations: [], + members: [], }), fetchAll: async (familyId: number) => { @@ -141,9 +143,9 @@ export const useClothingStore = create((set, get) => ({ get().fetchClothes(familyId), get().fetchOutfits(familyId), get().fetchCategories(familyId), - get().fetchTagGroups(familyId), get().fetchTags(familyId), get().fetchTagRelations(familyId), + get().fetchMembers(familyId), ]); }, @@ -213,24 +215,7 @@ export const useClothingStore = create((set, get) => ({ } }, - fetchTagGroups: async (familyId: number, type?: ClothingTargetType) => { - if (!familyId) return; - - const { condition, args } = typeCondition(type); - const res = await loadDataApi({ - view_name: TABLES.tagGroup, - search_condition: condition, - search_columns: [], - order_by: "type asc, sort_number asc, id asc", - args: [familyId, ClothingStatus.Enabled, ...args], - }); - - if (res.isSuccess) { - set({ tagGroups: res.data as ClothingTagGroup[] }); - } - }, - - fetchTags: async (familyId: number, type?: ClothingTargetType, tagGroupId?: number) => { + fetchTags: async (familyId: number, type?: ClothingTargetType, parentId?: number) => { if (!familyId) return; const conditions = ["family_id = ?", "status = ?"]; @@ -241,16 +226,16 @@ export const useClothingStore = create((set, get) => ({ args.push(type); } - if (tagGroupId !== undefined) { - conditions.push("tag_group_id = ?"); - args.push(tagGroupId); + if (parentId !== undefined) { + conditions.push("parent_id = ?"); + args.push(parentId); } const res = await loadDataApi({ view_name: TABLES.tag, search_condition: conditions.join(" and "), search_columns: [], - order_by: "type asc, tag_group_id asc, sort_number asc, id asc", + order_by: "type asc, parent_id asc, sort_number asc, id asc", args, }); @@ -292,6 +277,28 @@ export const useClothingStore = create((set, get) => ({ } }, + fetchMembers: async (familyId: number) => { + if (!familyId) return; + + const res = await loadDataApi({ + view_name: TABLES.familyMember, + search_condition: "family_id = ?", + search_columns: ["user_id", "nickname"], + order_by: "id asc", + args: [familyId], + }); + + if (res.isSuccess) { + set({ members: (res.data as ClothingFamilyMember[]) ?? [] }); + } + }, + + getOwnerName: (ownerId?: number | null) => { + if (!ownerId) return ""; + const m = get().members.find((row) => row.user_id === ownerId); + return m?.nickname || `用户${ownerId}`; + }, + saveClothesChanges: async (changes: ClothingSaveChanges) => { const res = await saveDataApi([ { table_name: TABLES.clothes, key_field: "id", ...normalizeChanges(changes) }, @@ -320,13 +327,6 @@ export const useClothingStore = create((set, get) => ({ return res.isSuccess; }, - saveTagGroupChanges: async (changes: ClothingSaveChanges) => { - const res = await saveDataApi([ - { table_name: TABLES.tagGroup, key_field: "id", ...normalizeChanges(changes) }, - ]); - return res.isSuccess; - }, - saveTagChanges: async (changes: ClothingSaveChanges) => { const res = await saveDataApi([ { table_name: TABLES.tag, key_field: "id", ...normalizeChanges(changes) }, @@ -350,13 +350,6 @@ export const useClothingStore = create((set, get) => ({ updates: payload.categoryUpdates ?? [], deletes: payload.categoryDeletes ?? [], }, - { - table_name: TABLES.tagGroup, - key_field: "id", - inserts: payload.tagGroupInserts ?? [], - updates: payload.tagGroupUpdates ?? [], - deletes: payload.tagGroupDeletes ?? [], - }, { table_name: TABLES.tag, key_field: "id", diff --git a/code/app/app-rn/src/types/clothing.ts b/code/app/app-rn/src/types/clothing.ts index 6baf389f..68d9269b 100644 --- a/code/app/app-rn/src/types/clothing.ts +++ b/code/app/app-rn/src/types/clothing.ts @@ -14,6 +14,7 @@ export interface ClothingClothes { name: string; image_file_key: string; category_id?: number | null; + owner_id?: number | null; brand?: string; remark?: string; sort_number: number; @@ -37,6 +38,7 @@ export interface ClothingOutfit { cover_file_key?: string; layout_json?: ClothingOutfitLayout | Record; category_id?: number | null; + owner_id?: number | null; remark?: string; sort_number: number; status: ClothingStatus; @@ -76,23 +78,10 @@ export interface ClothingCategory { update_by?: number; } -export interface ClothingTagGroup { - id: number; - family_id: number; - name: string; - type: ClothingTargetType; - sort_number: number; - status: ClothingStatus; - create_time?: string; - update_time?: string; - create_by?: number; - update_by?: number; -} - export interface ClothingTag { id: number; family_id: number; - tag_group_id: number; + parent_id: number; name: string; type: ClothingTargetType; color?: string; diff --git a/code/app/app-rn/test.html b/code/app/app-rn/test.html new file mode 100644 index 00000000..5716d083 --- /dev/null +++ b/code/app/app-rn/test.html @@ -0,0 +1,536 @@ + + + + + + + 分类与标签 + + + + + +
+
+

分类与标签

+

统一管理您的内容组织方式

+
+ +
+ +
分类管理
+
+
+
+ + +
标签管理 (支持父子级)
+
+
+
+
+ + + + + + +
+ + + + + \ No newline at end of file