20260703173210
This commit is contained in:
1 parent
d0f7f0d3d8
commit
3556d5e8c1
15 files changed
+853
-143
No files matched your search
@@ -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() {
|
||||
<TouchableOpacity key={item.id} activeOpacity={0.85} delayLongPress={260}
|
||||
onPress={() => { setActiveItem(null); router.push(`/clothes/detail/${item.id}` as any); }}
|
||||
onLongPress={(e) => { setMenuPos({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY }); setActiveItem(item); }}>
|
||||
<ClothesGridItem item={item} categoryName={renderCategoryName(item.category_id)} aspectRatio={item.r} />
|
||||
<ClothesGridItem item={item} categoryName={renderCategoryName(item.category_id)} ownerName={getOwnerName(item.owner_id)} aspectRatio={item.r} />
|
||||
</TouchableOpacity>
|
||||
), [renderCategoryName]);
|
||||
), [getOwnerName, renderCategoryName]);
|
||||
|
||||
return (
|
||||
<LayoutView>
|
||||
|
||||
@@ -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>(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<ClothingTagGroup | null>(null);
|
||||
const [editingGroup, setEditingGroup] = useState<ClothingTag | null>(null);
|
||||
const [showGroupSheet, setShowGroupSheet] = useState(false);
|
||||
const [groupDelete, setGroupDelete] = useState<ClothingTagGroup | null>(null);
|
||||
const [groupDelete, setGroupDelete] = useState<ClothingTag | null>(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 (
|
||||
<LayoutView>
|
||||
|
||||
@@ -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 (
|
||||
<TouchableOpacity key={item.id} activeOpacity={0.85} delayLongPress={260}
|
||||
onPress={() => router.push(`/clothes/outfit/detail/${item.id}` as any)}
|
||||
@@ -127,13 +130,19 @@ export default function ClothingOutfitList() {
|
||||
<View className="absolute bottom-1.5 left-1.5 rounded-full bg-black/50 px-2 py-0.5">
|
||||
<Text className="text-[9px] text-white font-medium">{getClothesCount(item)} 件</Text>
|
||||
</View>
|
||||
{!!ownerName && (
|
||||
<View className="absolute top-1.5 right-1.5 rounded-full bg-black/50 px-1.5 py-0.5 flex-row items-center">
|
||||
<UserIcon size={8} color="#fff" />
|
||||
<Text className="text-[9px] text-white font-medium ml-0.5" numberOfLines={1}>{ownerName}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text className="text-xs font-medium text-gray-900 mt-1.5" numberOfLines={1}>{item.name}</Text>
|
||||
{catName && <Text className="text-[10px] text-gray-400">{catName}</Text>}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}, [categoriesById, getClothesCount, getCoverKey]);
|
||||
}, [categoriesById, getClothesCount, getCoverKey, getOwnerName]);
|
||||
|
||||
return (
|
||||
<LayoutView>
|
||||
|
||||
@@ -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() {
|
||||
{/* 分类 + 标签数 - 横向排列,不套卡片 */}
|
||||
<View className="flex-row items-center gap-3 mt-4 pb-4 border-b border-gray-100">
|
||||
<View className="rounded-full bg-accent px-3 py-1"><Text className="text-xs text-white">{categoryName}</Text></View>
|
||||
{!!ownerName && (
|
||||
<View className="flex-row items-center gap-1 rounded-full bg-gray-100 px-2.5 py-1">
|
||||
<UserIcon size={10} color="#4B5563" />
|
||||
<Text className="text-xs text-gray-600">{ownerName}</Text>
|
||||
</View>
|
||||
)}
|
||||
{selectedTags.length > 0 && (
|
||||
<View className="flex-row flex-wrap gap-1.5 flex-1">
|
||||
{selectedTags.map((tag) => (
|
||||
|
||||
@@ -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<number, ClothingTag[]>();
|
||||
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 (
|
||||
<TouchableOpacity
|
||||
key={member.user_id}
|
||||
className={`rounded-full border px-3 py-2 ${
|
||||
selected ? "border-gray-900 bg-gray-900" : "border-gray-200 bg-white"
|
||||
}`}
|
||||
onPress={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
ownerId: prev.ownerId === member.user_id ? null : member.user_id,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<Text className={`text-xs ${selected ? "text-white" : "text-gray-700"}`}>
|
||||
{member.nickname || `用户${member.user_id}`}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
},
|
||||
[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)}
|
||||
</View>
|
||||
|
||||
{/* 归属者 */}
|
||||
{members.length > 0 && (
|
||||
<>
|
||||
<Text className="text-base font-semibold text-gray-900 mb-1 ml-1">归属者</Text>
|
||||
<Text className="text-xs text-gray-400 mb-3 ml-1">
|
||||
衣服仍为家庭共享,仅用于让家人知道这件衣服是谁的
|
||||
</Text>
|
||||
<View className="flex-row flex-wrap gap-2 mb-4">
|
||||
<TouchableOpacity
|
||||
className={`rounded-full border px-3 py-2 ${
|
||||
form.ownerId == null
|
||||
? "border-gray-900 bg-gray-900"
|
||||
: "border-gray-200 bg-white"
|
||||
}`}
|
||||
onPress={() => setForm((prev) => ({ ...prev, ownerId: null }))}
|
||||
>
|
||||
<Text
|
||||
className={`text-xs ${form.ownerId == null ? "text-white" : "text-gray-700"}`}
|
||||
>
|
||||
不指定
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{members.map(renderMemberChip)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 标签 */}
|
||||
{clothesTagGroups.length > 0 && (
|
||||
<>
|
||||
|
||||
@@ -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() {
|
||||
<View className="flex-row items-center gap-3 mt-4 pb-4 border-b border-gray-100">
|
||||
<View className="rounded-full bg-accent px-3 py-1"><Text className="text-xs text-white">{categoryName}</Text></View>
|
||||
<Text className="text-xs text-gray-500">{details.length} 件衣服</Text>
|
||||
{!!ownerName && (
|
||||
<View className="flex-row items-center gap-1 rounded-full bg-gray-100 px-2.5 py-1">
|
||||
<UserIcon size={10} color="#4B5563" />
|
||||
<Text className="text-xs text-gray-600">{ownerName}</Text>
|
||||
</View>
|
||||
)}
|
||||
{selectedTags.map((tag) => (
|
||||
<View key={tag.id} className="rounded-full px-2.5 py-1" style={{ backgroundColor: tag.color || "#F3F4F6" }}>
|
||||
<Text className="text-xs font-medium" style={{ color: tag.color ? "#fff" : "#4B5563" }}>{tag.name}</Text>
|
||||
|
||||
@@ -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<View>(null);
|
||||
const [name, setName] = useState("");
|
||||
const [categoryId, setCategoryId] = useState<number | null>(null);
|
||||
const [ownerId, setOwnerId] = useState<number | null>(null);
|
||||
const [remark, setRemark] = useState("");
|
||||
const [selectedTagIds, setSelectedTagIds] = useState<number[]>([]);
|
||||
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<number, ClothingTag[]>();
|
||||
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 (
|
||||
<TouchableOpacity
|
||||
key={member.user_id}
|
||||
className={`rounded-full border px-3 py-2 ${
|
||||
selected ? "border-gray-900 bg-gray-900" : "border-gray-200 bg-white"
|
||||
}`}
|
||||
onPress={() => setOwnerId((prev) => (prev === member.user_id ? null : member.user_id))}
|
||||
>
|
||||
<Text className={`text-xs ${selected ? "text-white" : "text-gray-700"}`}>
|
||||
{member.nickname || `用户${member.user_id}`}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
},
|
||||
[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)}
|
||||
</View>
|
||||
|
||||
{/* 归属者 */}
|
||||
{members.length > 0 && (
|
||||
<>
|
||||
<Text className="text-base font-semibold text-gray-900 mb-1 ml-1">归属者</Text>
|
||||
<Text className="text-xs text-gray-400 mb-3 ml-1">
|
||||
搭配仍为家庭共享,仅用于让家人知道这套搭配是谁的
|
||||
</Text>
|
||||
<View className="flex-row flex-wrap gap-2 mb-4">
|
||||
<TouchableOpacity
|
||||
className={`rounded-full border px-3 py-2 ${
|
||||
ownerId == null ? "border-gray-900 bg-gray-900" : "border-gray-200 bg-white"
|
||||
}`}
|
||||
onPress={() => setOwnerId(null)}
|
||||
>
|
||||
<Text
|
||||
className={`text-xs ${ownerId == null ? "text-white" : "text-gray-700"}`}
|
||||
>
|
||||
不指定
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{members.map(renderMemberChip)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 标签 */}
|
||||
{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 (
|
||||
<View key={g.id}>
|
||||
|
||||
@@ -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 (
|
||||
<View className="flex-col">
|
||||
<View className="relative rounded-xl overflow-hidden bg-gray-100" style={{ aspectRatio }}>
|
||||
@@ -18,6 +20,12 @@ export default function ClothesGridItem({ item, categoryName, aspectRatio = 1 }:
|
||||
<Text className="text-[9px] text-white font-medium">{categoryName}</Text>
|
||||
</View>
|
||||
)}
|
||||
{!!ownerName && (
|
||||
<View className="absolute top-1.5 right-1.5 rounded-full bg-black/50 px-1.5 py-0.5 flex-row items-center">
|
||||
<UserIcon size={8} color="#fff" />
|
||||
<Text className="text-[9px] text-white font-medium ml-0.5" numberOfLines={1}>{ownerName}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text className="text-xs font-medium text-gray-900 mt-1.5" numberOfLines={1}>{item.name}</Text>
|
||||
</View>
|
||||
|
||||
@@ -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<void>;
|
||||
fetchOutfitClothes: (familyId: number, outfitId?: number) => Promise<void>;
|
||||
fetchCategories: (familyId: number, type?: ClothingTargetType) => Promise<void>;
|
||||
fetchTagGroups: (familyId: number, type?: ClothingTargetType) => Promise<void>;
|
||||
fetchTags: (familyId: number, type?: ClothingTargetType, tagGroupId?: number) => Promise<void>;
|
||||
fetchTags: (familyId: number, type?: ClothingTargetType, parentId?: number) => Promise<void>;
|
||||
fetchTagRelations: (
|
||||
familyId: number,
|
||||
targetType?: ClothingTargetType,
|
||||
targetId?: number,
|
||||
) => Promise<void>;
|
||||
fetchMembers: (familyId: number) => Promise<void>;
|
||||
getOwnerName: (ownerId?: number | null) => string;
|
||||
|
||||
saveClothesChanges: (changes: ClothingSaveChanges<ClothingClothes>) => Promise<boolean>;
|
||||
saveOutfitChanges: (changes: ClothingSaveChanges<ClothingOutfit>) => Promise<boolean>;
|
||||
@@ -53,16 +59,12 @@ interface ClothingState {
|
||||
changes: ClothingSaveChanges<ClothingOutfitClothes>,
|
||||
) => Promise<boolean>;
|
||||
saveCategoryChanges: (changes: ClothingSaveChanges<ClothingCategory>) => Promise<boolean>;
|
||||
saveTagGroupChanges: (changes: ClothingSaveChanges<ClothingTagGroup>) => Promise<boolean>;
|
||||
saveTagChanges: (changes: ClothingSaveChanges<ClothingTag>) => Promise<boolean>;
|
||||
saveTagRelationChanges: (changes: ClothingSaveChanges<ClothingTagRelation>) => Promise<boolean>;
|
||||
saveMetadataBundle: (payload: {
|
||||
categoryInserts?: Partial<ClothingCategory>[];
|
||||
categoryUpdates?: Partial<ClothingCategory>[];
|
||||
categoryDeletes?: Partial<ClothingCategory>[];
|
||||
tagGroupInserts?: Partial<ClothingTagGroup>[];
|
||||
tagGroupUpdates?: Partial<ClothingTagGroup>[];
|
||||
tagGroupDeletes?: Partial<ClothingTagGroup>[];
|
||||
tagInserts?: Partial<ClothingTag>[];
|
||||
tagUpdates?: Partial<ClothingTag>[];
|
||||
tagDeletes?: Partial<ClothingTag>[];
|
||||
@@ -121,9 +123,9 @@ export const useClothingStore = create<ClothingState>((set, get) => ({
|
||||
outfits: [],
|
||||
outfitClothes: [],
|
||||
categories: [],
|
||||
tagGroups: [],
|
||||
tags: [],
|
||||
tagRelations: [],
|
||||
members: [],
|
||||
|
||||
clear: () =>
|
||||
set({
|
||||
@@ -131,9 +133,9 @@ export const useClothingStore = create<ClothingState>((set, get) => ({
|
||||
outfits: [],
|
||||
outfitClothes: [],
|
||||
categories: [],
|
||||
tagGroups: [],
|
||||
tags: [],
|
||||
tagRelations: [],
|
||||
members: [],
|
||||
}),
|
||||
|
||||
fetchAll: async (familyId: number) => {
|
||||
@@ -141,9 +143,9 @@ export const useClothingStore = create<ClothingState>((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<ClothingState>((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<ClothingState>((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<ClothingState>((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<ClothingClothes>) => {
|
||||
const res = await saveDataApi([
|
||||
{ table_name: TABLES.clothes, key_field: "id", ...normalizeChanges(changes) },
|
||||
@@ -320,13 +327,6 @@ export const useClothingStore = create<ClothingState>((set, get) => ({
|
||||
return res.isSuccess;
|
||||
},
|
||||
|
||||
saveTagGroupChanges: async (changes: ClothingSaveChanges<ClothingTagGroup>) => {
|
||||
const res = await saveDataApi([
|
||||
{ table_name: TABLES.tagGroup, key_field: "id", ...normalizeChanges(changes) },
|
||||
]);
|
||||
return res.isSuccess;
|
||||
},
|
||||
|
||||
saveTagChanges: async (changes: ClothingSaveChanges<ClothingTag>) => {
|
||||
const res = await saveDataApi([
|
||||
{ table_name: TABLES.tag, key_field: "id", ...normalizeChanges(changes) },
|
||||
@@ -350,13 +350,6 @@ export const useClothingStore = create<ClothingState>((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",
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user