@@ -0,0 +1,545 @@
< template >
< section class = "stores-page" >
< el-alert
v-if = "lastError"
class = "alert-line"
:title = "lastError"
type = "error"
show -icon
closable
@close ="lastError = ''"
/ >
< section class = "panel" >
< header class = "panel-toolbar store-toolbar" >
< div >
< p class = "section-kicker" > M08 - D · 门店资产 < / p >
< h3 > 门店与房间管理 < / h3 >
< / div >
< div class = "toolbar-actions" >
< el-input v-model = "search" class="search-input" clearable placeholder="搜索门店、城市或地址" / >
< el-select v-model = "statusFilter" class="filter-select" placeholder="营业状态" >
< el -option label = "全部状态" value = "" / >
< el-option label = "营业中" value = "OPEN" / >
< el-option label = "已闭店" value = "CLOSED" / >
< el-option label = "暂停营业" value = "SUSPENDED" / >
< / el-select >
< el-button :icon = "RefreshCw" :loading = "loadingStores" @click ="loadStores" > 刷新 < / el -button >
< el-button type = "primary" :icon = "Plus" @click ="openStoreForm()" > 新增门店 < / el -button >
< / div >
< / header >
< div class = "store-layout" >
< aside class = "store-list" aria -label = " 门店列表 " >
< button
v-for = "store in filteredStores"
:key = "store.id"
class = "store-card"
: class = "{ active: selectedStoreId === store.id }"
type = "button"
@click ="selectStore(store.id)"
>
< span class = "store-card-title" >
< strong > { { store . name } } < / strong >
< el-tag :type = "storeStatusType(store.businessStatus)" size = "small" >
{ { storeStatusLabel ( store . businessStatus ) } }
< / el-tag >
< / span >
< span > { { [ store . city , store . district ] . filter ( Boolean ) . join ( ' · ' ) || '未填写地区' } } < / span >
< small > { { store . address || '未填写地址' } } < / small >
< / button >
< el-empty v-if = "!loadingStores && filteredStores.length === 0" description="没有匹配门店" / >
< / aside >
< div v-if = "selectedStore" class="store-detail" >
< header class = "store-detail-header" >
< div >
< p class = "section-kicker" > 门店 # { { selectedStore . id } } < / p >
< h3 > { { selectedStore . name } } < / h3 >
< p > { { selectedStore . address || '未填写地址' } } < / p >
< / div >
< div class = "row-actions" >
< el-button :icon = "Edit3" @click ="openStoreForm(selectedStore)" > 编辑门店 < / el -button >
< el-button type = "danger" plain :icon = "Archive" @click ="archiveStore(selectedStore)" >
归档
< / el -button >
< / div >
< / header >
< div class = "detail-grid store-summary-grid" >
< span > < small > 联系电话 < / small > < strong > { { selectedStore . contactPhone || '-' } } < / strong > < / span >
< span > < small > 时区 < / small > < strong > { { selectedStore . timezone } } < / strong > < / span >
< span > < small > 访客 Wi - Fi < / small > < strong > { { wifiSummary ( selectedStore ) } } < / strong > < / span >
< span > < small > 通知地址 < / small > < strong > { { selectedStore . notificationUrl || '-' } } < / strong > < / span >
< / div >
< section class = "business-hours-board" >
< strong > 营业时段 < / strong >
< div class = "hours-chip-list" >
< span v-for = "hour in normalizedHours(selectedStore.businessHours)" :key="hour.weekday" >
{{ weekdayLabel ( hour.weekday ) }}
< b > { { hour . isClosed ? '休息' : ` ${ minuteLabel ( hour . openMinute ) } – ${ minuteLabel ( hour . closeMinute ) } ` } } < / b >
< / span >
< / div >
< / section >
< header class = "rooms-toolbar" >
< div >
< p class = "section-kicker" > 房间配置 < / p >
< h3 > { { rooms . length } } 个房间 < / h3 >
< / div >
< div class = "toolbar-actions" >
< el-input v-model = "roomSearch" class="search-input" clearable placeholder="搜索房号、名称、分类" / >
< el-select v-model = "roomStatusFilter" class="filter-select" placeholder="房态" >
< el -option label = "全部房态" value = "" / >
< el-option v-for = "item in operationalStatuses" :key="item.value" :label="item.label" :value="item.value" / >
< / el-select >
< el-button :icon = "RefreshCw" :loading = "loadingRooms" @click ="loadRooms" > 刷新 < / el -button >
< el-button type = "primary" :icon = "Plus" @click ="openRoomForm()" > 新增房间 < / el -button >
< / div >
< / header >
< el-table v-loading = "loadingRooms" :data="filteredRooms" class="data-table" row-key="id" >
< el -table -column label = "房间" min -width = " 180 " >
< template # default = "{ row }" >
< div class = "stack" >
< strong > { { row . roomNo } } · { { row . name } } < / strong >
< span > { { row . categoryName } } · { { row . capacity } } 人 < / span >
< / div >
< / template >
< / el-table-column >
< el-table-column label = "价格" min -width = " 150 " >
< template # default = "{ row }" >
< div class = "stack" >
< strong > { { money ( row . basePriceCents ) } } / 时段 < / strong >
< span > 节假日 { { money ( row . holidayPriceCents ) } } < / span >
< / div >
< / template >
< / el-table-column >
< el-table-column label = "状态" min -width = " 150 " >
< template # default = "{ row }" >
< div class = "profile-badges" >
< el-tag : type = "row.configurationStatus === 'ENABLED' ? 'success' : 'info'" size = "small" >
{ { row . configurationStatus === 'ENABLED' ? '已启用' : '已停用' } }
< / el-tag >
< el-tag :type = "roomStatusType(row.operationalStatus)" size = "small" >
{ { roomStatusLabel ( row . operationalStatus ) } }
< / el-tag >
< / div >
< / template >
< / el-table-column >
< el-table-column label = "标签" min -width = " 150 " >
< template # default = "{ row }" >
< div class = "profile-badges" >
< el-tag v-for = "tag in row.tags.slice(0, 3)" :key="tag" size="small" effect="plain" > {{ tag }} < / el -tag >
< span v-if = "row.tags.length === 0" > - < / span >
< / div >
< / template >
< / el -table -column >
< el-table-column label = "操作" min -width = " 285 " fixed = "right" >
< template # default = "{ row }" >
< div class = "row-actions" >
< el-button link type = "primary" @click ="openRoomForm(row)" > 编辑 < / el -button >
< el-button link type = "primary" @click ="openStatusForm(row)" > 改状态 < / el -button >
< el-button link @click ="openDisabledPeriodForm(row)" > 停用时段 < / el -button >
< el-button link type = "danger" @click ="archiveRoom(row)" > 归档 < / el -button >
< / div >
< / template >
< / el-table-column >
< / el-table >
< / div >
< el-empty v-else class = "store-detail-empty" description = "请选择门店查看详情与房间" / >
< / div >
< / section >
< el-dialog v-model = "storeDialog.open" :title="storeDialog.id ? '编辑门店' : '新增门店'" width="min(860px, 94vw)" >
< el -form label -position = " top " class = "asset-form" >
< el-form-item label = "门店名称" > < el-input v-model = "storeForm.name" maxlength="128" / > < / el-form-item >
< el-form-item label = "营业状态" >
< el-select v-model = "storeForm.businessStatus" >
< el -option label = "营业中" value = "OPEN" / >
< el-option label = "已闭店" value = "CLOSED" / >
< el-option label = "暂停营业" value = "SUSPENDED" / >
< / el-select >
< / el-form-item >
< el-form-item label = "城市" > < el-input v-model = "storeForm.city" / > < / el-form-item >
< el-form-item label = "区县" > < el-input v-model = "storeForm.district" / > < / el-form-item >
< el-form-item label = "详细地址" class = "form-span-2" > < el-input v-model = "storeForm.address" / > < / el-form-item >
< el-form-item label = "联系电话" > < el-input v-model = "storeForm.contactPhone" / > < / el-form-item >
< el-form-item label = "时区" > < el-input v-model = "storeForm.timezone" / > < / el-form-item >
< el-form-item label = "经度" > < el-input-number v-model = "storeForm.longitude" :min="-180" :max="180" :precision="6" controls-position="right" / > < / el-form-item >
< el-form-item label = "纬度" > < el-input-number v-model = "storeForm.latitude" :min="-90" :max="90" :precision="6" controls-position="right" / > < / el-form-item >
< el-form-item label = "Wi-Fi 名称" > < el-input v-model = "storeForm.wifiSsid" / > < / el-form-item >
< el-form-item : label = "storeDialog.id ? 'Wi-Fi 新密码(留空保持原值)' : 'Wi-Fi 密码'" >
< el-input v-model = "storeForm.wifiPassword" type="password" show-password autocomplete="new-password" / >
< / el-form-item >
< el-form-item label = "通知地址" class = "form-span-2" > < el-input v-model = "storeForm.notificationUrl" placeholder="https://..." / > < / el-form-item >
< el-form-item label = "排序" > < el-input-number v-model = "storeForm.sortOrder" :min="-100000" :max="100000" / > < / el-form-item >
< / el-form >
< section class = "hours-editor" >
< strong > 每周营业时间 < / strong >
< div v-for = "hour in storeForm.businessHours" :key="hour.weekday" class="hours-editor-row" >
< span > { { weekdayLabel ( hour . weekday ) } } < / span >
< el-switch v-model = "hour.isClosed" active-text="休息" inactive-text="营业" / >
< el-time-select v-model = "hour.openTime" :disabled="hour.isClosed" start="00:00" step="00:15" end="23:45" placeholder="开门" / >
< el-time-select v-model = "hour.closeTime" :disabled="hour.isClosed" start="00:00" step="00:15" end="23:45" placeholder="闭店" / >
< / div >
< / section >
< template # footer >
< el-button @click ="storeDialog.open = false" > 取消 < / el -button >
< el-button type = "primary" :loading = "saving" @click ="saveStore" > 保存门店 < / el -button >
< / template >
< / el-dialog >
< el-dialog v-model = "roomDialog.open" :title="roomDialog.id ? '编辑房间' : '新增房间'" width="min(900px, 94vw)" >
< el -form label -position = " top " class = "asset-form room-form" >
< el-form-item label = "房号" > < el-input v-model = "roomForm.roomNo" / > < / el-form-item >
< el-form-item label = "房间名称" > < el-input v-model = "roomForm.name" / > < / el-form-item >
< el-form-item label = "分类" > < el-input v-model = "roomForm.categoryName" / > < / el-form-item >
< el-form-item label = "容纳人数" > < el-input-number v-model = "roomForm.capacity" :min="1" :max="100" / > < / el-form-item >
< el-form-item label = "基础价(元)" > < el-input-number v-model = "roomForm.basePriceYuan" :min="0" :precision="2" / > < / el-form-item >
< el-form-item label = "工作日价(元)" > < el-input-number v-model = "roomForm.weekdayPriceYuan" :min="0" :precision="2" / > < / el-form-item >
< el-form-item label = "节假日价(元)" > < el-input-number v-model = "roomForm.holidayPriceYuan" :min="0" :precision="2" / > < / el-form-item >
< el-form-item label = "过夜价(元)" > < el-input-number v-model = "roomForm.overnightPriceYuan" :min="0" :precision="2" / > < / el-form-item >
< el-form-item label = "全天价(元)" > < el-input-number v-model = "roomForm.fullDayPriceYuan" :min="0" :precision="2" / > < / el-form-item >
< el-form-item label = "最低消费(元)" > < el-input-number v-model = "roomForm.minimumSpendYuan" :min="0" :precision="2" / > < / el-form-item >
< el-form-item label = "押金(元)" > < el-input-number v-model = "roomForm.depositYuan" :min="0" :precision="2" / > < / el-form-item >
< el-form-item label = "最短预订(分钟)" > < el-input-number v-model = "roomForm.minimumMinutes" :min="15" :max="1440" :step="15" / > < / el-form-item >
< el-form-item label = "最早提前入场(分钟)" > < el-input-number v-model = "roomForm.maxAdvanceStartMinutes" :min="0" :max="1440" / > < / el-form-item >
< el-form-item label = "最多提前预订(天)" > < el-input-number v-model = "roomForm.maxAdvanceDays" :min="0" :max="365" / > < / el-form-item >
< el-form-item label = "配置状态" >
< el-select v-model = "roomForm.configurationStatus"><el-option label="启用" value="ENABLED" /><el-option label="停用" value="DISABLED" / > < / el-select >
< / el-form-item >
< el-form-item label = "运营房态" >
< el-select v-model = "roomForm.operationalStatus"><el-option v-for="item in operationalStatuses" :key="item.value" :label="item.label" :value="item.value" / > < / el-select >
< / el-form-item >
< el-form-item label = "标签" class = "form-span-2" >
< el-select v-model = "roomForm.tags" multiple filterable allow-create default-first-option placeholder="输入后回车添加标签" / >
< / el-form-item >
< el-form-item label = "图片 URL(每行一个)" class = "form-span-2" > < el-input v-model = "roomForm.imagesText" type="textarea" :rows="3" / > < / el-form-item >
< el-form-item label = "排序" > < el-input-number v-model = "roomForm.sortOrder" :min="-100000" :max="100000" / > < / el-form-item >
< / el-form >
< template # footer >
< el-button @click ="roomDialog.open = false" > 取消 < / el -button >
< el-button type = "primary" :loading = "saving" @click ="saveRoom" > 保存房间 < / el -button >
< / template >
< / el-dialog >
< el-dialog v-model = "statusDialog.open" title="变更房间状态" width="min(520px, 94vw)" >
< el -form label -position = " top " >
< el-form-item label = "配置状态" > < el-select v-model = "statusDialog.configurationStatus" clearable><el-option label="启用" value="ENABLED" /><el-option label="停用" value="DISABLED" / > < / el-select > < / el-form-item >
< el-form-item label = "运营房态" > < el-select v-model = "statusDialog.operationalStatus" clearable><el-option v-for="item in operationalStatuses" :key="item.value" :label="item.label" :value="item.value" / > < / el-select > < / el-form-item >
< el-form-item label = "变更原因" > < el-input v-model = "statusDialog.reason" type="textarea" maxlength="255" show -word -limit / > < / el-form-item >
< / el-form >
< template # footer > < el-button @click ="statusDialog.open = false" > 取消 < / el -button > < el-button type = "primary" :loading = "saving" @click ="saveRoomStatus" > 确认变更 < / el -button > < / template >
< / el-dialog >
< el-dialog v-model = "periodDialog.open" title="新增房间停用时段" width="min(560px, 94vw)" >
< el -form label -position = " top " >
< el-form-item label = "开始时间" > < el-date-picker v-model = "periodDialog.startsAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" / > < / el-form-item >
< el-form-item label = "结束时间" > < el-date-picker v-model = "periodDialog.endsAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" / > < / el-form-item >
< el-form-item label = "原因" > < el-input v-model = "periodDialog.reason" maxlength="255" / > < / el-form-item >
< / el-form >
< template # footer > < el-button @click ="periodDialog.open = false" > 取消 < / el -button > < el-button type = "primary" :loading = "saving" @click ="saveDisabledPeriod" > 保存时段 < / el -button > < / template >
< / el-dialog >
< / section >
< / template >
< script setup lang = "ts" >
import { computed , reactive , ref , watch } from 'vue' ;
import { ElMessage , ElMessageBox } from 'element-plus' ;
import { Archive , Edit3 , Plus , RefreshCw } from '@lucide/vue' ;
import {
ApiError ,
addManagedRoomDisabledPeriod ,
archiveManagedRoom ,
archiveManagedStore ,
createManagedRoom ,
createManagedStore ,
listManagedRooms ,
listManagedStores ,
updateManagedRoom ,
updateManagedRoomStatus ,
updateManagedStore ,
type ApiSession
} from '../api' ;
import { money } from '../format' ;
import type {
ManagedRoom ,
ManagedStore ,
RoomConfigurationStatus ,
RoomInput ,
RoomOperationalStatus ,
StoreInput
} from '../types' ;
const props = defineProps < { session : ApiSession } > ( ) ;
const stores = ref < ManagedStore [ ] > ( [ ] ) ;
const rooms = ref < ManagedRoom [ ] > ( [ ] ) ;
const selectedStoreId = ref ( '' ) ;
const search = ref ( '' ) ;
const statusFilter = ref < ManagedStore [ ' businessStatus ' ] | ' ' > ( '' ) ;
const roomSearch = ref ( '' ) ;
const roomStatusFilter = ref < RoomOperationalStatus | ' ' > ( '' ) ;
const loadingStores = ref ( false ) ;
const loadingRooms = ref ( false ) ;
const saving = ref ( false ) ;
const lastError = ref ( '' ) ;
const storeDialog = reactive ( { open : false , id : '' } ) ;
const roomDialog = reactive ( { open : false , id : '' } ) ;
const statusDialog = reactive < {
open : boolean ; roomId : string ; configurationStatus : RoomConfigurationStatus | '' ;
operationalStatus : RoomOperationalStatus | '' ; reason : string ;
} > ( { open : false , roomId : '' , configurationStatus : '' , operationalStatus : '' , reason : '' } ) ;
const periodDialog = reactive ( { open : false , roomId : '' , startsAt : '' , endsAt : '' , reason : '' } ) ;
const operationalStatuses : Array < { value : RoomOperationalStatus ; label : string } > = [
{ value : 'AVAILABLE' , label : '空闲' } ,
{ value : 'MAINTENANCE' , label : '维护中' } ,
{ value : 'RESERVED' , label : '已预订' } ,
{ value : 'IN_USE' , label : '使用中' } ,
{ value : 'CLEANING_REQUIRED' , label : '待清洁' }
] ;
type StoreHourForm = { weekday : number ; openTime : string ; closeTime : string ; isClosed : boolean } ;
const storeForm = reactive ( {
name : '' , address : '' , city : '' , district : '' , longitude : null as number | null ,
latitude : null as number | null , contactPhone : '' , timezone : 'Asia/Shanghai' ,
businessStatus : 'OPEN' as ManagedStore [ 'businessStatus' ] , wifiSsid : '' , wifiPassword : '' ,
notificationUrl : '' , sortOrder : 0 , businessHours : defaultHourForms ( ) as StoreHourForm [ ]
} ) ;
const roomForm = reactive ( {
categoryName : '标准间' , name : '' , roomNo : '' , capacity : 4 ,
basePriceYuan : 0 , weekdayPriceYuan : 0 , holidayPriceYuan : 0 , overnightPriceYuan : 0 ,
fullDayPriceYuan : 0 , minimumSpendYuan : 0 , depositYuan : 0 , minimumMinutes : 60 ,
maxAdvanceStartMinutes : 0 , maxAdvanceDays : 30 ,
configurationStatus : 'ENABLED' as RoomConfigurationStatus ,
operationalStatus : 'AVAILABLE' as RoomOperationalStatus ,
tags : [ ] as string [ ] , imagesText : '' , sortOrder : 0
} ) ;
const filteredStores = computed ( ( ) => {
const keyword = search . value . trim ( ) . toLowerCase ( ) ;
return stores . value . filter ( ( store ) => {
const matchesStatus = ! statusFilter . value || store . businessStatus === statusFilter . value ;
const haystack = ` ${ store . name } ${ store . city } ${ store . district } ${ store . address } ` . toLowerCase ( ) ;
return matchesStatus && ( ! keyword || haystack . includes ( keyword ) ) ;
} ) ;
} ) ;
const selectedStore = computed ( ( ) => stores . value . find ( ( store ) => store . id === selectedStoreId . value ) ) ;
const filteredRooms = computed ( ( ) => {
const keyword = roomSearch . value . trim ( ) . toLowerCase ( ) ;
return rooms . value . filter ( ( room ) => {
const matchesStatus = ! roomStatusFilter . value || room . operationalStatus === roomStatusFilter . value ;
const haystack = ` ${ room . roomNo } ${ room . name } ${ room . categoryName } ${ room . tags . join ( ' ' ) } ` . toLowerCase ( ) ;
return matchesStatus && ( ! keyword || haystack . includes ( keyword ) ) ;
} ) ;
} ) ;
async function runAction < T > ( work : ( ) => Promise < T > , success ? : string ) {
lastError . value = '' ;
try {
const result = await work ( ) ;
if ( success ) ElMessage . success ( success ) ;
return result ;
} catch ( error ) {
lastError . value = error instanceof ApiError
? ` ${ error . code } ${ error . traceId ? ` · ${ error . traceId } ` : '' } `
: error instanceof Error ? error . message : '操作失败' ;
throw error ;
}
}
async function loadStores ( ) {
if ( ! props . session . token ) return ;
loadingStores . value = true ;
try {
const result = await runAction ( ( ) => listManagedStores ( props . session ) ) ;
stores . value = result ;
if ( ! result . some ( ( store ) => store . id === selectedStoreId . value ) ) {
selectedStoreId . value = result [ 0 ] ? . id || '' ;
}
if ( selectedStoreId . value ) await loadRooms ( ) ;
} catch { /* error already displayed */ } finally { loadingStores . value = false ; }
}
async function loadRooms ( ) {
if ( ! selectedStoreId . value || ! props . session . token ) { rooms . value = [ ] ; return ; }
loadingRooms . value = true ;
try { rooms . value = await runAction ( ( ) => listManagedRooms ( props . session , selectedStoreId . value ) ) ; }
catch { rooms . value = [ ] ; }
finally { loadingRooms . value = false ; }
}
function selectStore ( storeId : string ) {
if ( selectedStoreId . value === storeId ) return ;
selectedStoreId . value = storeId ;
roomSearch . value = '' ;
roomStatusFilter . value = '' ;
void loadRooms ( ) ;
}
function openStoreForm ( store ? : ManagedStore ) {
storeDialog . id = store ? . id || '' ;
Object . assign ( storeForm , {
name : store ? . name || '' , address : store ? . address || '' , city : store ? . city || '' ,
district : store ? . district || '' , longitude : store ? . longitude ? ? null ,
latitude : store ? . latitude ? ? null , contactPhone : store ? . contactPhone || '' ,
timezone : store ? . timezone || 'Asia/Shanghai' , businessStatus : store ? . businessStatus || 'OPEN' ,
wifiSsid : store ? . wifiSsid || '' , wifiPassword : '' , notificationUrl : store ? . notificationUrl || '' ,
sortOrder : store ? . sortOrder || 0 ,
businessHours : normalizedHours ( store ? . businessHours || [ ] ) . map ( ( hour ) => ( {
weekday : hour . weekday , openTime : minuteLabel ( hour . openMinute ) ,
closeTime : minuteLabel ( hour . closeMinute ) , isClosed : hour . isClosed
} ) )
} ) ;
storeDialog . open = true ;
}
async function saveStore ( ) {
if ( ! storeForm . name . trim ( ) ) return ElMessage . warning ( '请填写门店名称' ) ;
saving . value = true ;
const input : StoreInput = {
name : storeForm . name . trim ( ) , address : storeForm . address . trim ( ) , city : storeForm . city . trim ( ) ,
district : storeForm . district . trim ( ) , longitude : storeForm . longitude ,
latitude : storeForm . latitude , contactPhone : storeForm . contactPhone . trim ( ) ,
timezone : storeForm . timezone . trim ( ) || 'Asia/Shanghai' , businessStatus : storeForm . businessStatus ,
wifiSsid : storeForm . wifiSsid . trim ( ) , notificationUrl : storeForm . notificationUrl . trim ( ) ,
sortOrder : storeForm . sortOrder ,
businessHours : storeForm . businessHours . map ( ( hour ) => ( {
weekday : hour . weekday , openMinute : timeToMinute ( hour . openTime ) ,
closeMinute : timeToMinute ( hour . closeTime ) , isClosed : hour . isClosed
} ) )
} ;
if ( storeForm . wifiPassword ) input . wifiPassword = storeForm . wifiPassword ;
try {
const result = storeDialog . id
? await runAction ( ( ) => updateManagedStore ( props . session , storeDialog . id , input ) , '门店已更新' )
: await runAction ( ( ) => createManagedStore ( props . session , input ) , '门店已创建' ) ;
storeDialog . open = false ;
selectedStoreId . value = result . storeId ;
await loadStores ( ) ;
} catch { /* error already displayed */ } finally { saving . value = false ; }
}
async function archiveStore ( store : ManagedStore ) {
try {
await ElMessageBox . confirm ( '仅无未归档房间的门店可归档,操作会写入审计日志。' , ` 归档 ${ store . name } ` , { type : 'warning' } ) ;
await runAction ( ( ) => archiveManagedStore ( props . session , store . id ) , '门店已归档' ) ;
await loadStores ( ) ;
} catch ( error ) { if ( error !== 'cancel' && error !== 'close' && error instanceof ApiError ) lastError . value = error . code ; }
}
function openRoomForm ( room ? : ManagedRoom ) {
roomDialog . id = room ? . id || '' ;
Object . assign ( roomForm , {
categoryName : room ? . categoryName || '标准间' , name : room ? . name || '' , roomNo : room ? . roomNo || '' ,
capacity : room ? . capacity || 4 , basePriceYuan : centsToYuan ( room ? . basePriceCents ) ,
weekdayPriceYuan : centsToYuan ( room ? . weekdayPriceCents ) , holidayPriceYuan : centsToYuan ( room ? . holidayPriceCents ) ,
overnightPriceYuan : centsToYuan ( room ? . overnightPriceCents ) , fullDayPriceYuan : centsToYuan ( room ? . fullDayPriceCents ) ,
minimumSpendYuan : centsToYuan ( room ? . minimumSpendCents ) , depositYuan : centsToYuan ( room ? . depositCents ) ,
minimumMinutes : room ? . minimumMinutes || 60 , maxAdvanceStartMinutes : room ? . maxAdvanceStartMinutes || 0 ,
maxAdvanceDays : room ? . maxAdvanceDays ? ? 30 , configurationStatus : room ? . configurationStatus || 'ENABLED' ,
operationalStatus : room ? . operationalStatus || 'AVAILABLE' , tags : [ ... ( room ? . tags || [ ] ) ] ,
imagesText : ( room ? . images || [ ] ) . join ( '\n' ) , sortOrder : room ? . sortOrder || 0
} ) ;
roomDialog . open = true ;
}
async function saveRoom ( ) {
if ( ! selectedStoreId . value || ! roomForm . name . trim ( ) || ! roomForm . roomNo . trim ( ) ) {
return ElMessage . warning ( '请填写房号和房间名称' ) ;
}
const input : RoomInput = {
storeId : selectedStoreId . value , categoryName : roomForm . categoryName . trim ( ) ,
name : roomForm . name . trim ( ) , roomNo : roomForm . roomNo . trim ( ) , capacity : roomForm . capacity ,
basePriceCents : yuanToCents ( roomForm . basePriceYuan ) , weekdayPriceCents : yuanToCents ( roomForm . weekdayPriceYuan ) ,
holidayPriceCents : yuanToCents ( roomForm . holidayPriceYuan ) , overnightPriceCents : yuanToCents ( roomForm . overnightPriceYuan ) ,
fullDayPriceCents : yuanToCents ( roomForm . fullDayPriceYuan ) , minimumSpendCents : yuanToCents ( roomForm . minimumSpendYuan ) ,
depositCents : yuanToCents ( roomForm . depositYuan ) , minimumMinutes : roomForm . minimumMinutes ,
maxAdvanceStartMinutes : roomForm . maxAdvanceStartMinutes , maxAdvanceDays : roomForm . maxAdvanceDays ,
configurationStatus : roomForm . configurationStatus , operationalStatus : roomForm . operationalStatus ,
tags : roomForm . tags . map ( ( item ) => item . trim ( ) ) . filter ( Boolean ) ,
images : roomForm . imagesText . split ( /\r?\n/ ) . map ( ( item ) => item . trim ( ) ) . filter ( Boolean ) ,
sortOrder : roomForm . sortOrder
} ;
saving . value = true ;
try {
if ( roomDialog . id ) await runAction ( ( ) => updateManagedRoom ( props . session , roomDialog . id , input ) , '房间已更新' ) ;
else await runAction ( ( ) => createManagedRoom ( props . session , input ) , '房间已创建' ) ;
roomDialog . open = false ;
await loadRooms ( ) ;
} catch { /* error already displayed */ } finally { saving . value = false ; }
}
function openStatusForm ( room : ManagedRoom ) {
Object . assign ( statusDialog , {
open : true , roomId : room . id , configurationStatus : room . configurationStatus ,
operationalStatus : room . operationalStatus , reason : ''
} ) ;
}
async function saveRoomStatus ( ) {
if ( ! statusDialog . reason . trim ( ) ) return ElMessage . warning ( '请填写状态变更原因' ) ;
saving . value = true ;
try {
await runAction ( ( ) => updateManagedRoomStatus ( props . session , statusDialog . roomId , {
storeId : selectedStoreId . value ,
configurationStatus : statusDialog . configurationStatus || undefined ,
operationalStatus : statusDialog . operationalStatus || undefined ,
reason : statusDialog . reason . trim ( )
} ) , '房间状态已更新' ) ;
statusDialog . open = false ;
await loadRooms ( ) ;
} catch { /* error already displayed */ } finally { saving . value = false ; }
}
function openDisabledPeriodForm ( room : ManagedRoom ) {
Object . assign ( periodDialog , { open : true , roomId : room . id , startsAt : '' , endsAt : '' , reason : '' } ) ;
}
async function saveDisabledPeriod ( ) {
if ( ! periodDialog . startsAt || ! periodDialog . endsAt ) return ElMessage . warning ( '请选择完整停用时段' ) ;
saving . value = true ;
try {
await runAction ( ( ) => addManagedRoomDisabledPeriod ( props . session , periodDialog . roomId , {
storeId : selectedStoreId . value , startsAt : periodDialog . startsAt ,
endsAt : periodDialog . endsAt , reason : periodDialog . reason . trim ( )
} ) , '停用时段已添加' ) ;
periodDialog . open = false ;
} catch { /* error already displayed */ } finally { saving . value = false ; }
}
async function archiveRoom ( room : ManagedRoom ) {
try {
await ElMessageBox . confirm ( '归档后该房间不再出现在可用房间列表。' , ` 归档 ${ room . roomNo } ` , { type : 'warning' } ) ;
await runAction ( ( ) => archiveManagedRoom ( props . session , room . id , selectedStoreId . value ) , '房间已归档' ) ;
await loadRooms ( ) ;
} catch ( error ) { if ( error !== 'cancel' && error !== 'close' && error instanceof ApiError ) lastError . value = error . code ; }
}
function defaultHourForms ( ) : StoreHourForm [ ] {
return Array . from ( { length : 7 } , ( _ , index ) => ( {
weekday : index + 1 , openTime : '09:00' , closeTime : '22:00' , isClosed : false
} ) ) ;
}
function normalizedHours ( hours : ManagedStore [ 'businessHours' ] ) {
const byDay = new Map ( hours . map ( ( hour ) => [ hour . weekday , hour ] ) ) ;
return Array . from ( { length : 7 } , ( _ , index ) => byDay . get ( index + 1 ) || {
weekday : index + 1 , openMinute : 540 , closeMinute : 1320 , isClosed : false
} ) ;
}
function weekdayLabel ( weekday : number ) { return [ '一' , '二' , '三' , '四' , '五' , '六' , '日' ] [ weekday - 1 ] ? ` 周 ${ [ '一' , '二' , '三' , '四' , '五' , '六' , '日' ] [ weekday - 1 ] } ` : ` 周 ${ weekday } ` ; }
function minuteLabel ( value : number ) { return ` ${ String ( Math . floor ( value / 60 ) ) . padStart ( 2 , '0' ) } : ${ String ( value % 60 ) . padStart ( 2 , '0' ) } ` ; }
function timeToMinute ( value : string ) { const [ hour , minute ] = value . split ( ':' ) . map ( Number ) ; return ( hour || 0 ) * 60 + ( minute || 0 ) ; }
function centsToYuan ( value ? : number ) { return Number ( ( ( value || 0 ) / 100 ) . toFixed ( 2 ) ) ; }
function yuanToCents ( value : number ) { return Math . round ( Number ( value || 0 ) * 100 ) ; }
function storeStatusLabel ( status : ManagedStore [ 'businessStatus' ] ) { return ( { OPEN : '营业中' , CLOSED : '已闭店' , SUSPENDED : '暂停营业' } as const ) [ status ] ; }
function storeStatusType ( status : ManagedStore [ 'businessStatus' ] ) { return status === 'OPEN' ? 'success' : status === 'SUSPENDED' ? 'warning' : 'info' ; }
function roomStatusLabel ( status : RoomOperationalStatus ) { return operationalStatuses . find ( ( item ) => item . value === status ) ? . label || status ; }
function roomStatusType ( status : RoomOperationalStatus ) { return status === 'AVAILABLE' ? 'success' : status === 'MAINTENANCE' || status === 'CLEANING_REQUIRED' ? 'warning' : 'primary' ; }
function wifiSummary ( store : ManagedStore ) { return store . wifiSsid ? ` ${ store . wifiSsid } ${ store . wifiConfigured ? ' · 已设密码' : '' } ` : '未配置' ; }
watch ( ( ) => props . session . token , ( token ) => { if ( token ) void loadStores ( ) ; } , { immediate : true } ) ;
< / script >