refactor(api): 接口路径抽至 src/constants/endpoints 集中维护
- 新增 endpoints.ts:HOME/CARE_MEMBER/HOME_ROOM/HOME_DEVICE/HOME_ALARM/ USER/THIRD_PLATFORM 七组路径 + GATEWAY_PREFIX(user/biz 网关分组) - src/api/* 全部接入常量,http/client.ts 改用 GATEWAY_PREFIX.user 判定网关 - transferOwner 加上 TransferOwnerParams 类型约束(替代 Record<string,unknown>) - FallStatusCard 接收 alarmType(2026-09-22 实测字段),status 保留兼容 - 家庭圈详情页"转让群主"入口接通:仅群主可见,选人 Modal + 接口调用 - request.get/post params 类型放宽到 object 以匹配新实参/结构 - 新增 tests/unit/home/transferOwner.spec.ts(路径/业务码/202 三分支)
This commit is contained in:
parent
de8df8bfcd
commit
5e00280d2c
|
|
@ -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<WxMiniappLoginResponse> {
|
||||
return request.post<WxLoginResult>(
|
||||
'/thirdPlatform/wxDevLogin/miniapp/login',
|
||||
THIRD_PLATFORM.wxMiniappLogin,
|
||||
params as unknown as Record<string, unknown>,
|
||||
{ needAuth: false }
|
||||
)
|
||||
|
|
@ -52,5 +53,5 @@ export function wxMiniappLogin(params: WxMiniappLoginParams): Promise<WxMiniappL
|
|||
// 冷启动登录态校验:token 有效返回登录信息,过期返回 202 由 http 层统一踢登录页。
|
||||
// 响应 Content 结构(ResponseData<UserInfo>)联调实测后修正字段
|
||||
export function getLoginInfo(): Promise<WxUserInfo> {
|
||||
return request.get('/v0/app/users/getLoginInfo')
|
||||
return request.get(USER.getLoginInfo)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
return request.post('/yanglao/app/ylHomeCareMember/save', params as unknown as Record<string, unknown>)
|
||||
return request.post(CARE_MEMBER.save, params as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
export function getCareMemberList(homeId: string): Promise<CareMember[]> {
|
||||
return request.post('/yanglao/app/ylHomeCareMember/getList', { homeId })
|
||||
return request.post(CARE_MEMBER.getList, { homeId })
|
||||
}
|
||||
|
||||
export function getCareMemberDetail(memberId: string): Promise<CareMember> {
|
||||
return request.get(`/yanglao/app/ylHomeCareMember/get/${memberId}`)
|
||||
return request.get(CARE_MEMBER.getDetail(memberId))
|
||||
}
|
||||
|
||||
export function deleteCareMember(memberId: string): Promise<void> {
|
||||
return request.post('/yanglao/app/ylHomeCareMember/delete', { memberId })
|
||||
return request.post(CARE_MEMBER.delete, { memberId })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
// name 可空,空则后端默认「我的家庭圈」(文档 YlHomeAddReq)
|
||||
return request.post('/yanglao/app/ylHome/add', { name })
|
||||
return request.post(HOME.add, { name })
|
||||
}
|
||||
|
||||
export function updateHome(params: Record<string, unknown>): Promise<void> {
|
||||
return request.post('/yanglao/app/ylHome/update', params)
|
||||
return request.post(HOME.update, params)
|
||||
}
|
||||
|
||||
export function dissolveHome(params: Record<string, unknown>): Promise<void> {
|
||||
return request.post('/yanglao/app/ylHome/dissolve', params)
|
||||
return request.post(HOME.dissolve, params)
|
||||
}
|
||||
|
||||
export function getHomeList(): Promise<HomeInfo[]> {
|
||||
return request.post('/yanglao/app/ylHome/getList')
|
||||
return request.post(HOME.getList)
|
||||
}
|
||||
|
||||
export function getHomeMemberList(homeId: string): Promise<HomeMember[]> {
|
||||
return request.post('/yanglao/app/ylHome/getMemberList', { homeId })
|
||||
return request.post(HOME.getMemberList, { homeId })
|
||||
}
|
||||
|
||||
export function transferOwner(params: Record<string, unknown>): Promise<void> {
|
||||
return request.post('/yanglao/app/ylHome/transferOwner', params)
|
||||
export interface TransferOwnerParams {
|
||||
memberId: string
|
||||
}
|
||||
|
||||
export function transferOwner(params: TransferOwnerParams): Promise<void> {
|
||||
return request.post(HOME.transferOwner, params)
|
||||
}
|
||||
|
||||
export function updateMemberNickName(params: Record<string, unknown>): Promise<void> {
|
||||
return request.post('/yanglao/app/ylHome/updateMemberNickName', params)
|
||||
return request.post(HOME.updateMemberNickName, params)
|
||||
}
|
||||
|
||||
export function getHomeDetail(homeId: string): Promise<HomeInfo> {
|
||||
return request.get(`/yanglao/app/ylHome/get/${homeId}`)
|
||||
return request.get(HOME.getDetail(homeId))
|
||||
}
|
||||
|
||||
export function joinHome(code: string): Promise<void> {
|
||||
// code 为家庭编号(文档 YlHomeJoinReq)
|
||||
return request.post('/yanglao/app/ylHome/join', { code })
|
||||
return request.post(HOME.join, { code })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HomeAlarm[]> {
|
||||
return request.post('/yanglao/app/ylHomeAlarm/getList', params as unknown as Record<string, unknown>)
|
||||
return request.post(HOME_ALARM.getList, params as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
export function getAlarmDetail(alarmId: string): Promise<HomeAlarm> {
|
||||
return request.get(`/yanglao/app/ylHomeAlarm/get/${alarmId}`)
|
||||
return request.get(HOME_ALARM.getDetail(alarmId))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HomeDevice[]> {
|
||||
return request.post('/yanglao/app/ylHomeDevice/getList', params as unknown as Record<string, unknown>)
|
||||
return request.post(HOME_DEVICE.getList, params as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
// 单设备详情(GET + 路径参数,含 runTimeInfo 实时快照)
|
||||
export function getHomeDeviceDetail(installId: string): Promise<HomeDevice> {
|
||||
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<Vita
|
|||
sortDirection: 'desc'
|
||||
}
|
||||
}
|
||||
return request.post('/iot/propertyRecord/getPropertyRecords', params as unknown as Record<string, unknown>)
|
||||
return request.post(HOME_DEVICE.getPropertyRecords, params as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
export function addHomeDevice(params: HomeDeviceAddReq): Promise<void> {
|
||||
return request.post('/yanglao/app/ylHomeDevice/add', params as unknown as Record<string, unknown>)
|
||||
return request.post(HOME_DEVICE.add, params as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
export function unbindHomeDevice(params: HomeDeviceUnbindReq): Promise<void> {
|
||||
return request.post('/yanglao/app/ylHomeDevice/unbind', params as unknown as Record<string, unknown>)
|
||||
return request.post(HOME_DEVICE.unbind, params as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { request } from '@/http'
|
||||
import { HOME_ROOM } from '@/constants/endpoints'
|
||||
|
||||
// 房间使用场景枚举(apifox YlHomeRoomAddReq.sence 推荐值)
|
||||
export const ROOM_SENCE_MAP: Record<string, string> = {
|
||||
|
|
@ -45,17 +46,17 @@ export interface HomeRoom {
|
|||
|
||||
// 文档路径为 POST + body { homeId }(YlHomeRoomListReq)
|
||||
export function getRoomList(homeId: string): Promise<HomeRoom[]> {
|
||||
return request.post('/yanglao/app/ylHomeRoom/getList', { homeId })
|
||||
return request.post(HOME_ROOM.getList, { homeId })
|
||||
}
|
||||
|
||||
export function addRoom(params: HomeRoomAddReq): Promise<void> {
|
||||
return request.post('/yanglao/app/ylHomeRoom/add', params as unknown as Record<string, unknown>)
|
||||
return request.post(HOME_ROOM.add, params as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
export function updateRoom(params: HomeRoomUpdateReq): Promise<void> {
|
||||
return request.post('/yanglao/app/ylHomeRoom/update', params as unknown as Record<string, unknown>)
|
||||
return request.post(HOME_ROOM.update, params as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
export function deleteRoom(params: HomeRoomDeleteReq): Promise<void> {
|
||||
return request.post('/yanglao/app/ylHomeRoom/delete', params as unknown as Record<string, unknown>)
|
||||
return request.post(HOME_ROOM.delete, params as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/']
|
||||
}
|
||||
|
|
@ -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<T>(
|
|||
}
|
||||
|
||||
// 登录接口与 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<ApiEnvelope<T>>
|
||||
try {
|
||||
|
|
@ -140,8 +141,8 @@ function pickResponseToken(header: Record<string, unknown> | undefined): string
|
|||
return raw
|
||||
}
|
||||
|
||||
request.get = <T>(url: string, params?: Record<string, unknown>, config?: RequestConfig) =>
|
||||
request<T>(url, params, { ...config, method: 'GET' })
|
||||
request.get = <T>(url: string, params?: object, config?: RequestConfig) =>
|
||||
request<T>(url, params as Record<string, unknown> | undefined, { ...config, method: 'GET' })
|
||||
|
||||
request.post = <T>(url: string, params?: Record<string, unknown>, config?: RequestConfig) =>
|
||||
request<T>(url, params, { ...config, method: 'POST' })
|
||||
request.post = <T>(url: string, params?: object, config?: RequestConfig) =>
|
||||
request<T>(url, params as Record<string, unknown> | undefined, { ...config, method: 'POST' })
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ export default function Detail() {
|
|||
|
||||
{isFallDetector ? (
|
||||
<FallStatusCard
|
||||
alarmType={device?.runTimeInfo?.properties?.alarmType}
|
||||
status={device?.runTimeInfo?.properties?.status}
|
||||
lastTime={device?.runTimeInfo?.properties?.lastTime}
|
||||
version={device?.runTimeInfo?.properties?.version}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { FALL_STATUS_MAP } from '@/constants/enums'
|
|||
import styles from './fall-status-card.module.scss'
|
||||
|
||||
interface Props {
|
||||
// 实时跌倒状态编码(见 FALL_STATUS_MAP)。优先 alarmType,status 为兼容字段
|
||||
alarmType?: number
|
||||
status?: number
|
||||
lastTime?: string
|
||||
version?: string
|
||||
|
|
@ -18,9 +20,10 @@ const STATUS_CLS: Record<number, string> = {
|
|||
}
|
||||
|
||||
// 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 (
|
||||
<View className={`${styles.fallCard} ${styles[`fallCard${cap(level)}`]}`}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HomeMember[]>([])
|
||||
const [caredList, setCaredList] = useState<CareMember[]>([])
|
||||
|
||||
// 转让群主:底部入口仅群主可见,点击后弹选人 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 (
|
||||
<View className={styles.page}>
|
||||
|
|
@ -205,11 +237,40 @@ export default function FamilyDetail() {
|
|||
<Text className={styles.shareLabel}>邀请他人</Text>
|
||||
<Text className={styles.shareArrow}>›</Text>
|
||||
</Button>
|
||||
<ListRow label="转让群主" arrow onClick={stub('转让功能即将上线')} />
|
||||
<ListRow label="转让群主" arrow onClick={isOwner ? openTransfer : stub('仅群主可转让')} />
|
||||
<ListRow label="修改群昵称" arrow onClick={stub('修改昵称功能即将上线')} />
|
||||
</ListRowGroup>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 转让群主选人弹层:列出除自己外的成员,选中即调用 transferOwner */}
|
||||
<Modal visible={transferOpen} onClose={closeTransfer}>
|
||||
<Text className={styles.transferTitle}>转让群主给谁?</Text>
|
||||
<View className={styles.transferList}>
|
||||
{members.filter((m) => m.memberId !== myUserId).length === 0 ? (
|
||||
<Text className={styles.transferEmpty}>暂无可转让的成员</Text>
|
||||
) : (
|
||||
members
|
||||
.filter((m) => m.memberId !== myUserId)
|
||||
.map((m) => (
|
||||
<View
|
||||
key={m.memberId}
|
||||
className={styles.transferItem}
|
||||
hoverClass={styles.transferItemPressed}
|
||||
onClick={() => doTransfer(m)}
|
||||
>
|
||||
<Text className={styles.transferItemName}>{memberName(m)}</Text>
|
||||
<Text className={styles.transferItemMeta}>
|
||||
{m.role === 'OWNER' ? '群主' : '成员'} · 群昵称:{m.nickName ?? '未设置'}
|
||||
</Text>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
<View className={styles.transferCancel} hoverClass={styles.transferCancelPressed} onClick={closeTransfer}>
|
||||
<Text className={styles.transferCancelText}>取消</Text>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue