diff --git a/src/api/auth.ts b/src/api/auth.ts index f18d1cc..6a988de 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,4 +1,5 @@ import { request } from '@/http' +import { USER, THIRD_PLATFORM } from '@/constants/endpoints' // 微信小程序登录请求参数(2026-09-14 文档版,openId 为二次登录回传字段) export interface WxMiniappLoginParams { @@ -43,7 +44,7 @@ export type WxMiniappLoginResponse = WxLoginResult & export function wxMiniappLogin(params: WxMiniappLoginParams): Promise { return request.post( - '/thirdPlatform/wxDevLogin/miniapp/login', + THIRD_PLATFORM.wxMiniappLogin, params as unknown as Record, { needAuth: false } ) @@ -52,5 +53,5 @@ export function wxMiniappLogin(params: WxMiniappLoginParams): Promise)联调实测后修正字段 export function getLoginInfo(): Promise { - return request.get('/v0/app/users/getLoginInfo') + return request.get(USER.getLoginInfo) } diff --git a/src/api/careMember.ts b/src/api/careMember.ts index 0e75d9f..e902ac8 100644 --- a/src/api/careMember.ts +++ b/src/api/careMember.ts @@ -1,4 +1,5 @@ import { request } from '@/http' +import { CARE_MEMBER } from '@/constants/endpoints' // 关爱对象接口(apifox 2026-09-11 文档版) @@ -30,17 +31,17 @@ export interface CareMember { } export function saveCareMember(params: CareMemberSaveParams): Promise { - return request.post('/yanglao/app/ylHomeCareMember/save', params as unknown as Record) + return request.post(CARE_MEMBER.save, params as unknown as Record) } export function getCareMemberList(homeId: string): Promise { - return request.post('/yanglao/app/ylHomeCareMember/getList', { homeId }) + return request.post(CARE_MEMBER.getList, { homeId }) } export function getCareMemberDetail(memberId: string): Promise { - return request.get(`/yanglao/app/ylHomeCareMember/get/${memberId}`) + return request.get(CARE_MEMBER.getDetail(memberId)) } export function deleteCareMember(memberId: string): Promise { - return request.post('/yanglao/app/ylHomeCareMember/delete', { memberId }) + return request.post(CARE_MEMBER.delete, { memberId }) } diff --git a/src/api/home.ts b/src/api/home.ts index 329f5c5..15bf794 100644 --- a/src/api/home.ts +++ b/src/api/home.ts @@ -1,4 +1,5 @@ import { request } from '@/http' +import { HOME } from '@/constants/endpoints' // 家庭圈接口(apifox 2026-09-11 文档版) // 列表字段已按 2026-09-17 联调实测修正 @@ -25,38 +26,42 @@ export interface HomeMember { export function addHome(name: string): Promise { // name 可空,空则后端默认「我的家庭圈」(文档 YlHomeAddReq) - return request.post('/yanglao/app/ylHome/add', { name }) + return request.post(HOME.add, { name }) } export function updateHome(params: Record): Promise { - return request.post('/yanglao/app/ylHome/update', params) + return request.post(HOME.update, params) } export function dissolveHome(params: Record): Promise { - return request.post('/yanglao/app/ylHome/dissolve', params) + return request.post(HOME.dissolve, params) } export function getHomeList(): Promise { - return request.post('/yanglao/app/ylHome/getList') + return request.post(HOME.getList) } export function getHomeMemberList(homeId: string): Promise { - return request.post('/yanglao/app/ylHome/getMemberList', { homeId }) + return request.post(HOME.getMemberList, { homeId }) } -export function transferOwner(params: Record): Promise { - return request.post('/yanglao/app/ylHome/transferOwner', params) +export interface TransferOwnerParams { + memberId: string +} + +export function transferOwner(params: TransferOwnerParams): Promise { + return request.post(HOME.transferOwner, params) } export function updateMemberNickName(params: Record): Promise { - return request.post('/yanglao/app/ylHome/updateMemberNickName', params) + return request.post(HOME.updateMemberNickName, params) } export function getHomeDetail(homeId: string): Promise { - return request.get(`/yanglao/app/ylHome/get/${homeId}`) + return request.get(HOME.getDetail(homeId)) } export function joinHome(code: string): Promise { // code 为家庭编号(文档 YlHomeJoinReq) - return request.post('/yanglao/app/ylHome/join', { code }) + return request.post(HOME.join, { code }) } diff --git a/src/api/homeAlarm.ts b/src/api/homeAlarm.ts index 55a7eb2..8a056d4 100644 --- a/src/api/homeAlarm.ts +++ b/src/api/homeAlarm.ts @@ -1,4 +1,5 @@ import { request } from '@/http' +import { HOME_ALARM } from '@/constants/endpoints' // 入参对齐 apifox YlHomeAlarmListReq(homeId 必填,其余可选筛选) export interface AlarmListReq { @@ -24,9 +25,9 @@ export interface HomeAlarm { } export function getAlarmList(params: AlarmListReq): Promise { - return request.post('/yanglao/app/ylHomeAlarm/getList', params as unknown as Record) + return request.post(HOME_ALARM.getList, params as unknown as Record) } export function getAlarmDetail(alarmId: string): Promise { - return request.get(`/yanglao/app/ylHomeAlarm/get/${alarmId}`) + return request.get(HOME_ALARM.getDetail(alarmId)) } diff --git a/src/api/homeDevice.ts b/src/api/homeDevice.ts index ca5fba7..dda2b36 100644 --- a/src/api/homeDevice.ts +++ b/src/api/homeDevice.ts @@ -1,4 +1,5 @@ import { request } from '@/http' +import { HOME_DEVICE } from '@/constants/endpoints' // 入参对齐 apifox YlHomeDeviceListReq:homeId 与 roomId 至少填一个 export interface HomeDeviceListReq { @@ -30,11 +31,15 @@ export interface VitalSigns { bodyMovement?: number } -// ED719 跌倒检测快照(2026-09-21 实测:status 枚举见 FALL_STATUS_MAP;version 为固件版本) +// ED719 跌倒检测快照(2026-09-22 实测:alarmType 是实时跌倒状态枚举,编码同 FALL_STATUS_MAP; +// alarmTs 是事件 Unix 秒;version 为固件版本) export interface FallDetectSigns { lastTime?: string - status?: number + alarmType?: number + alarmTs?: number version?: string + // 兼容旧字段名(detail 页若只取到 status 也能渲染) + status?: number } // 响应字段按 2026-09-17 联调实测 @@ -57,12 +62,12 @@ export interface HomeDevice { } export function getHomeDeviceList(params: HomeDeviceListReq): Promise { - return request.post('/yanglao/app/ylHomeDevice/getList', params as unknown as Record) + return request.post(HOME_DEVICE.getList, params as unknown as Record) } // 单设备详情(GET + 路径参数,含 runTimeInfo 实时快照) export function getHomeDeviceDetail(installId: string): Promise { - return request.get(`/yanglao/app/ylHomeDevice/get/${installId}`) + return request.get(HOME_DEVICE.getDetail(installId)) } // —— 历史体征记录(web 端 IotPropertyRecord 同源接口,2026-09-18 实测)—— @@ -105,13 +110,13 @@ export function getVitalRecords(deviceName: string, pageSize = 50): Promise) + return request.post(HOME_DEVICE.getPropertyRecords, params as unknown as Record) } export function addHomeDevice(params: HomeDeviceAddReq): Promise { - return request.post('/yanglao/app/ylHomeDevice/add', params as unknown as Record) + return request.post(HOME_DEVICE.add, params as unknown as Record) } export function unbindHomeDevice(params: HomeDeviceUnbindReq): Promise { - return request.post('/yanglao/app/ylHomeDevice/unbind', params as unknown as Record) + return request.post(HOME_DEVICE.unbind, params as unknown as Record) } diff --git a/src/api/homeRoom.ts b/src/api/homeRoom.ts index f5383a0..5e85e9f 100644 --- a/src/api/homeRoom.ts +++ b/src/api/homeRoom.ts @@ -1,4 +1,5 @@ import { request } from '@/http' +import { HOME_ROOM } from '@/constants/endpoints' // 房间使用场景枚举(apifox YlHomeRoomAddReq.sence 推荐值) export const ROOM_SENCE_MAP: Record = { @@ -45,17 +46,17 @@ export interface HomeRoom { // 文档路径为 POST + body { homeId }(YlHomeRoomListReq) export function getRoomList(homeId: string): Promise { - return request.post('/yanglao/app/ylHomeRoom/getList', { homeId }) + return request.post(HOME_ROOM.getList, { homeId }) } export function addRoom(params: HomeRoomAddReq): Promise { - return request.post('/yanglao/app/ylHomeRoom/add', params as unknown as Record) + return request.post(HOME_ROOM.add, params as unknown as Record) } export function updateRoom(params: HomeRoomUpdateReq): Promise { - return request.post('/yanglao/app/ylHomeRoom/update', params as unknown as Record) + return request.post(HOME_ROOM.update, params as unknown as Record) } export function deleteRoom(params: HomeRoomDeleteReq): Promise { - return request.post('/yanglao/app/ylHomeRoom/delete', params as unknown as Record) + return request.post(HOME_ROOM.delete, params as unknown as Record) } diff --git a/src/constants/endpoints.ts b/src/constants/endpoints.ts new file mode 100644 index 0000000..7211698 --- /dev/null +++ b/src/constants/endpoints.ts @@ -0,0 +1,65 @@ +// 后端 API 路径集中表。集中维护便于: +// 1) 环境切换时只改本文件 +// 2) 排查路由/联调时一处定位所有端点 +// 3) 后续按网关/服务前缀批量调整 + +// —— 家庭 ylHome —— +export const HOME = { + add: '/yanglao/app/ylHome/add', + update: '/yanglao/app/ylHome/update', + dissolve: '/yanglao/app/ylHome/dissolve', + getList: '/yanglao/app/ylHome/getList', + getMemberList: '/yanglao/app/ylHome/getMemberList', + transferOwner: '/yanglao/app/ylHome/transferOwner', + updateMemberNickName: '/yanglao/app/ylHome/updateMemberNickName', + getDetail: (homeId: string): string => `/yanglao/app/ylHome/get/${homeId}`, + join: '/yanglao/app/ylHome/join' +} + +// —— 关爱对象 ylHomeCareMember —— +export const CARE_MEMBER = { + save: '/yanglao/app/ylHomeCareMember/save', + getList: '/yanglao/app/ylHomeCareMember/getList', + getDetail: (memberId: string): string => `/yanglao/app/ylHomeCareMember/get/${memberId}`, + delete: '/yanglao/app/ylHomeCareMember/delete' +} + +// —— 房间 ylHomeRoom —— +export const HOME_ROOM = { + getList: '/yanglao/app/ylHomeRoom/getList', + add: '/yanglao/app/ylHomeRoom/add', + update: '/yanglao/app/ylHomeRoom/update', + delete: '/yanglao/app/ylHomeRoom/delete' +} + +// —— 设备 ylHomeDevice —— +export const HOME_DEVICE = { + getList: '/yanglao/app/ylHomeDevice/getList', + getDetail: (installId: string): string => `/yanglao/app/ylHomeDevice/get/${installId}`, + getPropertyRecords: '/iot/propertyRecord/getPropertyRecords', + add: '/yanglao/app/ylHomeDevice/add', + unbind: '/yanglao/app/ylHomeDevice/unbind' +} + +// —— 告警 ylHomeAlarm —— +export const HOME_ALARM = { + getList: '/yanglao/app/ylHomeAlarm/getList', + getDetail: (alarmId: string): string => `/yanglao/app/ylHomeAlarm/get/${alarmId}` +} + +// —— 用户 v0/app/users —— +export const USER = { + getLoginInfo: '/v0/app/users/getLoginInfo' +} + +// —— 第三方平台登录 thirdPlatform/wxDevLogin —— +export const THIRD_PLATFORM = { + wxMiniappLogin: '/thirdPlatform/wxDevLogin/miniapp/login' +} + +// 网关分组前缀:用于 client.ts 决定走 user 服务还是 biz 服务网关 +// user 服务网关:登录 + 第三方平台 + 用户中心(/v0/、/thirdPlatform/) +// biz 服务网关:所有业务接口(/yanglao/、/iot/) +export const GATEWAY_PREFIX = { + user: ['/thirdPlatform/', '/v0/'] +} diff --git a/src/http/client.ts b/src/http/client.ts index d759e61..0fd37c5 100644 --- a/src/http/client.ts +++ b/src/http/client.ts @@ -1,6 +1,7 @@ import Taro from '@tarojs/taro' import { store } from '@/store' import { clearAuth } from '@/store/authSlice' +import { GATEWAY_PREFIX } from '@/constants/endpoints' import { ROUTES } from '@/constants/routes' import { reLaunch } from '@/utils/navigation' import { ApiError, BusinessError, AuthExpiredError } from './errors' @@ -73,7 +74,7 @@ export async function request( } // 登录接口与 users 系列冷启动校验接口走 user 服务前缀,其余业务接口走 biz 前缀 - const base = url.startsWith('/thirdPlatform/') || url.startsWith('/v0/') ? USER_BASE : BIZ_BASE + const base = GATEWAY_PREFIX.user.some((p) => url.startsWith(p)) ? USER_BASE : BIZ_BASE let res: Taro.request.SuccessCallbackResult> try { @@ -140,8 +141,8 @@ function pickResponseToken(header: Record | undefined): string return raw } -request.get = (url: string, params?: Record, config?: RequestConfig) => - request(url, params, { ...config, method: 'GET' }) +request.get = (url: string, params?: object, config?: RequestConfig) => + request(url, params as Record | undefined, { ...config, method: 'GET' }) -request.post = (url: string, params?: Record, config?: RequestConfig) => - request(url, params, { ...config, method: 'POST' }) +request.post = (url: string, params?: object, config?: RequestConfig) => + request(url, params as Record | undefined, { ...config, method: 'POST' }) diff --git a/src/pages/device/feature/detail.tsx b/src/pages/device/feature/detail.tsx index f5290fa..a058add 100644 --- a/src/pages/device/feature/detail.tsx +++ b/src/pages/device/feature/detail.tsx @@ -147,6 +147,7 @@ export default function Detail() { {isFallDetector ? ( = { } // ED719 跌倒检测状态卡:实时状态大卡(参考 web 端 DeviceScgDashboard DeviceItem 布局) -export function FallStatusCard({ status, lastTime, version, online }: Props) { - const label = FALL_STATUS_MAP[status ?? -1] ?? (online ? '正常' : '离线') - const level = STATUS_CLS[status ?? -1] ?? (online ? 'normal' : 'offline') +export function FallStatusCard({ alarmType, status, lastTime, version, online }: Props) { + const code = alarmType ?? status + const label = FALL_STATUS_MAP[code ?? -1] ?? (online ? '正常' : '离线') + const level = STATUS_CLS[code ?? -1] ?? (online ? 'normal' : 'offline') return ( diff --git a/src/pages/family/detail.module.scss b/src/pages/family/detail.module.scss index 9267d3f..4fe9f34 100644 --- a/src/pages/family/detail.module.scss +++ b/src/pages/family/detail.module.scss @@ -124,3 +124,71 @@ font-size: 26px; color: $color-text-tertiary; } + +// 转让群主选人弹层(Modal 内容) +.transferTitle { + display: block; + text-align: center; + font-size: 36px; + font-weight: 600; + color: $color-text; + margin-bottom: 24px; +} + +.transferList { + margin-bottom: 16px; + max-height: 600px; + overflow-y: auto; +} + +.transferEmpty { + display: block; + padding: 48px 16px; + text-align: center; + font-size: 26px; + color: $color-text-tertiary; +} + +.transferItem { + padding: 20px 24px; + background: $color-bg-gray; + border-radius: $radius-md; + margin-bottom: 12px; +} + +.transferItemPressed { + background: $color-bg-press; +} + +.transferItemName { + display: block; + font-size: 30px; + font-weight: 600; + color: $color-text; + margin-bottom: 6px; +} + +.transferItemMeta { + display: block; + font-size: 22px; + color: $color-text-secondary; +} + +.transferCancel { + height: 88px; + border-radius: $radius-pill; + background: $color-bg-gray; + display: flex; + align-items: center; + justify-content: center; + margin-top: 8px; +} + +.transferCancelPressed { + opacity: 0.85; +} + +.transferCancelText { + font-size: 30px; + color: $color-text-secondary; +} diff --git a/src/pages/family/detail.tsx b/src/pages/family/detail.tsx index b44a6e7..b74f250 100644 --- a/src/pages/family/detail.tsx +++ b/src/pages/family/detail.tsx @@ -8,7 +8,8 @@ import { Loading } from '@/components/loading' import { StateView } from '@/components/state-view' import { FamilyCard } from '@/components/family-card' import { ListRow, ListRowGroup } from '@/components/list-row' -import { getHomeDetail, getHomeMemberList, joinHome, type HomeInfo, type HomeMember } from '@/api/home' +import { Modal } from '@/components/modal' +import { getHomeDetail, getHomeMemberList, joinHome, transferOwner, type HomeInfo, type HomeMember } from '@/api/home' import { getCareMemberList, type CareMember } from '@/api/careMember' import { MAX_CARED_PER_FAMILY } from '@/constants/family' import { ROUTES } from '@/constants/routes' @@ -41,6 +42,10 @@ export default function FamilyDetail() { const [members, setMembers] = useState([]) const [caredList, setCaredList] = useState([]) + // 转让群主:底部入口仅群主可见,点击后弹选人 Modal + const [transferOpen, setTransferOpen] = useState(false) + const [transferring, setTransferring] = useState(false) + const load = async () => { // 分享卡直达本页且未登录:先去登录,登录后回跳本页 if (!token) { @@ -106,6 +111,33 @@ export default function FamilyDetail() { navigateTo(`${ROUTES.FAMILY_CONTACT_ADD}?familyId=${familyId}`) } + // 我在成员列表里的角色:转让入口只对群主开放 + const myMember = members.find((m) => m.memberId === myUserId) ?? null + const isOwner = myMember?.role === 'OWNER' + + const openTransfer = () => { + if (!isOwner || transferring) return + setTransferOpen(true) + } + const closeTransfer = () => { + if (transferring) return + setTransferOpen(false) + } + const doTransfer = async (target: HomeMember) => { + if (transferring) return + setTransferring(true) + try { + await transferOwner({ memberId: target.memberId }) + showToast('已转让群主') + setTransferOpen(false) + await load() + } catch (e) { + showToast(e instanceof Error ? e.message : '转让失败') + } finally { + setTransferring(false) + } + } + if (loading) { return ( @@ -205,11 +237,40 @@ export default function FamilyDetail() { 邀请他人 › - + + + {/* 转让群主选人弹层:列出除自己外的成员,选中即调用 transferOwner */} + + 转让群主给谁? + + {members.filter((m) => m.memberId !== myUserId).length === 0 ? ( + 暂无可转让的成员 + ) : ( + members + .filter((m) => m.memberId !== myUserId) + .map((m) => ( + doTransfer(m)} + > + {memberName(m)} + + {m.role === 'OWNER' ? '群主' : '成员'} · 群昵称:{m.nickName ?? '未设置'} + + + )) + )} + + + 取消 + + ) } diff --git a/tests/unit/home/transferOwner.spec.ts b/tests/unit/home/transferOwner.spec.ts new file mode 100644 index 0000000..007ef0b --- /dev/null +++ b/tests/unit/home/transferOwner.spec.ts @@ -0,0 +1,60 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' + +// Taro.request mock(hoisted 保证 vi.mock 工厂能拿到同一引用) +const mockRequest = vi.hoisted(() => vi.fn()) +vi.mock('@tarojs/taro', () => ({ default: { request: mockRequest } })) + +const reLaunch = vi.hoisted(() => vi.fn()) +vi.mock('@/utils/navigation', () => ({ reLaunch })) + +vi.mock('@/store', () => ({ + store: { + getState: () => ({ auth: { token: 'tk-test' } }), + dispatch: vi.fn() + } +})) + +vi.mock('@/store/authSlice', () => ({ clearAuth: vi.fn() })) + +import { transferOwner } from '@/api/home' + +const okEnvelope = () => ({ statusCode: 200, data: { Code: '200', Message: '', Content: null }, header: {} }) + +beforeEach(() => { + mockRequest.mockReset() + reLaunch.mockReset() + mockRequest.mockResolvedValue(okEnvelope()) +}) + +describe('transferOwner', () => { + it('POST /yanglao/app/ylHome/transferOwner 并把 { memberId } 作为请求体', async () => { + await transferOwner({ memberId: 'm-target-001' }) + expect(mockRequest).toHaveBeenCalledTimes(1) + const arg = mockRequest.mock.calls[0][0] + expect(arg.url).toMatch(/\/api\/biz\/yanglao\/app\/ylHome\/transferOwner$/) + expect(arg.method).toBe('POST') + expect(arg.data).toEqual({ memberId: 'm-target-001' }) + // 自动注入 token(gateway / biz 前缀走业务网关) + expect(arg.header.token).toBe('tk-test') + }) + + it('业务码非 200 抛出 BusinessError(不踢登录)', async () => { + mockRequest.mockResolvedValueOnce({ + statusCode: 200, + data: { Code: '500', Message: '无权操作', Content: null }, + header: {} + }) + await expect(transferOwner({ memberId: 'm-2' })).rejects.toThrow('无权操作') + expect(reLaunch).not.toHaveBeenCalled() + }) + + it('业务码 202(未登录)触发踢回登录', async () => { + mockRequest.mockResolvedValueOnce({ + statusCode: 200, + data: { Code: '202', Message: '未登录', Content: null }, + header: {} + }) + await expect(transferOwner({ memberId: 'm-3' })).rejects.toThrow() + expect(reLaunch).toHaveBeenCalled() + }) +})