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