diff --git a/src/api/family.ts b/src/api/family.ts new file mode 100644 index 0000000..dc8a847 --- /dev/null +++ b/src/api/family.ts @@ -0,0 +1,190 @@ +import { storage } from '@/utils/storage' +import { + FAMILY_DATA_STORAGE_KEY, + MAX_CARED_PER_FAMILY, + MOCK_ID_SEED +} from '@/constants/family' + +export interface Family { + id: string + name: string + memberCount: number + caredCount: number + caredLabel: string + address: string + deviceCount: number +} + +export interface EmergencyContact { + id: string + name: string + phone: string + relation: string +} + +export interface CaredPerson { + id: string + familyId: string + name: string + idNumber: string + address: string + contacts: EmergencyContact[] +} + +export interface FamilyMember { + id: string + familyId: string + name: string + role: 'owner' | 'member' + phone: string + notify: boolean +} + +export interface FamilyDetailData { + family: Family + members: FamilyMember[] + cared: CaredPerson[] +} + +interface FamilyData { + families: Family[] + cared: CaredPerson[] + members: FamilyMember[] + nextId: number +} + +const SEED_DATA: FamilyData = { + families: [ + { id: '846443196293189', name: 'JollyPlace', memberCount: 6, caredCount: 1, caredLabel: 'Jay', address: '--', deviceCount: 24 }, + { id: '735201884550361', name: '我的家庭圈', memberCount: 4, caredCount: 0, caredLabel: '--', address: '--', deviceCount: 1 }, + { id: '628934071266852', name: '康乐之家', memberCount: 1, caredCount: 0, caredLabel: '--', address: '--', deviceCount: 21 }, + { id: '519027468331740', name: '平安庭院', memberCount: 1, caredCount: 0, caredLabel: '--', address: '--', deviceCount: 0 } + ], + cared: [ + { + id: '900001', + familyId: '846443196293189', + name: 'Jay', + idNumber: '440301195603150000', + address: '深圳市南山区科技园', + contacts: [ + { id: '910001', name: '张女士', phone: '13800138000', relation: '配偶' }, + { id: '910002', name: '李先生', phone: '13900139000', relation: '儿子' } + ] + } + ], + members: [ + { id: '900001', familyId: '846443196293189', name: 'Jay', role: 'owner', phone: '13800138000', notify: true }, + { id: '900002', familyId: '846443196293189', name: '小美', role: 'member', phone: '13900139000', notify: false } + ], + nextId: MOCK_ID_SEED +} + +// 读全量数据;storage 为空时写入样例(首次启动初始化) +function loadData(): FamilyData { + const saved = storage.get(FAMILY_DATA_STORAGE_KEY) + if (saved) { + if (!saved.families || !Array.isArray(saved.families)) { + console.warn('familyData 存储结构异常,回退默认数据') + return resetData() + } + return saved + } + return resetData() +} + +function resetData(): FamilyData { + const data: FamilyData = JSON.parse(JSON.stringify(SEED_DATA)) + storage.set(FAMILY_DATA_STORAGE_KEY, data) + return data +} + +function saveData(data: FamilyData): void { + storage.set(FAMILY_DATA_STORAGE_KEY, data) +} + +// mock 异步语义:与真实 API 调用形式一致 +const delay = (value: T, ms = 100): Promise => + new Promise((resolve) => setTimeout(() => resolve(value), ms)) + +function nextIdOf(data: FamilyData): string { + const id = String(data.nextId) + data.nextId += 1 + return id +} + +export async function getFamilies(): Promise { + return delay(loadData().families) +} + +export async function getFamilyDetail(id: string): Promise { + const data = loadData() + const family = data.families.find((f) => f.id === id) + if (!family) return delay(null) + const members = data.members.filter((m) => m.familyId === id) + const cared = data.cared.filter((c) => c.familyId === id) + return delay({ family, members, cared }) +} + +export async function addFamily(name: string): Promise { + const trimmed = name.trim() + if (!trimmed) throw new Error('请输入家庭名') + const data = loadData() + const family: Family = { + id: nextIdOf(data), + name: trimmed, + memberCount: 1, + caredCount: 0, + caredLabel: '--', + address: '--', + deviceCount: 0 + } + data.families = [...data.families, family] + saveData(data) + return delay(family) +} + +export async function joinFamily(code: string): Promise { + const trimmed = code.trim() + if (!trimmed) throw new Error('请输入家庭编号') + const data = loadData() + const family: Family = { + id: nextIdOf(data), + name: `加入的 ${trimmed.slice(-4)}`, + memberCount: 2, + caredCount: 0, + caredLabel: '--', + address: '--', + deviceCount: 0 + } + data.families = [...data.families, family] + saveData(data) + return delay(family) +} + +export async function canAddCared(familyId: string): Promise { + const data = loadData() + const count = data.cared.filter((c) => c.familyId === familyId).length + return count < MAX_CARED_PER_FAMILY +} + +export async function addCared(input: Omit): Promise { + const data = loadData() + const count = data.cared.filter((c) => c.familyId === input.familyId).length + if (count >= MAX_CARED_PER_FAMILY) throw new Error('已达上限') + + const person: CaredPerson = { ...input, id: nextIdOf(data) } + data.cared = [...data.cared, person] + // 同步家庭卡上的关爱对象摘要 + const family = data.families.find((f) => f.id === input.familyId) + if (family) { + family.caredCount = count + 1 + family.caredLabel = person.name + } + saveData(data) + return delay(person) +} + +export async function getCaredByFamily(familyId: string): Promise { + return delay(loadData().cared.filter((c) => c.familyId === familyId)) +} diff --git a/tests/unit/family/family.spec.ts b/tests/unit/family/family.spec.ts new file mode 100644 index 0000000..2974a1a --- /dev/null +++ b/tests/unit/family/family.spec.ts @@ -0,0 +1,114 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' + +// mock storage:内存 Map 语义,与 Taro.setStorageSync/getStorageSync 等价 +const store = new Map() +vi.mock('@/utils/storage', () => ({ + storage: { + get: (key: string): T | null => (store.has(key) ? (store.get(key) as T) : null), + set: (key: string, value: T): void => { store.set(key, value) }, + remove: (key: string): void => { store.delete(key) } + } +})) + +import { + getFamilies, + getFamilyDetail, + addFamily, + joinFamily, + canAddCared, + addCared, + getCaredByFamily, + type CaredPerson +} from '@/api/family' +import { FAMILY_DATA_STORAGE_KEY } from '@/constants/family' + +const caredInput = (over: Partial = {}): Omit => ({ + familyId: '735201884550361', + name: '王奶奶', + idNumber: '440301194001010000', + address: '深圳市南山区', + contacts: [], + ...over +}) + +beforeEach(() => { + store.clear() +}) + +describe('家庭圈 mock 数据层', () => { + it('首次调用 getFamilies 写入样例并返回 4 条', async () => { + const list = await getFamilies() + expect(list).toHaveLength(4) + expect(store.has(FAMILY_DATA_STORAGE_KEY)).toBe(true) + }) + + it('addFamily 追加家庭且持久化,caredCount 从 0 开始', async () => { + await getFamilies() + const f = await addFamily('测试家庭') + expect(f.name).toBe('测试家庭') + expect(f.caredCount).toBe(0) + const again = await getFamilies() + expect(again).toHaveLength(5) + expect(again.some((x) => x.id === f.id)).toBe(true) + }) + + it('addFamily 空名抛错', async () => { + await getFamilies() + await expect(addFamily(' ')).rejects.toThrow('请输入家庭名') + }) + + it('joinFamily 按编号生成新家庭', async () => { + await getFamilies() + const f = await joinFamily('1234567890') + expect(f.name).toBe('加入的 7890') + const again = await getFamilies() + expect(again).toHaveLength(5) + }) + + it('getFamilyDetail 返回家庭 + 成员 + 关爱对象', async () => { + await getFamilies() + const detail = await getFamilyDetail('846443196293189') + expect(detail.family.name).toBe('JollyPlace') + expect(detail.members.length).toBe(2) + expect(detail.cared.length).toBe(1) + expect(detail.cared[0].name).toBe('Jay') + }) + + it('getFamilyDetail 不存在返回 null', async () => { + await getFamilies() + expect(await getFamilyDetail('nope')).toBeNull() + }) + + it('canAddCared 未达上限 true,加满后 false', async () => { + await getFamilies() + // JollyPlace 已有 Jay(1/2),仍未达上限 + expect(await canAddCared('846443196293189')).toBe(true) + expect(await canAddCared('735201884550361')).toBe(true) + await addCared(caredInput()) + await addCared(caredInput({ name: '李爷爷' })) + expect(await canAddCared('735201884550361')).toBe(false) + }) + + it('addCared 写入并同步家庭 caredCount/caredLabel', async () => { + await getFamilies() + const saved = await addCared(caredInput()) + expect(saved.id).toBeTruthy() + const detail = await getFamilyDetail('735201884550361') + expect(detail.family.caredCount).toBe(1) + expect(detail.family.caredLabel).toBe('王奶奶') + expect(detail.cared[0].contacts).toEqual([]) + }) + + it('addCared 达上限抛错', async () => { + await getFamilies() + await addCared(caredInput()) + await addCared(caredInput({ name: '李爷爷' })) + await expect(addCared(caredInput({ name: '第三人' }))).rejects.toThrow('已达上限') + }) + + it('getCaredByFamily 返回该家庭的关爱对象数组', async () => { + await getFamilies() + const list = await getCaredByFamily('846443196293189') + expect(list.map((c) => c.name)).toEqual(['Jay']) + }) +})