feat(app): implement all sub-pages and admin management pages
Sub-pages: card purchase with WeChat Pay flow, my memberships with progress bars, my bookings with tabs, personal info editor Admin: management center grid, week template CRUD, slot adjustment, member management with search, order list with filters, card type CRUD with form modal, studio settings editor Admin Pinia store for all admin API calls
This commit is contained in:
@@ -3,107 +3,98 @@
|
||||
<!-- Tabs -->
|
||||
<view class="tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
v-for="(tab, i) in tabs"
|
||||
:key="i"
|
||||
class="tab"
|
||||
:class="{ 'tab--active': activeTab === tab.key }"
|
||||
@tap="activeTab = tab.key"
|
||||
:class="{ 'tab--active': activeTab === i }"
|
||||
@tap="activeTab = i"
|
||||
>
|
||||
<text class="tab-text">{{ tab.label }}</text>
|
||||
<text class="tab-text">{{ tab }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ── Tab: Manual add ───────────────────── -->
|
||||
<view v-if="activeTab === 'add'" class="section">
|
||||
<text class="section-title">手动新增时段</text>
|
||||
|
||||
<!-- ① Add slot -->
|
||||
<view v-if="activeTab === 0" class="panel">
|
||||
<view class="form-card">
|
||||
<view class="form-row">
|
||||
<text class="form-label">日期</text>
|
||||
<picker mode="date" :value="addForm.date" @change="(e: any) => addForm.date = e.detail.value">
|
||||
<view class="picker-display">
|
||||
<text class="picker-text">{{ addForm.date }}</text>
|
||||
<text class="picker-text">{{ addForm.date || '请选择' }}</text>
|
||||
<text class="picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-row">
|
||||
<text class="form-label">开始时间</text>
|
||||
<picker mode="time" :value="addForm.startTime" @change="(e: any) => addForm.startTime = e.detail.value">
|
||||
<view class="picker-display">
|
||||
<text class="picker-text">{{ addForm.startTime }}</text>
|
||||
<text class="picker-text">{{ addForm.startTime || '请选择' }}</text>
|
||||
<text class="picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-row">
|
||||
<text class="form-label">结束时间</text>
|
||||
<picker mode="time" :value="addForm.endTime" @change="(e: any) => addForm.endTime = e.detail.value">
|
||||
<view class="picker-display">
|
||||
<text class="picker-text">{{ addForm.endTime }}</text>
|
||||
<text class="picker-text">{{ addForm.endTime || '请选择' }}</text>
|
||||
<text class="picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-row form-row--last">
|
||||
<text class="form-label">容量(人)</text>
|
||||
<text class="form-label">容量</text>
|
||||
<input
|
||||
class="form-input"
|
||||
type="number"
|
||||
v-model="addForm.capacityStr"
|
||||
placeholder="默认10"
|
||||
placeholder="如:10"
|
||||
placeholder-style="color:#bbb"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="action-btn primary-btn"
|
||||
:class="{ 'primary-btn--loading': addingSlot }"
|
||||
@tap="handleAddSlot"
|
||||
>
|
||||
<text class="primary-btn-text">{{ addingSlot ? '添加中...' : '添加时段' }}</text>
|
||||
<view class="action-wrap">
|
||||
<view class="action-btn" :class="{ 'action-btn--loading': submitting }" @tap="submitAddSlot">
|
||||
<text class="action-btn-text">{{ submitting ? '提交中...' : '新增时段' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ── Tab: Close slots ──────────────────── -->
|
||||
<view v-else-if="activeTab === 'close'" class="section">
|
||||
<view class="search-row">
|
||||
<picker mode="date" :value="closeDateFilter" @change="(e: any) => { closeDateFilter = e.detail.value; fetchSlotsForClose() }">
|
||||
<view class="date-filter">
|
||||
<text class="date-filter-text">{{ closeDateFilter }}</text>
|
||||
<text class="date-filter-arrow">›</text>
|
||||
<!-- ② Close slot -->
|
||||
<view v-else-if="activeTab === 1" class="panel">
|
||||
<view class="date-picker-row">
|
||||
<picker mode="date" :value="closeDate" @change="(e: any) => { closeDate = e.detail.value; loadSlotsForClose() }">
|
||||
<view class="picker-display">
|
||||
<text class="picker-label">选择日期:</text>
|
||||
<text class="picker-text">{{ closeDate }}</text>
|
||||
<text class="picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view v-if="loadingClose" class="skeleton-list">
|
||||
<view v-for="i in 3" :key="i" class="skeleton-item" />
|
||||
<view v-if="slotsLoading" class="skeleton-list">
|
||||
<view v-for="i in 4" :key="i" class="skeleton-item" />
|
||||
</view>
|
||||
|
||||
<view v-else-if="!closeSlots.length" class="empty-state">
|
||||
<text class="empty-icon">🗓️</text>
|
||||
<view v-else-if="!daySlots.length" class="empty-state">
|
||||
<text class="empty-icon">📭</text>
|
||||
<text class="empty-text">该日暂无时段</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="slot-list">
|
||||
<view
|
||||
v-for="slot in closeSlots"
|
||||
:key="slot.id"
|
||||
class="slot-card"
|
||||
:class="{ 'slot-card--closed': slot.status === 'CLOSED' }"
|
||||
>
|
||||
<view v-for="slot in daySlots" :key="slot.id" class="slot-row">
|
||||
<view class="slot-info">
|
||||
<text class="slot-time">{{ slot.startTime.slice(0, 5) }}–{{ slot.endTime.slice(0, 5) }}</text>
|
||||
<text class="slot-cap">容量 {{ slot.capacity }} · 已预约 {{ slot.bookedCount }}</text>
|
||||
<text class="slot-time">{{ slot.startTime }} – {{ slot.endTime }}</text>
|
||||
<view class="slot-badge" :class="slotBadgeClass(slot.status)">
|
||||
<text class="slot-badge-text">{{ slot.status }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="slot-count">{{ slot.bookedCount }}/{{ slot.capacity }}</text>
|
||||
<view
|
||||
v-if="slot.status !== 'CLOSED'"
|
||||
class="close-btn"
|
||||
@tap="confirmClose(slot)"
|
||||
@tap="closeSlot(slot)"
|
||||
>
|
||||
<text class="close-btn-text">关闭</text>
|
||||
</view>
|
||||
@@ -114,114 +105,107 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ── Tab: Generate ─────────────────────── -->
|
||||
<view v-else-if="activeTab === 'generate'" class="section">
|
||||
<text class="section-title">按模板生成时段</text>
|
||||
<text class="section-sub">将依据当前排课模板,生成未来指定天数的课程时段(已存在的时段不会重复生成)。</text>
|
||||
|
||||
<!-- ③ Batch generate -->
|
||||
<view v-else class="panel">
|
||||
<view class="form-card">
|
||||
<view class="form-row">
|
||||
<text class="form-label">开始日期</text>
|
||||
<picker mode="date" :value="genForm.startDate" @change="(e: any) => genForm.startDate = e.detail.value">
|
||||
<view class="picker-display">
|
||||
<text class="picker-text">{{ genForm.startDate || '请选择' }}</text>
|
||||
<text class="picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-row form-row--last">
|
||||
<text class="form-label">生成天数</text>
|
||||
<input
|
||||
class="form-input"
|
||||
type="number"
|
||||
v-model="generateDaysStr"
|
||||
placeholder="如:14"
|
||||
placeholder-style="color:#bbb"
|
||||
/>
|
||||
<text class="form-label">结束日期</text>
|
||||
<picker mode="date" :value="genForm.endDate" @change="(e: any) => genForm.endDate = e.detail.value">
|
||||
<view class="picker-display">
|
||||
<text class="picker-text">{{ genForm.endDate || '请选择' }}</text>
|
||||
<text class="picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="action-btn primary-btn"
|
||||
:class="{ 'primary-btn--loading': generating }"
|
||||
@tap="handleGenerate"
|
||||
>
|
||||
<text class="primary-btn-text">{{ generating ? '生成中...' : '生成时段' }}</text>
|
||||
<text class="gen-hint">将根据排课模板,自动生成所选日期范围内的时段</text>
|
||||
<view class="action-wrap">
|
||||
<view class="action-btn" :class="{ 'action-btn--loading': submitting }" @tap="submitGenerate">
|
||||
<text class="action-btn-text">{{ submitting ? '生成中...' : '批量生成' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { get, post, put } from '../../utils/request'
|
||||
import { ref } from 'vue'
|
||||
import { useAdminStore } from '../../stores/admin'
|
||||
import { formatDate } from '../../utils/format'
|
||||
import type { TimeSlot } from '@mp-pilates/shared'
|
||||
|
||||
const tabs = [
|
||||
{ key: 'add', label: '新增时段' },
|
||||
{ key: 'close', label: '关闭时段' },
|
||||
{ key: 'generate', label: '批量生成' },
|
||||
]
|
||||
const adminStore = useAdminStore()
|
||||
|
||||
const activeTab = ref<string>('add')
|
||||
const tabs = ['新增时段', '关闭时段', '批量生成']
|
||||
const activeTab = ref(0)
|
||||
const submitting = ref(false)
|
||||
const slotsLoading = ref(false)
|
||||
|
||||
// ── Add slot form ─────────────────────────────────
|
||||
const todayStr = new Date().toISOString().slice(0, 10)
|
||||
// ── Add slot form ────────────────────────────────────────────────
|
||||
const addForm = ref({
|
||||
date: todayStr,
|
||||
date: formatDate(new Date()),
|
||||
startTime: '09:00',
|
||||
endTime: '10:00',
|
||||
capacityStr: '10',
|
||||
})
|
||||
const addingSlot = ref(false)
|
||||
|
||||
async function handleAddSlot() {
|
||||
if (addingSlot.value) return
|
||||
const capacity = parseInt(addForm.value.capacityStr, 10)
|
||||
async function submitAddSlot() {
|
||||
if (submitting.value) return
|
||||
if (!addForm.value.date || !addForm.value.startTime || !addForm.value.endTime) {
|
||||
uni.showToast({ title: '请完整填写信息', icon: 'none' })
|
||||
uni.showToast({ title: '请填写完整信息', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
addingSlot.value = true
|
||||
const capacity = parseInt(addForm.value.capacityStr, 10)
|
||||
submitting.value = true
|
||||
try {
|
||||
await post('/admin/time-slot/manual', {
|
||||
await adminStore.createManualSlot({
|
||||
date: addForm.value.date,
|
||||
startTime: addForm.value.startTime,
|
||||
endTime: addForm.value.endTime,
|
||||
capacity: isNaN(capacity) ? undefined : capacity,
|
||||
})
|
||||
uni.showToast({ title: '时段已添加', icon: 'success' })
|
||||
addForm.value = { date: todayStr, startTime: '09:00', endTime: '10:00', capacityStr: '10' }
|
||||
uni.showToast({ title: '新增成功', icon: 'success' })
|
||||
} catch (e: any) {
|
||||
uni.showToast({ title: e?.message ?? '添加失败', icon: 'none' })
|
||||
uni.showToast({ title: e?.message ?? '新增失败', icon: 'none' })
|
||||
} finally {
|
||||
addingSlot.value = false
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── Close slots ────────────────────────────────────
|
||||
interface SlotRow extends TimeSlot {
|
||||
bookedCount: number
|
||||
}
|
||||
// ── Close slot ────────────────────────────────────────────────────
|
||||
const closeDate = ref(formatDate(new Date()))
|
||||
const daySlots = ref<TimeSlot[]>([])
|
||||
|
||||
const closeDateFilter = ref(todayStr)
|
||||
const closeSlots = ref<SlotRow[]>([])
|
||||
const loadingClose = ref(false)
|
||||
|
||||
async function fetchSlotsForClose() {
|
||||
loadingClose.value = true
|
||||
async function loadSlotsForClose() {
|
||||
slotsLoading.value = true
|
||||
try {
|
||||
const data = await get<SlotRow[]>(`/admin/time-slots?date=${closeDateFilter.value}`)
|
||||
closeSlots.value = data
|
||||
daySlots.value = await adminStore.fetchSlotsByDate(closeDate.value)
|
||||
} catch {
|
||||
closeSlots.value = []
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loadingClose.value = false
|
||||
slotsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function confirmClose(slot: SlotRow) {
|
||||
async function closeSlot(slot: TimeSlot) {
|
||||
uni.showModal({
|
||||
title: '关闭时段',
|
||||
content: `确认关闭 ${slot.startTime.slice(0, 5)}–${slot.endTime.slice(0, 5)} 的时段?`,
|
||||
title: '确认关闭',
|
||||
content: `关闭 ${slot.startTime}–${slot.endTime} 时段?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await put(`/admin/time-slot/${slot.id}/close`, {})
|
||||
await adminStore.closeSlot(slot.id)
|
||||
uni.showToast({ title: '已关闭', icon: 'success' })
|
||||
await fetchSlotsForClose()
|
||||
await loadSlotsForClose()
|
||||
} catch {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
@@ -230,44 +214,48 @@ function confirmClose(slot: SlotRow) {
|
||||
})
|
||||
}
|
||||
|
||||
// ── Generate slots ─────────────────────────────────
|
||||
const generateDaysStr = ref('14')
|
||||
const generating = ref(false)
|
||||
function slotBadgeClass(status: string) {
|
||||
if (status === 'OPEN') return 'badge--open'
|
||||
if (status === 'FULL') return 'badge--full'
|
||||
return 'badge--closed'
|
||||
}
|
||||
|
||||
async function handleGenerate() {
|
||||
if (generating.value) return
|
||||
const days = parseInt(generateDaysStr.value, 10)
|
||||
if (isNaN(days) || days < 1 || days > 90) {
|
||||
uni.showToast({ title: '请输入 1–90 天', icon: 'none' })
|
||||
// ── Batch generate ────────────────────────────────────────────────
|
||||
const genForm = ref({
|
||||
startDate: formatDate(new Date()),
|
||||
endDate: formatDate(new Date(Date.now() + 7 * 86400000)),
|
||||
})
|
||||
|
||||
async function submitGenerate() {
|
||||
if (submitting.value) return
|
||||
if (!genForm.value.startDate || !genForm.value.endDate) {
|
||||
uni.showToast({ title: '请选择日期范围', icon: 'none' })
|
||||
return
|
||||
}
|
||||
generating.value = true
|
||||
submitting.value = true
|
||||
try {
|
||||
await post('/admin/generate-slots', { days })
|
||||
uni.showToast({ title: '生成成功', icon: 'success' })
|
||||
const result = await adminStore.generateSlots(genForm.value.startDate, genForm.value.endDate)
|
||||
uni.showToast({ title: `生成 ${result.count} 个时段`, icon: 'success' })
|
||||
} catch (e: any) {
|
||||
uni.showToast({ title: e?.message ?? '生成失败', icon: 'none' })
|
||||
} finally {
|
||||
generating.value = false
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSlotsForClose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f3f0;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* ── Tabs ────────────────────────────────── */
|
||||
.tabs {
|
||||
display: flex;
|
||||
background: #ffffff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.tab {
|
||||
@@ -276,57 +264,34 @@ onMounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
&--active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 20%;
|
||||
right: 20%;
|
||||
height: 4rpx;
|
||||
background: #1a1a2e;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
|
||||
.tab--active & {
|
||||
color: #1a1a2e;
|
||||
font-weight: 700;
|
||||
}
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── Section ─────────────────────────────── */
|
||||
.section {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
.tab--active .tab-text {
|
||||
color: #1a1a2e;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.section-sub {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
display: block;
|
||||
margin-bottom: 24rpx;
|
||||
.tab--active {
|
||||
border-bottom: 4rpx solid #c9a87c;
|
||||
}
|
||||
|
||||
/* ── Panel ───────────────────────────────── */
|
||||
.panel {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
/* ── Form card ───────────────────────────── */
|
||||
.form-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
@@ -337,99 +302,34 @@ onMounted(() => {
|
||||
padding: 28rpx 28rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
&--last {
|
||||
border-bottom: none;
|
||||
}
|
||||
&--last { border-bottom: none; }
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 28rpx;
|
||||
color: #555;
|
||||
width: 160rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.form-label { font-size: 28rpx; color: #555; width: 160rpx; flex-shrink: 0; }
|
||||
|
||||
.picker-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.form-input { flex: 1; text-align: right; font-size: 28rpx; color: #222; background: transparent; }
|
||||
|
||||
.picker-text {
|
||||
font-size: 28rpx;
|
||||
color: #222;
|
||||
}
|
||||
.picker-display { display: flex; align-items: center; gap: 8rpx; }
|
||||
.picker-label { font-size: 28rpx; color: #555; }
|
||||
.picker-text { font-size: 28rpx; color: #222; }
|
||||
.picker-arrow { font-size: 26rpx; color: #bbb; }
|
||||
|
||||
.picker-arrow {
|
||||
font-size: 28rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
font-size: 28rpx;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
/* ── Buttons ─────────────────────────────── */
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
background: linear-gradient(90deg, #1a1a2e, #2d2d5e);
|
||||
|
||||
&--loading {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.primary-btn-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #c9a87c;
|
||||
}
|
||||
|
||||
/* ── Close tab ───────────────────────────── */
|
||||
.search-row {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.date-filter {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
/* ── Date picker row ─────────────────────── */
|
||||
.date-picker-row {
|
||||
background: #ffffff;
|
||||
border-radius: 32rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.date-filter-text {
|
||||
font-size: 26rpx;
|
||||
color: #1a1a2e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.date-filter-arrow {
|
||||
font-size: 26rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.skeleton-list {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
/* ── Skeleton ────────────────────────────── */
|
||||
.skeleton-list { }
|
||||
|
||||
.skeleton-item {
|
||||
height: 100rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 400% 100%;
|
||||
animation: shimmer 1.4s infinite;
|
||||
@@ -440,73 +340,88 @@ onMounted(() => {
|
||||
100% { background-position: -100% 0; }
|
||||
}
|
||||
|
||||
/* ── Empty ───────────────────────────────── */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 80rpx 0;
|
||||
gap: 20rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.empty-icon { font-size: 80rpx; }
|
||||
.empty-icon { font-size: 64rpx; }
|
||||
.empty-text { font-size: 28rpx; color: #bbb; }
|
||||
|
||||
.slot-list {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
/* ── Slot list ───────────────────────────── */
|
||||
.slot-list { }
|
||||
|
||||
.slot-card {
|
||||
.slot-row {
|
||||
background: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
margin-bottom: 12rpx;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
&--closed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.slot-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
.slot-info { flex: 1; display: flex; align-items: center; gap: 12rpx; }
|
||||
.slot-time { font-size: 28rpx; font-weight: 600; color: #1a1a2e; }
|
||||
|
||||
.slot-badge {
|
||||
border-radius: 16rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
}
|
||||
|
||||
.slot-time {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
.badge--open { background: rgba(39,174,96,0.1); }
|
||||
.badge--open .slot-badge-text { font-size: 20rpx; color: #27ae60; }
|
||||
.badge--full { background: rgba(230,126,34,0.1); }
|
||||
.badge--full .slot-badge-text { font-size: 20rpx; color: #e67e22; }
|
||||
.badge--closed { background: rgba(0,0,0,0.06); }
|
||||
.badge--closed .slot-badge-text { font-size: 20rpx; color: #999; }
|
||||
|
||||
.slot-cap {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
.slot-count { font-size: 24rpx; color: #888; }
|
||||
|
||||
.close-btn {
|
||||
background: #fde8e8;
|
||||
border-radius: 8rpx;
|
||||
padding: 12rpx 28rpx;
|
||||
background: rgba(192,57,43,0.1);
|
||||
border-radius: 20rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
}
|
||||
|
||||
.close-btn-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #c0392b;
|
||||
}
|
||||
.close-btn-text { font-size: 26rpx; color: #c0392b; font-weight: 600; }
|
||||
|
||||
.closed-tag {
|
||||
background: #f0f0f0;
|
||||
border-radius: 8rpx;
|
||||
padding: 12rpx 28rpx;
|
||||
background: rgba(0,0,0,0.06);
|
||||
border-radius: 20rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
}
|
||||
|
||||
.closed-tag-text {
|
||||
font-size: 26rpx;
|
||||
.closed-tag-text { font-size: 26rpx; color: #bbb; }
|
||||
|
||||
/* ── Generate hint ───────────────────────── */
|
||||
.gen-hint {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
display: block;
|
||||
padding: 0 8rpx 24rpx;
|
||||
}
|
||||
|
||||
/* ── Action button ───────────────────────── */
|
||||
.action-wrap { }
|
||||
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
background: linear-gradient(90deg, #1a1a2e, #2d2d5e);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&--loading { opacity: 0.6; }
|
||||
}
|
||||
|
||||
.action-btn-text { font-size: 30rpx; font-weight: 700; color: #c9a87c; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user