feat(room): 房间管理入参类型化并支持使用场景选择(add/update 带 sence 枚举)

This commit is contained in:
ozh 2026-09-17 14:44:26 +08:00
parent cb2dc3d2aa
commit 6377e53ecb
3 changed files with 345 additions and 86 deletions

View File

@ -1,10 +1,45 @@
import { request } from '@/http'
// 房间使用场景枚举(apifox YlHomeRoomAddReq.sence 推荐值)
export const ROOM_SENCE_MAP: Record<string, string> = {
LIVING_ROOM: '客厅',
BEDROOM: '卧室',
MASTER_BEDROOM: '主卧',
BATHROOM: '卫生间',
KITCHEN: '厨房',
DINING_ROOM: '餐厅',
STUDY: '书房',
BALCONY: '阳台',
OTHER: '其他'
}
// 入参对齐 apifox YlHomeRoomAddReq
export interface HomeRoomAddReq {
homeId: string
name: string
sence?: string // 使用场景,ROOM_SENCE_MAP 的 key
icon?: string
}
// 入参对齐 apifox YlHomeRoomUpdateReq
export interface HomeRoomUpdateReq {
roomId: string
name?: string
sence?: string
icon?: string
}
// 入参对齐 apifox YlHomeRoomDeleteReq
export interface HomeRoomDeleteReq {
roomId: string
}
// 骨架:本期仅锁定接口路径,字段类型联调时修正(spec D7)
export interface HomeRoom {
roomId: string
homeId: string
roomName: string
sence?: string | null
}
// 文档路径为 POST + body { homeId }(YlHomeRoomListReq)
@ -12,14 +47,14 @@ export function getRoomList(homeId: string): Promise<HomeRoom[]> {
return request.post('/yanglao/app/ylHomeRoom/getList', { homeId })
}
export function addRoom(params: Record<string, unknown>): Promise<void> {
return request.post('/yanglao/app/ylHomeRoom/add', params)
export function addRoom(params: HomeRoomAddReq): Promise<void> {
return request.post('/yanglao/app/ylHomeRoom/add', params as unknown as Record<string, unknown>)
}
export function updateRoom(params: Record<string, unknown>): Promise<void> {
return request.post('/yanglao/app/ylHomeRoom/update', params)
export function updateRoom(params: HomeRoomUpdateReq): Promise<void> {
return request.post('/yanglao/app/ylHomeRoom/update', params as unknown as Record<string, unknown>)
}
export function deleteRoom(params: Record<string, unknown>): Promise<void> {
return request.post('/yanglao/app/ylHomeRoom/delete', params)
export function deleteRoom(params: HomeRoomDeleteReq): Promise<void> {
return request.post('/yanglao/app/ylHomeRoom/delete', params as unknown as Record<string, unknown>)
}

View File

@ -1,6 +1,7 @@
.page {
min-height: 100vh;
background: $color-bg;
padding-bottom: 40px;
}
.stateWrap {
@ -13,8 +14,8 @@
.list { margin-top: 20px; }
.item {
height: 104px;
padding: 0 32px;
min-height: 104px;
padding: 16px 32px;
margin-bottom: 2px;
background: $color-bg-white;
display: flex;
@ -22,26 +23,43 @@
justify-content: space-between;
}
.itemActive { background: $color-primary-light-bg; }
// 名称 + 场景标签组:点击进编辑
.itemMain {
display: flex;
align-items: center;
min-width: 0;
flex: 1;
}
.name {
font-size: 32px;
color: $color-text;
font-weight: 600;
margin-right: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.itemActive .name { color: $color-primary; }
.check {
font-size: 32px;
color: $color-text-placeholder;
.senceTag {
flex-shrink: 0;
padding: 4px 16px;
background: $color-primary-light-bg;
border-radius: $radius-pill;
font-size: 22px;
color: $color-primary-deep;
}
.itemActive .check { color: $color-primary; }
.del {
flex-shrink: 0;
margin-left: 24px;
font-size: 26px;
color: $color-danger;
}
// 添加房间:虚线边框卡,与家庭圈添加卡风格一致
.addBtn {
margin-top: 24px;
margin: 24px 24px 0;
height: 104px;
border: 1px dashed $color-primary-light-border;
border-radius: $radius-md;
@ -64,3 +82,132 @@
font-size: 28px;
color: $color-primary;
}
// —— 添加/编辑弹窗表单 ——
.formBody {
padding: 8px 40px 40px;
}
.formField {
margin-bottom: 32px;
}
.formLabel {
display: block;
font-size: 28px;
color: $color-text-secondary;
margin-bottom: 16px;
}
.formInput {
width: 100%;
height: 88px;
padding: 0 32px;
background: $color-bg-gray;
border-radius: $radius-md;
font-size: 28px;
color: $color-text;
box-sizing: border-box;
}
.senceGrid {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.senceChip {
padding: 14px 32px;
background: $color-bg-gray;
border: 1px solid transparent;
border-radius: $radius-pill;
font-size: 26px;
color: $color-text-secondary;
}
.senceChipActive {
background: $color-primary-light-bg;
border-color: $color-primary;
color: $color-primary-deep;
}
.formSubmit {
height: 96px;
background: $color-primary-grad;
border-radius: $radius-pill;
display: flex;
align-items: center;
justify-content: center;
}
.formSubmitPressed { opacity: 0.85; }
.formSubmitDisabled { opacity: 0.6; }
.formSubmitText {
font-size: 32px;
font-weight: 600;
color: #ffffff;
}
// —— 删除确认弹窗 ——
.delMask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: $color-mask;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
}
.delDialog {
width: 600px;
background: $color-bg-white;
border-radius: $radius-lg;
padding: 48px 40px 32px;
}
.delTitle {
display: block;
text-align: center;
font-size: 34px;
font-weight: 600;
color: $color-text;
margin-bottom: 24px;
}
.delContent {
display: block;
font-size: 28px;
color: $color-text-secondary;
line-height: 1.6;
text-align: center;
margin-bottom: 40px;
}
.delBtnRow {
display: flex;
gap: 24px;
}
.delCancel,
.delConfirm {
flex: 1;
height: 88px;
border-radius: $radius-pill;
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
}
.delCancel {
background: $color-bg-gray;
color: $color-text-secondary;
}
.delConfirm {
background: $color-danger;
color: #ffffff;
font-weight: 600;
}

View File

@ -1,31 +1,29 @@
import { useState, useEffect } from 'react'
import Taro from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { View, Text, Input } from '@tarojs/components'
import { Navbar } from '@/components/navbar'
import { Loading } from '@/components/loading'
import { Empty } from '@/components/empty'
import { StateView } from '@/components/state-view'
import { BottomSheet } from '@/components/bottom-sheet'
import { getHomeList, type HomeInfo } from '@/api/home'
import { getRoomList, addRoom, updateRoom, deleteRoom, type HomeRoom } from '@/api/homeRoom'
import {
getRoomList, addRoom, updateRoom, deleteRoom, ROOM_SENCE_MAP, type HomeRoom
} from '@/api/homeRoom'
import { showToast } from '@/utils/toast'
import styles from './select.module.scss'
// Taro 类型未跟进 wx.showModal 的 editable 扩展(微信 2.17.1+),运行时真实存在
interface EditableModalOption extends Taro.showModal.Option {
editable?: boolean
placeholderText?: string
// 添加/编辑弹窗的表单态:editingRoom 为空是新增,有值是编辑
interface RoomFormState {
name: string
sence: string
}
interface EditableModalResult extends Taro.showModal.SuccessCallbackResult {
content?: string
}
// 消除 Taro.showModal cancel/confirm 分支中的隐式 any
type ModalResult = { confirm: boolean; cancel: boolean; content?: string }
const showEditableModal = (option: EditableModalOption) =>
Taro.showModal(option as Taro.showModal.Option) as Promise<EditableModalResult>
// 房间操作菜单:编辑名称 / 删除
const showActionSheet = () =>
Taro.showActionSheet({ itemList: ['编辑名称', '删除房间'] }) as Promise<Taro.showActionSheet.SuccessCallbackResult>
const confirmModal = (option: Taro.showModal.Option): Promise<ModalResult> =>
Taro.showModal(option) as Promise<ModalResult>
export default function SceneSelect() {
const [loading, setLoading] = useState(true)
@ -33,6 +31,15 @@ export default function SceneSelect() {
const [home, setHome] = useState<HomeInfo | null>(null)
const [list, setList] = useState<HomeRoom[]>([])
// 表单弹窗
const [formOpen, setFormOpen] = useState(false)
const [editingRoom, setEditingRoom] = useState<HomeRoom | null>(null)
const [form, setForm] = useState<RoomFormState>({ name: '', sence: '' })
const [saving, setSaving] = useState(false)
// 删除确认弹窗
const [deletingRoom, setDeletingRoom] = useState<HomeRoom | null>(null)
const load = async () => {
setLoading(true)
setLoadError(false)
@ -55,56 +62,68 @@ export default function SceneSelect() {
useEffect(() => { load() }, [])
// 添加房间:可编辑弹窗输入名称,成功后刷新列表
const addRoomByName = () => {
showEditableModal({
title: '添加房间',
editable: true,
placeholderText: '请输入房间名称(如:阳台)'
}).then(async ({ confirm, content }) => {
if (!confirm || !home) return
try {
await addRoom({ homeId: home.homeId, name: (content ?? '').trim() })
showToast('添加成功')
await load()
} catch (error) {
showToast(error instanceof Error ? error.message : '添加失败')
}
})
// —— 添加 / 编辑 ——
const openAddForm = () => {
setEditingRoom(null)
setForm({ name: '', sence: '' })
setFormOpen(true)
}
// 长按/操作:编辑名称或删除
const onRoomAction = (room: HomeRoom) => {
if (!home) return
showActionSheet()
.then(async ({ tapIndex }) => {
if (tapIndex === 0) {
const { confirm, content } = await showEditableModal({
title: '编辑房间名称',
editable: true,
content: room.roomName,
placeholderText: '请输入房间名称'
})
if (!confirm) return
await updateRoom({ roomId: room.roomId, name: (content ?? '').trim() })
showToast('已修改')
await load()
} else if (tapIndex === 1) {
const { confirm } = await Taro.showModal({
title: '删除房间',
content: `确定删除「${room.roomName}」吗?`,
confirmText: '删除',
confirmColor: '#EE4B4B'
})
if (!confirm) return
await deleteRoom({ roomId: room.roomId })
showToast('已删除')
await load()
const openEditForm = (room: HomeRoom) => {
setEditingRoom(room)
setForm({ name: room.roomName, sence: room.sence ?? '' })
setFormOpen(true)
}
const closeForm = () => {
if (saving) return
setFormOpen(false)
setEditingRoom(null)
}
const submitForm = async () => {
if (saving) return
const name = form.name.trim()
if (!name) {
showToast('请输入房间名称')
return
}
if (!home) return
setSaving(true)
try {
if (editingRoom) {
await updateRoom({ roomId: editingRoom.roomId, name, sence: form.sence || undefined })
showToast('已修改')
} else {
await addRoom({ homeId: home.homeId, name, sence: form.sence || undefined })
showToast('添加成功')
}
setFormOpen(false)
setEditingRoom(null)
await load()
} catch (e) {
showToast(e instanceof Error ? e.message : '保存失败')
} finally {
setSaving(false)
}
}
// —— 删除 ——
const confirmDelete = async () => {
if (!deletingRoom || saving) return
setSaving(true)
try {
await deleteRoom({ roomId: deletingRoom.roomId })
showToast('已删除')
setDeletingRoom(null)
await load()
} catch (e) {
showToast(e instanceof Error ? e.message : '删除失败')
} finally {
setSaving(false)
}
})
.catch(() => {
// 取消操作静默
})
}
return (
@ -117,25 +136,83 @@ export default function SceneSelect() {
) : (
<View className={styles.list}>
{list.map((room) => (
<View
key={room.roomId}
className={styles.item}
onLongPress={() => onRoomAction(room)}
>
<View key={room.roomId} className={styles.item}>
<View className={styles.itemMain} onClick={() => openEditForm(room)}>
<Text className={styles.name}>{room.roomName}</Text>
<Text className={styles.check} onClick={() => onRoomAction(room)}>编辑</Text>
{room.sence && ROOM_SENCE_MAP[room.sence] && (
<Text className={styles.senceTag}>{ROOM_SENCE_MAP[room.sence]}</Text>
)}
</View>
<Text className={styles.del} onClick={() => setDeletingRoom(room)}>删除</Text>
</View>
))}
{/* 空列表时也保留添加入口 */}
{list.length === 0 && (
<View className={styles.stateWrap}><Empty text="暂无房间,点击下方添加" /></View>
)}
<View className={styles.addBtn} hoverClass={styles.addBtnPressed} onClick={addRoomByName}>
<View className={styles.addBtn} hoverClass={styles.addBtnPressed} onClick={openAddForm}>
<Text className={styles.addIcon}>+</Text>
<Text className={styles.addText}>添加房间</Text>
</View>
</View>
)}
{/* 添加/编辑房间弹窗:名称输入 + 场景九宫格 */}
<BottomSheet visible={formOpen} title={editingRoom ? '编辑房间' : '添加房间'} onClose={closeForm}>
<View className={styles.formBody}>
<View className={styles.formField}>
<Text className={styles.formLabel}>房间名称</Text>
<Input
className={styles.formInput}
placeholder="请输入房间名称(如:阳台)"
maxlength={20}
value={form.name}
onInput={(e) => setForm((f) => ({ ...f, name: e.detail.value }))}
/>
</View>
<View className={styles.formField}>
<Text className={styles.formLabel}>使用场景(可选)</Text>
<View className={styles.senceGrid}>
{Object.entries(ROOM_SENCE_MAP).map(([key, label]) => (
<View
key={key}
className={`${styles.senceChip} ${form.sence === key ? styles.senceChipActive : ''}`}
onClick={() => setForm((f) => ({ ...f, sence: f.sence === key ? '' : key }))}
>
<Text>{label}</Text>
</View>
))}
</View>
</View>
<View
className={`${styles.formSubmit} ${saving ? styles.formSubmitDisabled : ''}`}
hoverClass={styles.formSubmitPressed}
onClick={submitForm}
>
<Text className={styles.formSubmitText}>{saving ? '保存中...' : '保存'}</Text>
</View>
</View>
</BottomSheet>
{/* 删除确认 */}
{deletingRoom && (
<View className={styles.delMask} onClick={() => !saving && setDeletingRoom(null)} catchMove>
<View className={styles.delDialog} onClick={(e) => e.stopPropagation()}>
<Text className={styles.delTitle}>删除房间</Text>
<Text className={styles.delContent}>
确定删除「{deletingRoom.roomName}」吗?该房间下的设备需重新分配。
</Text>
<View className={styles.delBtnRow}>
<View className={styles.delCancel} onClick={() => setDeletingRoom(null)}>
<Text>取消</Text>
</View>
<View className={styles.delConfirm} onClick={confirmDelete}>
<Text>{saving ? '删除中...' : '删除'}</Text>
</View>
</View>
</View>
</View>
)}
</View>
)
}