@@ -1,528 +0,0 @@
< template >
< view class = "page" : style = "{ paddingTop: navBarHeight }" >
< CustomNavBar title = "排课模板" show -back / >
<!-- Toolbar -- >
< view class = "toolbar" >
< text class = "toolbar-hint" > 共 { { templates . length } } 条模板 < / text >
< view class = "add-btn" @tap ="openAdd" >
< text class = "add-btn-text" > + 新增时段 < / text >
< / view >
< / view >
<!-- Loading skeleton -- >
< view v-if = "loading" class="skeleton-list" >
< view v-for = "i in 5" :key="i" class="skeleton-item" / >
< / view >
<!-- Empty -- >
< view v-else-if = "!templates.length" class="empty-state" >
< text class = "empty-icon" > 📅 < / text >
< text class = "empty-text" > 暂无模板 , 点击右上角新增 < / text >
< / view >
<!-- Template list grouped by weekday -- >
< view v-else >
< view v-for = "(group, day) in grouped" :key="day" class="day-group" >
< view class = "day-header" >
< text class = "day-label" > { { WEEKDAY _LABELS [ Number ( day ) ] } } < / text >
< text class = "day-count" > { { group . length } } 个时段 < / text >
< / view >
< view
v-for = "tpl in group"
: key = "tpl.id ?? tpl._key"
class = "tpl-row"
: class = "{ 'tpl-row--inactive': !tpl.isActive }"
>
< view class = "tpl-time" >
< text class = "tpl-time-text" > { { tpl . startTime } } – { { tpl . endTime } } < / text >
< text class = "tpl-capacity" > { { tpl . capacity } } 人 < / text >
< / view >
< view class = "tpl-actions" >
< view
class = "tpl-toggle"
: class = "tpl.isActive ? 'toggle--on' : 'toggle--off'"
@tap ="toggleTemplate(tpl)"
>
< text class = "tpl-toggle-text" > { { tpl . isActive ? '启用' : '停用' } } < / text >
< / view >
< view class = "tpl-edit" @tap ="openEdit(tpl)" >
< text class = "tpl-edit-text" > 编辑 < / text >
< / view >
< view class = "tpl-delete" @tap ="deleteTemplate(tpl)" >
< text class = "tpl-delete-text" > 删除 < / text >
< / view >
< / view >
< / view >
< / view >
< / view >
<!-- Save bar -- >
< view v-if = "isDirty" class="save-bar" >
< view class = "save-btn" : class = "{ 'save-btn--loading': saving }" @tap ="handleSave" >
< text class = "save-btn-text" > { { saving ? '保存中...' : '保存全部更改' } } < / text >
< / view >
< / view >
<!-- Add / Edit modal -- >
< view v-if = "showModal" class="modal-mask" @tap.self="closeModal" >
< view class = "modal" >
< text class = "modal-title" > { { editTarget ? '编辑时段' : '新增时段' } } < / text >
< view class = "modal-field" >
< text class = "modal-label" > 星期 < / text >
< picker
mode = "selector"
:range = "dayOptions"
range -key = " label "
:value = "form.dayIdx"
@change ="(e: any) => form.dayIdx = Number(e.detail.value)"
>
< view class = "picker-display" >
< text class = "picker-text" > { { dayOptions [ form . dayIdx ] . label } } < / text >
< text class = "picker-arrow" > › < / text >
< / view >
< / picker >
< / view >
< view class = "modal-field" >
< text class = "modal-label" > 开始时间 < / text >
< picker
mode = "time"
:value = "form.startTime"
@change ="(e: any) => form.startTime = e.detail.value"
>
< view class = "picker-display" >
< text class = "picker-text" > { { form . startTime || '请选择' } } < / text >
< text class = "picker-arrow" > › < / text >
< / view >
< / picker >
< / view >
< view class = "modal-field" >
< text class = "modal-label" > 结束时间 < / text >
< picker
mode = "time"
:value = "form.endTime"
@change ="(e: any) => form.endTime = e.detail.value"
>
< view class = "picker-display" >
< text class = "picker-text" > { { form . endTime || '请选择' } } < / text >
< text class = "picker-arrow" > › < / text >
< / view >
< / picker >
< / view >
< view class = "modal-field modal-field--last" >
< text class = "modal-label" > 容量 < / text >
< input
class = "modal-input"
type = "number"
v-model = "form.capacityStr"
placeholder = "如: 10"
placeholder -style = " color : # bbb "
/ >
< / view >
< view class = "modal-actions" >
< view class = "modal-cancel" @tap ="closeModal" >
< text class = "modal-cancel-text" > 取消 < / text >
< / view >
< view class = "modal-confirm" @tap ="submitForm" >
< text class = "modal-confirm-text" > 确认 < / text >
< / view >
< / view >
< / view >
< / view >
< / view >
< / template >
< script setup lang = "ts" >
import { ref , computed , onMounted } from 'vue'
import CustomNavBar from '../../components/CustomNavBar.vue'
import { getSystemLayout } from '../../utils/system'
import { useAdminStore } from '../../stores/admin'
import { WEEKDAY _LABELS } from '@mp-pilates/shared'
import type { WeekTemplate } from '@mp-pilates/shared'
type LocalTemplate = Partial < WeekTemplate > & {
_key ? : string
dayOfWeek : number
startTime : string
endTime : string
capacity : number
isActive : boolean
}
const adminStore = useAdminStore ( )
const navBarHeight = ref ( '64px' )
const loading = ref ( false )
const saving = ref ( false )
const isDirty = ref ( false )
const showModal = ref ( false )
const editTarget = ref < LocalTemplate | null > ( null )
const templates = ref < LocalTemplate [ ] > ( [ ] )
const dayOptions = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 ] . map ( ( d ) => ( { label : WEEKDAY _LABELS [ d ] , value : d } ) )
const form = ref ( {
dayIdx : 0 ,
startTime : '08:00' ,
endTime : '09:00' ,
capacityStr : '1' ,
} )
const grouped = computed ( ( ) => {
const map : Record < number , LocalTemplate [ ] > = { }
for ( const tpl of templates . value ) {
if ( ! map [ tpl . dayOfWeek ] ) map [ tpl . dayOfWeek ] = [ ]
map [ tpl . dayOfWeek ] . push ( tpl )
}
// Sort by day
return Object . fromEntries (
Object . entries ( map ) . sort ( ( [ a ] , [ b ] ) => Number ( a ) - Number ( b ) ) ,
)
} )
/** 生成默认模板: 周一到周日, 8:00-22:00 每小时一个时段 */
function generateDefaultTemplates ( ) : LocalTemplate [ ] {
const defaults : LocalTemplate [ ] = [ ]
for ( let day = 1 ; day <= 7 ; day ++ ) {
for ( let hour = 8 ; hour < 22 ; hour ++ ) {
const start = String ( hour ) . padStart ( 2 , '0' ) + ':00'
const end = String ( hour + 1 ) . padStart ( 2 , '0' ) + ':00'
defaults . push ( {
_key : ` default- ${ day } - ${ start } ` ,
dayOfWeek : day ,
startTime : start ,
endTime : end ,
capacity : 1 ,
isActive : true ,
} )
}
}
return defaults
}
async function fetchTemplates ( ) {
loading . value = true
try {
const data = await adminStore . fetchWeekTemplates ( )
if ( data . length === 0 ) {
// No templates yet — pre-fill with defaults
templates . value = generateDefaultTemplates ( )
isDirty . value = true
} else {
templates . value = data
isDirty . value = false
}
} catch {
uni . showToast ( { title : '加载失败' , icon : 'none' } )
} finally {
loading . value = false
}
}
function openAdd ( ) {
editTarget . value = null
form . value = { dayIdx : 0 , startTime : '08:00' , endTime : '09:00' , capacityStr : '1' }
showModal . value = true
}
function openEdit ( tpl : LocalTemplate ) {
editTarget . value = tpl
const dayIdx = dayOptions . findIndex ( ( d ) => d . value === tpl . dayOfWeek )
form . value = {
dayIdx : dayIdx >= 0 ? dayIdx : 0 ,
startTime : tpl . startTime ,
endTime : tpl . endTime ,
capacityStr : String ( tpl . capacity ) ,
}
showModal . value = true
}
function closeModal ( ) {
showModal . value = false
editTarget . value = null
}
function submitForm ( ) {
const capacity = parseInt ( form . value . capacityStr , 10 )
if ( ! form . value . startTime || ! form . value . endTime ) {
uni . showToast ( { title : '请填写时间' , icon : 'none' } )
return
}
if ( isNaN ( capacity ) || capacity < 1 ) {
uni . showToast ( { title : '请填写有效容量' , icon : 'none' } )
return
}
const day = dayOptions [ form . value . dayIdx ] . value
if ( editTarget . value ) {
const tpl = editTarget . value
tpl . dayOfWeek = day
tpl . startTime = form . value . startTime
tpl . endTime = form . value . endTime
tpl . capacity = capacity
} else {
templates . value . push ( {
_key : String ( Date . now ( ) ) ,
dayOfWeek : day ,
startTime : form . value . startTime ,
endTime : form . value . endTime ,
capacity ,
isActive : true ,
} )
}
isDirty . value = true
closeModal ( )
}
function toggleTemplate ( tpl : LocalTemplate ) {
tpl . isActive = ! tpl . isActive
isDirty . value = true
}
function deleteTemplate ( tpl : LocalTemplate ) {
uni . showModal ( {
title : '确认删除' ,
content : '删除该时段模板?' ,
success : ( res ) => {
if ( res . confirm ) {
const idx = templates . value . indexOf ( tpl )
if ( idx >= 0 ) templates . value . splice ( idx , 1 )
isDirty . value = true
}
} ,
} )
}
async function handleSave ( ) {
if ( saving . value ) return
saving . value = true
try {
const payload = templates . value . map ( ( t ) => ( {
id : t . id ,
dayOfWeek : t . dayOfWeek ,
startTime : t . startTime ,
endTime : t . endTime ,
capacity : t . capacity ,
isActive : t . isActive ,
} ) )
await adminStore . saveWeekTemplates ( payload as any )
isDirty . value = false
uni . showToast ( { title : '保存成功' , icon : 'success' } )
await fetchTemplates ( )
} catch ( e : any ) {
uni . showToast ( { title : e ? . message ? ? '保存失败' , icon : 'none' } )
} finally {
saving . value = false
}
}
onMounted ( ( ) => {
navBarHeight . value = ` ${ getSystemLayout ( ) . navBarHeight } px `
fetchTemplates ( )
} )
< / script >
< style lang = "scss" scoped >
. page {
min - height : 100 vh ;
background : # f5f3f0 ;
padding - bottom : 120 rpx ;
}
/* ── Toolbar ─────────────────────────────── */
. toolbar {
display : flex ;
align - items : center ;
justify - content : space - between ;
padding : 24 rpx 24 rpx 16 rpx ;
}
. toolbar - hint { font - size : 24 rpx ; color : # 999 ; }
. add - btn {
background : # 1 a1a2e ;
border - radius : 32 rpx ;
padding : 12 rpx 28 rpx ;
}
. add - btn - text { font - size : 26 rpx ; font - weight : 600 ; color : $primary - dark ; }
/* ── Skeleton ────────────────────────────── */
. skeleton - list { padding : 0 24 rpx ; }
. skeleton - item {
height : 80 rpx ;
border - radius : 12 rpx ;
margin - bottom : 16 rpx ;
background : linear - gradient ( 90 deg , # f0f0f0 25 % , # e0e0e0 50 % , # f0f0f0 75 % ) ;
background - size : 400 % 100 % ;
animation : shimmer 1.4 s infinite ;
}
/* ── Empty ───────────────────────────────── */
. empty - state {
display : flex ;
flex - direction : column ;
align - items : center ;
padding : 100 rpx 0 ;
gap : 20 rpx ;
}
. empty - icon { font - size : 80 rpx ; }
. empty - text { font - size : 28 rpx ; color : # bbb ; }
/* ── Day group ───────────────────────────── */
. day - group { margin : 0 24 rpx 24 rpx ; }
. day - header {
display : flex ;
align - items : center ;
justify - content : space - between ;
padding : 16 rpx 8 rpx ;
}
. day - label { font - size : 28 rpx ; font - weight : 700 ; color : # 1 a1a2e ; }
. day - count { font - size : 22 rpx ; color : # 999 ; }
/* ── Template row ────────────────────────── */
. tpl - row {
background : # ffffff ;
border - radius : 12 rpx ;
padding : 20 rpx 24 rpx ;
display : flex ;
align - items : center ;
justify - content : space - between ;
margin - bottom : 12 rpx ;
box - shadow : 0 2 rpx 8 rpx rgba ( 0 , 0 , 0 , 0.06 ) ;
& -- inactive { opacity : 0.5 ; }
}
. tpl - time { display : flex ; flex - direction : column ; gap : 6 rpx ; }
. tpl - time - text { font - size : 28 rpx ; font - weight : 600 ; color : # 1 a1a2e ; }
. tpl - capacity { font - size : 22 rpx ; color : # 888 ; }
. tpl - actions { display : flex ; gap : 12 rpx ; }
. tpl - toggle ,
. tpl - edit ,
. tpl - delete {
border - radius : 20 rpx ;
padding : 8 rpx 20 rpx ;
}
. toggle -- on { background : rgba ( 39 , 174 , 96 , 0.12 ) ; }
. toggle -- on . tpl - toggle - text { font - size : 24 rpx ; color : # 27 ae60 ; }
. toggle -- off { background : rgba ( 230 , 126 , 34 , 0.12 ) ; }
. toggle -- off . tpl - toggle - text { font - size : 24 rpx ; color : # e67e22 ; }
. tpl - edit { background : rgba ( 26 , 26 , 46 , 0.08 ) ; }
. tpl - edit - text { font - size : 24 rpx ; color : # 1 a1a2e ; }
. tpl - delete { background : rgba ( 192 , 57 , 43 , 0.08 ) ; }
. tpl - delete - text { font - size : 24 rpx ; color : # c0392b ; }
/* ── Save bar ────────────────────────────── */
. save - bar {
position : fixed ;
bottom : 0 ;
left : 0 ;
right : 0 ;
padding : 20 rpx 24 rpx 48 rpx ;
background : # ffffff ;
box - shadow : 0 - 4 rpx 20 rpx rgba ( 0 , 0 , 0 , 0.08 ) ;
}
. save - btn {
width : 100 % ;
height : 96 rpx ;
border - radius : 48 rpx ;
background : linear - gradient ( 90 deg , # 1 a1a2e , # 2 d2d5e ) ;
display : flex ;
align - items : center ;
justify - content : center ;
& -- loading { opacity : 0.6 ; }
}
. save - btn - text { font - size : 30 rpx ; font - weight : 700 ; color : $primary - dark ; }
/* ── Modal ───────────────────────────────── */
. modal - mask {
position : fixed ;
inset : 0 ;
background : rgba ( 0 , 0 , 0 , 0.5 ) ;
display : flex ;
align - items : flex - end ;
z - index : 100 ;
}
. modal {
width : 100 % ;
background : # ffffff ;
border - radius : 24 rpx 24 rpx 0 0 ;
padding : 40 rpx 32 rpx 60 rpx ;
}
. modal - title {
font - size : 32 rpx ;
font - weight : 700 ;
color : # 1 a1a2e ;
display : block ;
margin - bottom : 24 rpx ;
}
. modal - field {
display : flex ;
align - items : center ;
justify - content : space - between ;
padding : 24 rpx 0 ;
border - bottom : 1 rpx solid # f5f5f5 ;
& -- last { border - bottom : none ; }
}
. modal - label { font - size : 26 rpx ; color : # 555 ; width : 140 rpx ; flex - shrink : 0 ; }
. modal - input { flex : 1 ; text - align : right ; font - size : 26 rpx ; color : # 222 ; }
. picker - display { display : flex ; align - items : center ; gap : 8 rpx ; }
. picker - text { font - size : 26 rpx ; color : # 222 ; }
. picker - arrow { font - size : 26 rpx ; color : # bbb ; }
. modal - actions {
display : flex ;
gap : 16 rpx ;
margin - top : 32 rpx ;
}
. modal - cancel {
flex : 1 ;
height : 88 rpx ;
background : # f0f0f0 ;
border - radius : 44 rpx ;
display : flex ;
align - items : center ;
justify - content : center ;
}
. modal - cancel - text { font - size : 28 rpx ; color : # 555 ; }
. modal - confirm {
flex : 2 ;
height : 88 rpx ;
background : linear - gradient ( 90 deg , # 1 a1a2e , # 2 d2d5e ) ;
border - radius : 44 rpx ;
display : flex ;
align - items : center ;
justify - content : center ;
}
. modal - confirm - text { font - size : 28 rpx ; font - weight : 700 ; color : $primary - dark ; }
< / style >