diff --git a/code/app/app-rn/.env.development b/code/app/app-rn/.env.development index 9d252886..d2bcf72b 100644 --- a/code/app/app-rn/.env.development +++ b/code/app/app-rn/.env.development @@ -1,3 +1,3 @@ -EXPO_PUBLIC_API_URL=http://192.168.5.72:8087 +EXPO_PUBLIC_API_URL=http://192.168.5.80:8087 EXPO_PUBLIC_G3_URL=http://118.89.70.199:9001 EXPO_PUBLIC_G3_BUCKET=test \ No newline at end of file diff --git a/code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx b/code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx index 49dd0e1c..75f80a1a 100644 --- a/code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx +++ b/code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx @@ -1,5 +1,8 @@ import { useCallback, useEffect, useRef, useState, memo, useMemo } from "react"; -import { Image, Text, TextInput, View } from "react-native"; +import JizhangIcon from "@/static/images/subapps/jizhang.webp"; +import CaidanIcon from "@/static/images/subapps/caidan.webp"; +import YifudapeiIcon from "@/static/images/subapps/yifudapei.webp"; +import { Image, ImageSourcePropType, Text, TextInput, TouchableOpacity, View } from "react-native"; import { KeyboardAvoidingView } from "react-native-keyboard-controller"; import { useRouter } from "expo-router"; import { Menu, PressableFeedback, Spinner } from "heroui-native"; @@ -28,6 +31,58 @@ function formatTime(ts: string) { return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`; } +// ===================== 模块卡片配置 ===================== + +const SUBAPP_ICONS: Record = { + finance: JizhangIcon, + meal: CaidanIcon, + clothes: YifudapeiIcon, +}; + +const SOURCE_CARD_CONFIG: Record string }> = { + b_finance: { label: "记账", icon: SUBAPP_ICONS.finance, route: (id) => `/finance/detail/${id}` }, + b_todo: { label: "待办", route: (id) => `/todo/detail/${id}` }, + b_meal_dish: { label: "家庭菜单", icon: SUBAPP_ICONS.meal, route: (id) => `/meal/dish/detail/${id}` }, + b_meal_plan: { label: "家庭菜单", icon: SUBAPP_ICONS.meal, route: (id) => `/meal/plan/detail/${id}` }, + b_clothing_outfit: { label: "衣服穿搭", icon: SUBAPP_ICONS.clothes, route: (id) => `/clothes/outfit/detail/${id}` }, + b_clothing_clothes: { label: "衣服穿搭", icon: SUBAPP_ICONS.clothes, route: (id) => `/clothes/detail/${id}` }, +}; + +/** 从 content 中解析模块名和正文,格式:『【模块】正文』 */ +function parseModuleContent(content: string): { module: string; body: string } { + const m = content.match(/^【(.+?)】(.*)$/); + return m ? { module: m[1], body: m[2] } : { module: "", body: content }; +} + +/** 根据 source_table 从 source_data 中提取关键信息 */ +function formatSourceDetail(sourceTable: string, sourceData: string | null): string | null { + if (!sourceData) return null; + try { + const d = JSON.parse(sourceData); + switch (sourceTable) { + case "b_finance": { + const parts: string[] = []; + if (d.amount != null) parts.push(`¥${d.amount}`); + if (d.remark) parts.push(d.remark); + return parts.join(" · ") || null; + } + case "b_todo": + if (d.description) return String(d.description); + return null; + case "b_meal_dish": { + const parts: string[] = []; + if (d.cooking_time_minutes) parts.push(`约${d.cooking_time_minutes}分钟`); + if (d.difficulty) parts.push(`${"★".repeat(d.difficulty)}`); + return parts.join(" · ") || null; + } + default: + return null; + } + } catch { + return null; + } +} + // ===================== 在线栏 ===================== function ChatOnlineBar({ users, onSearch }: { users: OnlineUser[]; onSearch: () => void }) { @@ -100,6 +155,18 @@ const ChatMessageItem = memo(function ChatMessageItem({ const canRecall = isSelf && within2Min; const canShowActions = canRecall || isAdmin; + // 是否为模块卡片消息(Forward 或带 source 的 System) + const isCardMsg = item.msg_type === ChatMsgType.Forward || (item.msg_type === ChatMsgType.System && !!item.source_table); + const cardConfig = isCardMsg && item.source_table ? SOURCE_CARD_CONFIG[item.source_table] : null; + const { module: moduleName, body: moduleBody } = parseModuleContent(item.content); + const sourceDetail = isCardMsg && item.source_table ? formatSourceDetail(item.source_table, item.source_data) : null; + + const handleCardPress = useCallback(() => { + if (cardConfig && item.source_id) { + router.push(cardConfig.route(item.source_id) as any); + } + }, [cardConfig, item.source_id, router]); + if (item.recalled === 1) { return ( @@ -108,7 +175,7 @@ const ChatMessageItem = memo(function ChatMessageItem({ ); } - if (item.msg_type === ChatMsgType.System) { + if (item.msg_type === ChatMsgType.System && !item.source_table) { return ( @@ -229,40 +296,46 @@ const ChatMessageItem = memo(function ChatMessageItem({ )} - {item.msg_type === ChatMsgType.Forward && item.source_data && ( - - - 来自 {item.source_table} - - {(() => { - try { - const d = JSON.parse(item.source_data); - return Object.entries(d) - .filter( - ([k]) => - ![ - "id", - "create_time", - "update_time", - "create_by", - "update_by", - "family_id", - ].includes(k), - ) - .map(([k, v]) => `${k}: ${v}`) - .join(" "); - } catch { - return item.source_data; - } - })()} - - - {item.content ? ( - - {item.content} + {isCardMsg && ( + + + {/* 模块标识:图标 + 名称 */} + + {cardConfig?.icon ? ( + + ) : ( + + )} + + {cardConfig?.label ?? moduleName ?? "动态"} + - ) : null} - + + {/* 操作描述 */} + + {moduleBody || item.content} + + + {/* 来源补充信息 */} + {sourceDetail && ( + + {sourceDetail} + + )} + + )} @@ -547,7 +620,7 @@ export default function ChatPage() { } // Retry a failed message - function handleRetry(p: PendingMsg) { + const handleRetry = useCallback((p: PendingMsg) => { setPending((prev) => prev.map((x) => (x.localId === p.localId ? { ...x, status: "sending" } : x)), ); @@ -559,7 +632,50 @@ export default function ChatPage() { ); toast.error("发送失败", { id: p.localId }); }, 10_000); - } + }, [ws]); + + // memo 稳定引用,避免 inverted FlatList 反复 renderItem + const keyExtractor = useCallback((item: any) => { + const p = item._pending; + return p ? p.localId : String(item.id); + }, []); + + const renderItem = useCallback(({ item }: { item: any }) => { + const pending = item._pending as PendingMsg | undefined; + if (pending) { + return ( + + {pending.status === "sending" ? ( + + + + ) : ( + handleRetry(pending)}> + + + + + + )} + + + {pending.content} + + + + ); + } + return ( + + ); + }, [userId, members, adminRole, ws.recall, ws.deleteMessage, handleRetry]); if (loading) { return ( @@ -612,10 +728,7 @@ export default function ChatPage() { } data={invertedMessages} inverted - keyExtractor={(item) => { - const p = (item as any)._pending; - return p ? p.localId : String(item.id); - }} + keyExtractor={keyExtractor} contentContainerStyle={{ paddingTop: 40 }} onEndReached={() => { if (hasMore && !loadingMore) handleLoadMore(); @@ -637,42 +750,7 @@ export default function ChatPage() { 暂无消息,开始聊天吧 } - renderItem={({ item }: { item: any }) => { - const pending = item._pending as PendingMsg | undefined; - if (pending) { - return ( - - {pending.status === "sending" ? ( - - - - ) : ( - handleRetry(pending)}> - - - - - - )} - - - {pending.content} - - - - ); - } - return ( - - ); - }} + renderItem={renderItem} /> diff --git a/code/app/app-rn/src/app/(main)/(tabs)/home/index.tsx b/code/app/app-rn/src/app/(main)/(tabs)/home/index.tsx index 9393be24..f6d87ebd 100644 --- a/code/app/app-rn/src/app/(main)/(tabs)/home/index.tsx +++ b/code/app/app-rn/src/app/(main)/(tabs)/home/index.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { View, Text } from "react-native"; +import { Image, View, Text } from "react-native"; import { PressableFeedback } from "heroui-native"; import { useFamilyStore, useFavoriteStore } from "@/store"; import { loadDataApi } from "@/request/api"; @@ -34,17 +34,17 @@ function SectionLabel({ // ── 组件:宫格项 ────────────────────────────────── function GridItem({ - icon: Icon, + icon, title, onPress, }: { - icon: any; + icon: React.ReactNode; title: string; onPress: () => void; }) { return ( - + {icon} {title} @@ -100,12 +100,12 @@ export default function Home() { {SUB_APPS.map((app) => ( } title={app.title} onPress={() => router.navigate(app.id as any)} /> ))} - router.navigate("/tools")} /> + } title="更多工具" onPress={() => router.navigate("/tools")} /> {/* 填充空位保证对齐 */} {Array.from({ length: Math.max(0, 2 - SUB_APPS.length), diff --git a/code/app/app-rn/src/app/(main)/chat/history.tsx b/code/app/app-rn/src/app/(main)/chat/history.tsx index fda6582c..af528616 100644 --- a/code/app/app-rn/src/app/(main)/chat/history.tsx +++ b/code/app/app-rn/src/app/(main)/chat/history.tsx @@ -21,7 +21,7 @@ const MSG_TYPES = [ { label: "图片", value: ChatMsgType.Image }, { label: "视频", value: ChatMsgType.Video }, { label: "文件", value: ChatMsgType.File }, - { label: "转发", value: ChatMsgType.Forward }, + { label: "卡片", value: ChatMsgType.Forward }, { label: "系统", value: ChatMsgType.System }, ]; diff --git a/code/app/app-rn/src/app/(main)/tools/index.tsx b/code/app/app-rn/src/app/(main)/tools/index.tsx index f517c9c2..99f5740c 100644 --- a/code/app/app-rn/src/app/(main)/tools/index.tsx +++ b/code/app/app-rn/src/app/(main)/tools/index.tsx @@ -1,7 +1,7 @@ import LayoutView from "@/components/layout/view"; import { SearchField, CloseButton, PressableFeedback, useThemeColor } from "heroui-native"; import { toast } from "sonner-native"; -import { View, Text } from "react-native"; +import { Image, View, Text } from "react-native"; import { Stack, useRouter } from "expo-router"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { getGroupedApps, type SubAppItem } from "@/configs/pages"; @@ -109,7 +109,7 @@ export default function Tools() { } }} > - + {/* 收藏分组特殊标记 */} {item.title === "收藏" && ( @@ -129,9 +129,8 @@ export default function Tools() { } return ( - + {item.data.map((subItem) => { - const Icon = subItem.icon; const isFav = favoriteStore.isFavorite(subItem.id); return ( @@ -143,7 +142,7 @@ export default function Tools() { onPress={() => router.navigate(subItem.id as any)} className="flex-row items-center py-3.5 px-4" > - + {subItem.title} 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 e915db66..dafa387b 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 @@ -112,7 +112,7 @@ export default function ClothingHome() { deleteS3FileQuietly(item.image_file_key); if (deleteRelatedOutfits) state.outfits.filter((o) => relatedIds.includes(o.id)).forEach((o) => deleteS3FileQuietly(o.cover_file_key)); await loadBaseData(); - notifyModuleOperation({ module: "衣服搭配", action: "删除衣服", target: item.name, detail: deleteRelatedOutfits ? `同时删除 ${relatedIds.length} 个关联搭配` : undefined, sourceTable: "b_clothing_clothes", sourceId: item.id }); + notifyModuleOperation({ module: "衣服穿搭", action: "删除衣服", target: item.name, detail: deleteRelatedOutfits ? `同时删除 ${relatedIds.length} 个关联搭配` : undefined, sourceTable: "b_clothing_clothes", sourceId: item.id }); stopLoading({ type: "success", message: "已删除" }); } catch { stopLoading({ type: "error", message: "删除失败" }); } }, [deleteClothesBundle, familyId, isLoading, loadBaseData, notifyModuleOperation, startLoading, stopLoading]); 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 78a6e27e..4e55ff78 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 @@ -80,7 +80,7 @@ export default function ClothingOutfitList() { const state = useClothingStore.getState(); const details = state.outfitClothes.filter((r) => r.outfit_id === outfit.id); const ok = await saveOutfitBundle({ outfitDeletes: [{ id: outfit.id }], clothesDeletes: details.map((r) => ({ id: r.id })), tagRelationDeletes: state.tagRelations.filter((r) => r.outfit_id === outfit.id).map((r) => ({ id: r.id })) }); if (!ok) { stopLoading({ type: "error", message: "删除失败" }); return; } - await loadData(); notifyModuleOperation({ module: "衣服搭配", action: "删除搭配", target: outfit.name, detail: `${details.length} 件衣服`, sourceTable: "b_clothing_outfit", sourceId: outfit.id }); + await loadData(); notifyModuleOperation({ module: "衣服穿搭", action: "删除搭配", target: outfit.name, detail: `${details.length} 件衣服`, sourceTable: "b_clothing_outfit", sourceId: outfit.id }); stopLoading({ type: "success", message: "已删除" }); } catch { stopLoading({ type: "error", message: "删除失败" }); } }, [deleteConfirm, familyId, isLoading, loadData, notifyModuleOperation, saveOutfitBundle, startLoading, stopLoading]); 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 55b5242a..f416c097 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 @@ -63,7 +63,7 @@ export default function ClothingDetail() { if (!ok) { stopLoading({ type: "error", message: "删除失败" }); return; } deleteS3FileQuietly(item.image_file_key); state.outfits.filter((o) => relatedIds.includes(o.id)).forEach((o) => deleteS3FileQuietly(o.cover_file_key)); - notifyModuleOperation({ module: "衣服搭配", action: "删除衣服", target: item.name, detail: relatedIds.length > 0 ? `同时删除 ${relatedIds.length} 个关联搭配` : undefined, sourceTable: "b_clothing_clothes", sourceId: item.id }); + notifyModuleOperation({ module: "衣服穿搭", action: "删除衣服", target: item.name, detail: relatedIds.length > 0 ? `同时删除 ${relatedIds.length} 个关联搭配` : undefined, sourceTable: "b_clothing_clothes", sourceId: item.id }); stopLoading({ type: "success", message: "已删除" }); router.replace("/clothes/home" as any); } catch { stopLoading({ type: "error", message: "删除失败" }); } }, [deleteClothesBundle, familyId, isLoading, item, notifyModuleOperation, router, startLoading, stopLoading]); 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 56237291..4d32d306 100644 --- a/code/app/app-rn/src/app/clothes/edit/index.tsx +++ b/code/app/app-rn/src/app/clothes/edit/index.tsx @@ -287,7 +287,7 @@ export default function ClothingEdit() { } await Promise.all([fetchClothes(familyId), fetchTagRelations(familyId)]); notifyModuleOperation({ - module: "衣服搭配", + module: "衣服穿搭", action: isEdit ? "更新衣服" : "新增衣服", target: name, sourceTable: "b_clothing_clothes", 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 4dd7ed86..484d27cf 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 @@ -61,7 +61,7 @@ export default function OutfitDetail() { const d = state.outfitClothes.filter((r) => r.outfit_id === item.id); const ok = await saveOutfitBundle({ outfitDeletes: [{ id: item.id }], clothesDeletes: d.map((r) => ({ id: r.id })), tagRelationDeletes: state.tagRelations.filter((r) => r.outfit_id === item.id).map((r) => ({ id: r.id })) }); if (!ok) { stopLoading({ type: "error", message: "删除失败" }); return; } - notifyModuleOperation({ module: "衣服搭配", action: "删除搭配", target: item.name, detail: `${d.length} 件衣服`, sourceTable: "b_clothing_outfit", sourceId: item.id }); + notifyModuleOperation({ module: "衣服穿搭", action: "删除搭配", target: item.name, detail: `${d.length} 件衣服`, sourceTable: "b_clothing_outfit", sourceId: item.id }); stopLoading({ type: "success", message: "已删除" }); router.replace("/clothes/outfit" as any); } catch { stopLoading({ type: "error", message: "删除失败" }); } }, [familyId, isLoading, item, notifyModuleOperation, router, saveOutfitBundle, startLoading, stopLoading]); 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 e9e1535b..323096a2 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 @@ -648,7 +648,7 @@ export default function ClothingOutfitEdit() { fetchTagRelations(familyId), ]); notifyModuleOperation({ - module: "衣服搭配", + module: "衣服穿搭", action: isEdit ? "更新搭配" : "新增搭配", target: trimmedName, detail: `${sortedItems.length} 件衣服`, diff --git a/code/app/app-rn/src/app/meal/(tabs)/dishes/index.tsx b/code/app/app-rn/src/app/meal/(tabs)/dishes/index.tsx index 3a47cfb5..2080656b 100644 --- a/code/app/app-rn/src/app/meal/(tabs)/dishes/index.tsx +++ b/code/app/app-rn/src/app/meal/(tabs)/dishes/index.tsx @@ -1,9 +1,11 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Dimensions, FlatList, ScrollView, Text, TouchableOpacity, View } from "react-native"; import { router, useFocusEffect } from "expo-router"; +import dayjs from "dayjs"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Button, Dialog, Label, TextField } from "heroui-native"; import { CheckIcon, FilterIcon, PencilIcon, PlusIcon, RotateCcwIcon, Trash2Icon } from "lucide-react-native"; +import { toast } from "sonner-native"; import BottomSheet from "@/components/BottomSheet"; import ContextMenu from "@/components/ContextMenu"; import EmptyState from "@/components/EmptyStatus"; @@ -11,7 +13,7 @@ import LayoutView from "@/components/layout/view"; import MealDishImage from "@/components/meal/MealDishImage"; import { LoadingOverlay } from "@/components/LoadingOverlay"; import { useListLoading, useLoading } from "@/hooks/useLoading"; -import { useFamilyStore, useMealStore } from "@/store"; +import { useFamilyStore, useMealStore, useUserStore } from "@/store"; import { MealDish, MealStatus, MealTag } from "@/types/meal"; // ============================================================ @@ -36,14 +38,20 @@ const MODULE_HEADER_H = 50; // 模块顶部 header 高度(对齐 LayoutView export default function MealDishesPage() { const familyId = useFamilyStore((s) => s.getFamilyId()); + const userId = useUserStore((s) => s.getUserId()); const dishes = useMealStore((s) => s.dishes); const categories = useMealStore((s) => s.categories); const tags = useMealStore((s) => s.tags); const dishTags = useMealStore((s) => s.dishTags); + const slots = useMealStore((s) => s.slots); const fetchDishes = useMealStore((s) => s.fetchDishes); const fetchCategories = useMealStore((s) => s.fetchCategories); const fetchTags = useMealStore((s) => s.fetchTags); const fetchDishTags = useMealStore((s) => s.fetchDishTags); + const fetchSlots = useMealStore((s) => s.fetchSlots); + const fetchPlansByDate = useMealStore((s) => s.fetchPlansByDate); + const createEmptyPlan = useMealStore((s) => s.createEmptyPlan); + const addDishToPlan = useMealStore((s) => s.addDishToPlan); const saveDishChanges = useMealStore((s) => s.saveDishChanges); const { isLoading, startLoading, stopLoading } = useLoading(); const { refreshing, runInitial, runRefresh } = useListLoading(); @@ -51,6 +59,10 @@ export default function MealDishesPage() { const insets = useSafeAreaInsets(); const screenH = Dimensions.get("window").height; + // 日期常量 + const today = dayjs().format("YYYY-MM-DD"); + const tomorrow = dayjs().add(1, "day").format("YYYY-MM-DD"); + // 筛选状态 const [keyword, setKeyword] = useState(""); const [filter, setFilter] = useState(createDefaultFilter); @@ -63,12 +75,17 @@ export default function MealDishesPage() { const [menuPos, setMenuPos] = useState({ x: 0, y: 0 }); const [deleteConfirm, setDeleteConfirm] = useState(null); + // 加入安排 + const [addDishTarget, setAddDishTarget] = useState(null); + const [addDishDate, setAddDishDate] = useState(today); + // 左右两栏可用高度 const bodyH = screenH - insets.top - MODULE_HEADER_H - HEADER_H - TAB_BAR_H - insets.bottom; // ======== 派生数据 ======== const enabledCats = useMemo(() => categories.filter((c) => c.status !== 0), [categories]); + const enabledSlots = useMemo(() => slots.filter((s) => s.status !== 0).sort((a, b) => a.sort_number - b.sort_number), [slots]); const activeFilterCount = useMemo(() => { let c = 0; if (filter.categoryId) c++; if (filter.tagIds.length > 0) c++; return c; @@ -99,8 +116,8 @@ export default function MealDishesPage() { const loadData = useCallback(async () => { if (!familyId) return; - await Promise.all([fetchDishes(familyId), fetchCategories(familyId), fetchTags(familyId), fetchDishTags(familyId)]); - }, [familyId, fetchCategories, fetchDishTags, fetchDishes, fetchTags]); + await Promise.all([fetchDishes(familyId), fetchCategories(familyId), fetchTags(familyId), fetchDishTags(familyId), fetchSlots(familyId)]); + }, [familyId, fetchCategories, fetchDishTags, fetchDishes, fetchTags, fetchSlots]); useEffect(() => { if (familyId) runInitial(loadData); }, [familyId]); useFocusEffect(useCallback(() => { if (familyId) runRefresh(loadData); }, [familyId, loadData, runRefresh])); @@ -132,6 +149,50 @@ export default function MealDishesPage() { return setDraftFilter((p) => ({ ...p, tagIds: p.tagIds.includes(t.id) ? p.tagIds.filter((id) => id !== t.id) : [...p.tagIds, t.id] }))}>{t.name}; }, [draftFilter.tagIds]); + // ======== 加入安排 ======== + + const handleAddToSlot = useCallback(async (slotId: number, slotName: string) => { + if (!familyId || !userId || !addDishTarget) return; + const dish = addDishTarget; + const date = addDishDate; + setAddDishTarget(null); + startLoading("添加中..."); + try { + // 获取该日期的安排 + await fetchPlansByDate(familyId, date); + const plans = useMealStore.getState().plans; + let planId = plans.find((p) => p.meal_slot_id === slotId)?.id; + + // 没有安排则创建 + if (!planId) { + planId = await createEmptyPlan({ familyId, planDate: date, mealSlotId: slotId, userId }); + } + if (!planId) { + stopLoading({ type: "error", message: "创建安排失败" }); + return; + } + + // 添加菜品 + const result = await addDishToPlan({ + familyId, + planId, + dishId: dish.id, + dishName: dish.name, + userId, + sortNumber: 0, + }); + if (result.planItemId) { + const dateLabel = date === today ? "今天" : date === tomorrow ? "明天" : date; + toast.success(`已加入${dateLabel}${slotName}`); + stopLoading(); + } else { + stopLoading({ type: "error", message: "添加失败" }); + } + } catch { + stopLoading({ type: "error", message: "添加失败" }); + } + }, [familyId, userId, addDishTarget, addDishDate, today, tomorrow, fetchPlansByDate, createEmptyPlan, addDishToPlan, startLoading, stopLoading]); + // ======== 渲染 ======== return ( @@ -227,6 +288,14 @@ export default function MealDishesPage() { {!!item.cooking_time_minutes && 约{item.cooking_time_minutes}分钟} + { setAddDishDate(today); setAddDishTarget(item); }} + > + + {">"} )} @@ -250,6 +319,52 @@ export default function MealDishesPage() { + {/* ======== 加入安排 BottomSheet ======== */} + { if (!v) setAddDishTarget(null); }}> + + + + 加入安排{addDishTarget ? ` · ${addDishTarget.name}` : ""} + + + {/* 日期选择 */} + + setAddDishDate(today)} + > + 今天 + + setAddDishDate(tomorrow)} + > + 明天 + + + + {/* 餐次选择 */} + + 选择餐次 + {enabledSlots.length > 0 ? ( + + {enabledSlots.map((slot) => ( + handleAddToSlot(slot.id, slot.name)} + > + {slot.name} + + ))} + + ) : ( + 暂无餐次,请先在设置中添加 + )} + + + + {/* ======== 长按菜单 ======== */} setActiveItem(null)} items={[ diff --git a/code/app/app-rn/src/app/meal/dish/edit/index.tsx b/code/app/app-rn/src/app/meal/dish/edit/index.tsx index b3f93b07..96ba825e 100644 --- a/code/app/app-rn/src/app/meal/dish/edit/index.tsx +++ b/code/app/app-rn/src/app/meal/dish/edit/index.tsx @@ -110,14 +110,14 @@ export default function MealDishEdit() { if (isEdit) { const ok = await saveDishChanges({ updates: [{ id: editId, name: form.name.trim(), image_file_key: imageFileKey, category_id: form.categoryId, difficulty: form.difficulty, cooking_time_minutes: form.cookingTime ? Number(form.cookingTime) : 0, remark: form.remark.trim() }] }); if (!ok) { stopLoading({ type: "error", message: "保存失败" }); return; } - notifyOp({ module: "餐食", action: "编辑菜品", target: form.name.trim(), sourceTable: "b_meal_dish", sourceId: editId }); + notifyOp({ module: "家庭菜单", action: "编辑菜品", target: form.name.trim(), sourceTable: "b_meal_dish", sourceId: editId }); } else { const sc = steps.length + 1; const idsRes = await getUniqueIdApi(sc); const dishId = idsRes.data?.[0] ?? 0; if (!dishId) { stopLoading({ type: "error", message: "创建失败" }); return; } const sids2 = idsRes.data?.slice(1) ?? []; const sis2 = steps.map((s, i) => ({ id: sids2[i] ?? 0, family_id: familyId, dish_id: dishId, step_number: s.stepNumber, description: s.description, image_file_key: s.imageFileKey, sort_number: i })); const ok = await saveDishBundle({ dishInserts: [{ id: dishId, family_id: familyId, name: form.name.trim(), image_file_key: imageFileKey, category_id: form.categoryId, difficulty: form.difficulty, cooking_time_minutes: form.cookingTime ? Number(form.cookingTime) : 0, remark: form.remark.trim(), status: MealStatus.Enabled, sort_number: 0 }], dishStepInserts: sis2 }); if (!ok) { stopLoading({ type: "error", message: "保存失败" }); return; } - notifyOp({ module: "餐食", action: "新增菜品", target: form.name.trim(), sourceTable: "b_meal_dish", sourceId: dishId }); + notifyOp({ module: "家庭菜单", action: "新增菜品", target: form.name.trim(), sourceTable: "b_meal_dish", sourceId: dishId }); } stopLoading({ type: "success", message: "已保存" }); router.back(); diff --git a/code/app/app-rn/src/app/meal/plan/detail/[id]/index.tsx b/code/app/app-rn/src/app/meal/plan/detail/[id]/index.tsx index cf44e1e4..93182239 100644 --- a/code/app/app-rn/src/app/meal/plan/detail/[id]/index.tsx +++ b/code/app/app-rn/src/app/meal/plan/detail/[id]/index.tsx @@ -357,7 +357,7 @@ export default function MealPlanDetail() { setComment(""); await loadData(); notifyOp({ - module: "餐食", + module: "家庭菜单", action: "评论菜单", target: planId.toString(), sourceTable: "b_meal_plan_log", diff --git a/code/app/app-rn/src/app/meal/plan/edit/index.tsx b/code/app/app-rn/src/app/meal/plan/edit/index.tsx index b075819d..acff4939 100644 --- a/code/app/app-rn/src/app/meal/plan/edit/index.tsx +++ b/code/app/app-rn/src/app/meal/plan/edit/index.tsx @@ -120,7 +120,7 @@ export default function MealPlanEdit() { } notifyOp({ - module: "餐食", + module: "家庭菜单", action: "创建菜单", target: `${planDate} ${slots.find((s) => s.id === mealSlotId)?.name ?? ""}`, sourceTable: "b_meal_plan", diff --git a/code/app/app-rn/src/configs/pages.ts b/code/app/app-rn/src/configs/pages.ts index e979b83e..d3fd7bf0 100644 --- a/code/app/app-rn/src/configs/pages.ts +++ b/code/app/app-rn/src/configs/pages.ts @@ -1,9 +1,12 @@ -import { IconAppFinance, IconAppClothes, IconAppMeal, IconAppTodo, IconAppAnniversary } from "@/components/iconfont"; +import type { ImageSourcePropType } from "react-native"; +import JizhangIcon from "@/static/images/subapps/jizhang.webp"; +import CaidanIcon from "@/static/images/subapps/caidan.webp"; +import YifudapeiIcon from "@/static/images/subapps/yifudapei.webp"; export interface SubAppItem { id: string; title: string; - icon: any; + icon: ImageSourcePropType; group: string; description?: string; } @@ -25,21 +28,21 @@ export const SUB_APPS: SubAppItem[] = [ { id: "/finance/home", title: "记账", - icon: IconAppFinance, + icon: JizhangIcon, group: "J", description: "记录日常收支,管理你的每一笔钱", }, { id: "/clothes/home", - title: "衣服搭配", - icon: IconAppClothes, + title: "衣服穿搭", + icon: YifudapeiIcon, group: "Y", description: "帮你整理每日穿搭灵感", }, { id: "/meal/home", - title: "餐食", - icon: IconAppMeal, + title: "家庭菜单", + icon: CaidanIcon, group: "J", description: "家庭菜品库与餐食安排", }, @@ -47,7 +50,7 @@ export const SUB_APPS: SubAppItem[] = [ // { // id: "/todo/home", // title: "家庭待办", - // icon: IconAppTodo, + // icon: require("@/static/images/subapps/daiban.webp"), // group: "J", // description: "家庭任务分配与跟踪", // }, @@ -55,7 +58,7 @@ export const SUB_APPS: SubAppItem[] = [ // { // id: "/anniversary/home", // title: "纪念日", - // icon: IconAppAnniversary, + // icon: require("@/static/images/subapps/jinianri.webp"), // group: "J", // description: "记录家人的生日与重要纪念日", // }, diff --git a/code/app/app-rn/src/hooks/useModuleChatNotifier.ts b/code/app/app-rn/src/hooks/useModuleChatNotifier.ts index 509748b9..7884af54 100644 --- a/code/app/app-rn/src/hooks/useModuleChatNotifier.ts +++ b/code/app/app-rn/src/hooks/useModuleChatNotifier.ts @@ -24,7 +24,7 @@ export function useModuleChatNotifier() { chat.send({ type: "send", - msg_type: ChatMsgType.System, + msg_type: ChatMsgType.Forward, content, source_table: sourceTable, source_id: sourceId, diff --git a/code/app/app-rn/src/static/images/subapps/caidan.png b/code/app/app-rn/src/static/images/subapps/caidan.png new file mode 100644 index 00000000..b169d3a2 Binary files /dev/null and b/code/app/app-rn/src/static/images/subapps/caidan.png differ diff --git a/code/app/app-rn/src/static/images/subapps/caidan.webp b/code/app/app-rn/src/static/images/subapps/caidan.webp new file mode 100644 index 00000000..b5e4232b Binary files /dev/null and b/code/app/app-rn/src/static/images/subapps/caidan.webp differ diff --git a/code/app/app-rn/src/static/images/subapps/jizhang.png b/code/app/app-rn/src/static/images/subapps/jizhang.png new file mode 100644 index 00000000..fe046702 Binary files /dev/null and b/code/app/app-rn/src/static/images/subapps/jizhang.png differ diff --git a/code/app/app-rn/src/static/images/subapps/jizhang.webp b/code/app/app-rn/src/static/images/subapps/jizhang.webp new file mode 100644 index 00000000..00609171 Binary files /dev/null and b/code/app/app-rn/src/static/images/subapps/jizhang.webp differ diff --git a/code/app/app-rn/src/static/images/subapps/yifudapei.png b/code/app/app-rn/src/static/images/subapps/yifudapei.png new file mode 100644 index 00000000..5471785b Binary files /dev/null and b/code/app/app-rn/src/static/images/subapps/yifudapei.png differ diff --git a/code/app/app-rn/src/static/images/subapps/yifudapei.webp b/code/app/app-rn/src/static/images/subapps/yifudapei.webp new file mode 100644 index 00000000..cae0f9ff Binary files /dev/null and b/code/app/app-rn/src/static/images/subapps/yifudapei.webp differ diff --git a/code/app/app-rn/src/types/chat.ts b/code/app/app-rn/src/types/chat.ts index d576d02a..b9a97cd4 100644 --- a/code/app/app-rn/src/types/chat.ts +++ b/code/app/app-rn/src/types/chat.ts @@ -8,9 +8,9 @@ export enum ChatMsgType { Video = 2, /** 文件 */ File = 3, - /** 转发(模块内容分享) */ + /** 模块卡片(记账、待办、餐食等操作动态,以卡片形式展示) */ Forward = 4, - /** 系统消息(加入家庭、改名、记账、收藏等) */ + /** 系统消息(加入家庭、改名等纯通知) */ System = 5, } diff --git a/code/app/app-rn/src/types/global.d.ts b/code/app/app-rn/src/types/global.d.ts index 1ee6356a..07daad06 100644 --- a/code/app/app-rn/src/types/global.d.ts +++ b/code/app/app-rn/src/types/global.d.ts @@ -1,5 +1,23 @@ export {}; +declare module "@/static/images/subapps/jizhang.webp" { + import { ImageSourcePropType } from "react-native"; + const content: ImageSourcePropType; + export default content; +} + +declare module "@/static/images/subapps/caidan.webp" { + import { ImageSourcePropType } from "react-native"; + const content: ImageSourcePropType; + export default content; +} + +declare module "@/static/images/subapps/yifudapei.webp" { + import { ImageSourcePropType } from "react-native"; + const content: ImageSourcePropType; + export default content; +} + declare global { enum HttpReponseCode { Success = 1000, diff --git a/code/app/app-rn/tsconfig.json b/code/app/app-rn/tsconfig.json index afa1230b..ddb8498b 100644 --- a/code/app/app-rn/tsconfig.json +++ b/code/app/app-rn/tsconfig.json @@ -11,6 +11,7 @@ "**/*.ts", "**/*.tsx", "./src/uniwind-types.d.ts", + "./src/types/global.d.ts", "env.js", ".expo/types/**/*.ts", "expo-env.d.ts",