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:
@@ -1,86 +1,83 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- Top toolbar -->
|
||||
<!-- Toolbar -->
|
||||
<view class="toolbar">
|
||||
<text class="toolbar-hint">共 {{ templates.length }} 条模板</text>
|
||||
<view class="add-btn" @tap="openAdd">
|
||||
<text class="add-btn-text">+ 新增</text>
|
||||
<text class="add-btn-text">+ 新增时段</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Loading skeleton -->
|
||||
<view v-if="loading" class="skeleton-list">
|
||||
<view v-for="i in 4" :key="i" class="skeleton-item" />
|
||||
<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>
|
||||
<text class="empty-text">暂无模板,点击右上角新增</text>
|
||||
</view>
|
||||
|
||||
<!-- Template list grouped by weekday -->
|
||||
<template v-else>
|
||||
<view v-for="day in weekDays" :key="day.value" class="day-group">
|
||||
<view v-else>
|
||||
<view v-for="(group, day) in grouped" :key="day" class="day-group">
|
||||
<view class="day-header">
|
||||
<text class="day-label">{{ day.label }}</text>
|
||||
<text class="day-count">{{ dayTemplates(day.value).length }} 节</text>
|
||||
</view>
|
||||
<view v-if="!dayTemplates(day.value).length" class="day-empty">
|
||||
<text class="day-empty-text">该天无课</text>
|
||||
<text class="day-label">{{ WEEKDAY_LABELS[Number(day)] }}</text>
|
||||
<text class="day-count">{{ group.length }} 个时段</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="tpl in dayTemplates(day.value)"
|
||||
:key="tpl.id"
|
||||
class="tpl-card"
|
||||
:class="{ 'tpl-card--inactive': !tpl.isActive }"
|
||||
v-for="tpl in group"
|
||||
:key="tpl.id ?? tpl._key"
|
||||
class="tpl-row"
|
||||
:class="{ 'tpl-row--inactive': !tpl.isActive }"
|
||||
>
|
||||
<view class="tpl-main">
|
||||
<view class="tpl-time-block">
|
||||
<text class="tpl-time">{{ tpl.startTime.slice(0, 5) }}–{{ tpl.endTime.slice(0, 5) }}</text>
|
||||
<view class="tpl-status-dot" :class="tpl.isActive ? 'dot--active' : 'dot--inactive'" />
|
||||
</view>
|
||||
<view class="tpl-meta">
|
||||
<text class="tpl-capacity">容量 {{ tpl.capacity }} 人</text>
|
||||
<text class="tpl-active-label">{{ tpl.isActive ? '启用中' : '已停用' }}</text>
|
||||
</view>
|
||||
<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="action-btn edit-btn" @tap="openEdit(tpl)">
|
||||
<text class="action-btn-text">编辑</text>
|
||||
</view>
|
||||
<view
|
||||
class="action-btn toggle-btn"
|
||||
:class="tpl.isActive ? 'toggle-btn--off' : 'toggle-btn--on'"
|
||||
@tap="toggleActive(tpl)"
|
||||
class="tpl-toggle"
|
||||
:class="tpl.isActive ? 'toggle--on' : 'toggle--off'"
|
||||
@tap="toggleTemplate(tpl)"
|
||||
>
|
||||
<text class="action-btn-text">{{ tpl.isActive ? '停用' : '启用' }}</text>
|
||||
<text class="tpl-toggle-text">{{ tpl.isActive ? '启用' : '停用' }}</text>
|
||||
</view>
|
||||
<view class="action-btn delete-btn" @tap="confirmDelete(tpl)">
|
||||
<text class="action-btn-text">删除</text>
|
||||
<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>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<!-- Save all button -->
|
||||
<view v-if="dirty" class="save-bar">
|
||||
<view class="save-bar-btn" :class="{ 'save-bar-btn--loading': saving }" @tap="saveAll">
|
||||
<text class="save-bar-text">{{ saving ? '保存中...' : '保存全部更改' }}</text>
|
||||
<!-- 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>
|
||||
<text class="modal-title">{{ editTarget ? '编辑时段' : '新增时段' }}</text>
|
||||
|
||||
<view class="modal-field">
|
||||
<text class="modal-label">星期</text>
|
||||
<picker mode="selector" :range="weekDays" range-key="label" :value="form.dayOfWeek" @change="onDayChange">
|
||||
<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">{{ weekDays[form.dayOfWeek].label }}</text>
|
||||
<text class="picker-text">{{ dayOptions[form.dayIdx].label }}</text>
|
||||
<text class="picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
@@ -88,9 +85,13 @@
|
||||
|
||||
<view class="modal-field">
|
||||
<text class="modal-label">开始时间</text>
|
||||
<picker mode="time" :value="form.startTime" @change="(e: any) => form.startTime = e.detail.value">
|
||||
<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-text">{{ form.startTime || '请选择' }}</text>
|
||||
<text class="picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
@@ -98,16 +99,20 @@
|
||||
|
||||
<view class="modal-field">
|
||||
<text class="modal-label">结束时间</text>
|
||||
<picker mode="time" :value="form.endTime" @change="(e: any) => form.endTime = e.detail.value">
|
||||
<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-text">{{ form.endTime || '请选择' }}</text>
|
||||
<text class="picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="modal-field">
|
||||
<text class="modal-label">容量(人)</text>
|
||||
<view class="modal-field modal-field--last">
|
||||
<text class="modal-label">容量</text>
|
||||
<input
|
||||
class="modal-input"
|
||||
type="number"
|
||||
@@ -121,8 +126,8 @@
|
||||
<view class="modal-cancel" @tap="closeModal">
|
||||
<text class="modal-cancel-text">取消</text>
|
||||
</view>
|
||||
<view class="modal-confirm" :class="{ 'modal-confirm--loading': submitting }" @tap="submitForm">
|
||||
<text class="modal-confirm-text">{{ submitting ? '保存中...' : '确认' }}</text>
|
||||
<view class="modal-confirm" @tap="submitForm">
|
||||
<text class="modal-confirm-text">确认</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -132,43 +137,54 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { get, put } from '../../utils/request'
|
||||
import type { WeekTemplate, WeekTemplateInput } from '@mp-pilates/shared'
|
||||
import { useAdminStore } from '../../stores/admin'
|
||||
import { WEEKDAY_LABELS } from '@mp-pilates/shared'
|
||||
import type { WeekTemplate } from '@mp-pilates/shared'
|
||||
|
||||
const templates = ref<WeekTemplate[]>([])
|
||||
type LocalTemplate = Partial<WeekTemplate> & {
|
||||
_key?: string
|
||||
dayOfWeek: number
|
||||
startTime: string
|
||||
endTime: string
|
||||
capacity: number
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
const adminStore = useAdminStore()
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const dirty = ref(false)
|
||||
const isDirty = ref(false)
|
||||
const showModal = ref(false)
|
||||
const submitting = ref(false)
|
||||
const editTarget = ref<WeekTemplate | null>(null)
|
||||
const editTarget = ref<LocalTemplate | null>(null)
|
||||
|
||||
const weekDays = [
|
||||
{ label: '周一', value: 1 },
|
||||
{ label: '周二', value: 2 },
|
||||
{ label: '周三', value: 3 },
|
||||
{ label: '周四', value: 4 },
|
||||
{ label: '周五', value: 5 },
|
||||
{ label: '周六', value: 6 },
|
||||
{ label: '周日', value: 0 },
|
||||
]
|
||||
const templates = ref<LocalTemplate[]>([])
|
||||
|
||||
const dayOptions = [1, 2, 3, 4, 5, 6, 7].map((d) => ({ label: WEEKDAY_LABELS[d], value: d }))
|
||||
|
||||
const form = ref({
|
||||
dayOfWeek: 0,
|
||||
dayIdx: 0,
|
||||
startTime: '09:00',
|
||||
endTime: '10:00',
|
||||
capacityStr: '10',
|
||||
})
|
||||
|
||||
function dayTemplates(dayVal: number) {
|
||||
return templates.value.filter((t) => t.dayOfWeek === dayVal)
|
||||
}
|
||||
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)),
|
||||
)
|
||||
})
|
||||
|
||||
async function fetchTemplates() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await get<WeekTemplate[]>('/admin/week-template')
|
||||
templates.value = data
|
||||
templates.value = await adminStore.fetchWeekTemplates()
|
||||
isDirty.value = false
|
||||
} catch {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
@@ -178,16 +194,17 @@ async function fetchTemplates() {
|
||||
|
||||
function openAdd() {
|
||||
editTarget.value = null
|
||||
form.value = { dayOfWeek: 0, startTime: '09:00', endTime: '10:00', capacityStr: '10' }
|
||||
form.value = { dayIdx: 0, startTime: '09:00', endTime: '10:00', capacityStr: '10' }
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function openEdit(tpl: WeekTemplate) {
|
||||
function openEdit(tpl: LocalTemplate) {
|
||||
editTarget.value = tpl
|
||||
const dayIdx = dayOptions.findIndex((d) => d.value === tpl.dayOfWeek)
|
||||
form.value = {
|
||||
dayOfWeek: weekDays.findIndex((d) => d.value === tpl.dayOfWeek),
|
||||
startTime: tpl.startTime.slice(0, 5),
|
||||
endTime: tpl.endTime.slice(0, 5),
|
||||
dayIdx: dayIdx >= 0 ? dayIdx : 0,
|
||||
startTime: tpl.startTime,
|
||||
endTime: tpl.endTime,
|
||||
capacityStr: String(tpl.capacity),
|
||||
}
|
||||
showModal.value = true
|
||||
@@ -198,87 +215,77 @@ function closeModal() {
|
||||
editTarget.value = null
|
||||
}
|
||||
|
||||
function onDayChange(e: any) {
|
||||
form.value.dayOfWeek = Number(e.detail.value)
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
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' })
|
||||
uni.showToast({ title: '请填写有效容量', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const dayVal = weekDays[form.value.dayOfWeek].value
|
||||
const day = dayOptions[form.value.dayIdx].value
|
||||
|
||||
if (editTarget.value) {
|
||||
// Update in local list
|
||||
const idx = templates.value.findIndex((t) => t.id === editTarget.value!.id)
|
||||
if (idx !== -1) {
|
||||
templates.value[idx] = {
|
||||
...templates.value[idx],
|
||||
dayOfWeek: dayVal,
|
||||
startTime: form.value.startTime,
|
||||
endTime: form.value.endTime,
|
||||
capacity,
|
||||
}
|
||||
}
|
||||
const tpl = editTarget.value
|
||||
tpl.dayOfWeek = day
|
||||
tpl.startTime = form.value.startTime
|
||||
tpl.endTime = form.value.endTime
|
||||
tpl.capacity = capacity
|
||||
} else {
|
||||
// Add locally with a temp id
|
||||
templates.value.push({
|
||||
id: `tmp_${Date.now()}`,
|
||||
dayOfWeek: dayVal,
|
||||
_key: String(Date.now()),
|
||||
dayOfWeek: day,
|
||||
startTime: form.value.startTime,
|
||||
endTime: form.value.endTime,
|
||||
capacity,
|
||||
isActive: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as unknown as WeekTemplate)
|
||||
})
|
||||
}
|
||||
|
||||
dirty.value = true
|
||||
isDirty.value = true
|
||||
closeModal()
|
||||
}
|
||||
|
||||
function toggleActive(tpl: WeekTemplate) {
|
||||
const idx = templates.value.findIndex((t) => t.id === tpl.id)
|
||||
if (idx !== -1) {
|
||||
templates.value[idx] = { ...templates.value[idx], isActive: !templates.value[idx].isActive }
|
||||
dirty.value = true
|
||||
}
|
||||
function toggleTemplate(tpl: LocalTemplate) {
|
||||
tpl.isActive = !tpl.isActive
|
||||
isDirty.value = true
|
||||
}
|
||||
|
||||
function confirmDelete(tpl: WeekTemplate) {
|
||||
function deleteTemplate(tpl: LocalTemplate) {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: `删除 ${weekDays.find((d) => d.value === tpl.dayOfWeek)?.label} ${tpl.startTime.slice(0, 5)} 的模板?`,
|
||||
content: '删除该时段模板?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
templates.value = templates.value.filter((t) => t.id !== tpl.id)
|
||||
dirty.value = true
|
||||
const idx = templates.value.indexOf(tpl)
|
||||
if (idx >= 0) templates.value.splice(idx, 1)
|
||||
isDirty.value = true
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function saveAll() {
|
||||
async function handleSave() {
|
||||
if (saving.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
const payload: WeekTemplateInput[] = templates.value.map((t) => ({
|
||||
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 put('/admin/week-template', { templates: payload })
|
||||
dirty.value = false
|
||||
await adminStore.saveWeekTemplates(payload as any)
|
||||
isDirty.value = false
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
await fetchTemplates()
|
||||
} catch {
|
||||
uni.showToast({ title: '保存失败,请重试', icon: 'none' })
|
||||
} catch (e: any) {
|
||||
uni.showToast({ title: e?.message ?? '保存失败', icon: 'none' })
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -291,10 +298,10 @@ onMounted(fetchTemplates)
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f3f0;
|
||||
padding-bottom: 160rpx;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
/* ── Toolbar ────────────────────────────── */
|
||||
/* ── Toolbar ─────────────────────────────── */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -302,30 +309,21 @@ onMounted(fetchTemplates)
|
||||
padding: 24rpx 24rpx 16rpx;
|
||||
}
|
||||
|
||||
.toolbar-hint {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
.toolbar-hint { font-size: 24rpx; color: #999; }
|
||||
|
||||
.add-btn {
|
||||
background: #1a1a2e;
|
||||
border-radius: 32rpx;
|
||||
padding: 12rpx 32rpx;
|
||||
padding: 12rpx 28rpx;
|
||||
}
|
||||
|
||||
.add-btn-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #c9a87c;
|
||||
}
|
||||
.add-btn-text { font-size: 26rpx; font-weight: 600; color: #c9a87c; }
|
||||
|
||||
/* ── Skeleton ───────────────────────────── */
|
||||
.skeleton-list {
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
/* ── Skeleton ────────────────────────────── */
|
||||
.skeleton-list { padding: 0 24rpx; }
|
||||
|
||||
.skeleton-item {
|
||||
height: 120rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
@@ -338,189 +336,99 @@ onMounted(fetchTemplates)
|
||||
100% { background-position: -100% 0; }
|
||||
}
|
||||
|
||||
/* ── Empty ──────────────────────────────── */
|
||||
/* ── Empty ───────────────────────────────── */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
padding: 100rpx 0;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
}
|
||||
.empty-icon { font-size: 80rpx; }
|
||||
.empty-text { font-size: 28rpx; color: #bbb; }
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
/* ── Day group ──────────────────────────── */
|
||||
.day-group {
|
||||
margin: 0 24rpx 24rpx;
|
||||
}
|
||||
/* ── Day group ───────────────────────────── */
|
||||
.day-group { margin: 0 24rpx 24rpx; }
|
||||
|
||||
.day-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0 12rpx;
|
||||
padding: 16rpx 8rpx;
|
||||
}
|
||||
|
||||
.day-label {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
.day-label { font-size: 28rpx; font-weight: 700; color: #1a1a2e; }
|
||||
.day-count { font-size: 22rpx; color: #999; }
|
||||
|
||||
.day-count {
|
||||
font-size: 22rpx;
|
||||
color: #c9a87c;
|
||||
}
|
||||
|
||||
.day-empty {
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
.day-empty-text {
|
||||
font-size: 24rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* ── Template card ──────────────────────── */
|
||||
.tpl-card {
|
||||
/* ── Template row ────────────────────────── */
|
||||
.tpl-row {
|
||||
background: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 12rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
&--inactive {
|
||||
opacity: 0.55;
|
||||
}
|
||||
}
|
||||
|
||||
.tpl-main {
|
||||
padding: 20rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
margin-bottom: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
|
||||
|
||||
&--inactive { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.tpl-time-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
.tpl-time { display: flex; flex-direction: column; gap: 6rpx; }
|
||||
.tpl-time-text { font-size: 28rpx; font-weight: 600; color: #1a1a2e; }
|
||||
.tpl-capacity { font-size: 22rpx; color: #888; }
|
||||
|
||||
.tpl-actions { display: flex; gap: 12rpx; }
|
||||
|
||||
.tpl-toggle,
|
||||
.tpl-edit,
|
||||
.tpl-delete {
|
||||
border-radius: 20rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
}
|
||||
|
||||
.tpl-time {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
.toggle--on { background: rgba(39,174,96,0.12); }
|
||||
.toggle--on .tpl-toggle-text { font-size: 24rpx; color: #27ae60; }
|
||||
.toggle--off { background: rgba(230,126,34,0.12); }
|
||||
.toggle--off .tpl-toggle-text { font-size: 24rpx; color: #e67e22; }
|
||||
|
||||
.tpl-status-dot {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.tpl-edit { background: rgba(26,26,46,0.08); }
|
||||
.tpl-edit-text { font-size: 24rpx; color: #1a1a2e; }
|
||||
|
||||
.dot--active { background: #27ae60; }
|
||||
.dot--inactive { background: #ccc; }
|
||||
.tpl-delete { background: rgba(192,57,43,0.08); }
|
||||
.tpl-delete-text { font-size: 24rpx; color: #c0392b; }
|
||||
|
||||
.tpl-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.tpl-capacity {
|
||||
font-size: 24rpx;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.tpl-active-label {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tpl-actions {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 12rpx 0;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-btn-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
background: #f0f0f0;
|
||||
.action-btn-text { color: #1a1a2e; }
|
||||
}
|
||||
|
||||
.toggle-btn--off {
|
||||
background: #fff3cd;
|
||||
.action-btn-text { color: #a07000; }
|
||||
}
|
||||
|
||||
.toggle-btn--on {
|
||||
background: #d4edda;
|
||||
.action-btn-text { color: #155724; }
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background: #fde8e8;
|
||||
.action-btn-text { color: #c0392b; }
|
||||
}
|
||||
|
||||
/* ── Save bar ───────────────────────────── */
|
||||
/* ── Save bar ────────────────────────────── */
|
||||
.save-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 24rpx;
|
||||
padding: 20rpx 24rpx 48rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.save-bar-btn {
|
||||
.save-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
background: linear-gradient(90deg, #1a1a2e, #2d2d5e);
|
||||
border-radius: 44rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&--loading {
|
||||
opacity: 0.6;
|
||||
}
|
||||
&--loading { opacity: 0.6; }
|
||||
}
|
||||
|
||||
.save-bar-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #c9a87c;
|
||||
}
|
||||
.save-btn-text { font-size: 30rpx; font-weight: 700; color: #c9a87c; }
|
||||
|
||||
/* ── Modal ──────────────────────────────── */
|
||||
/* ── Modal ───────────────────────────────── */
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
z-index: 100;
|
||||
@@ -538,53 +446,31 @@ onMounted(fetchTemplates)
|
||||
font-weight: 700;
|
||||
color: #1a1a2e;
|
||||
display: block;
|
||||
margin-bottom: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.modal-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 0;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
&--last { border-bottom: none; }
|
||||
}
|
||||
|
||||
.modal-label {
|
||||
font-size: 28rpx;
|
||||
color: #555;
|
||||
width: 160rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.modal-label { font-size: 26rpx; color: #555; width: 140rpx; flex-shrink: 0; }
|
||||
|
||||
.picker-display {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.modal-input { flex: 1; text-align: right; font-size: 26rpx; color: #222; }
|
||||
|
||||
.picker-text {
|
||||
font-size: 28rpx;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
font-size: 28rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.modal-input {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
font-size: 28rpx;
|
||||
color: #222;
|
||||
}
|
||||
.picker-display { display: flex; align-items: center; gap: 8rpx; }
|
||||
.picker-text { font-size: 26rpx; color: #222; }
|
||||
.picker-arrow { font-size: 26rpx; color: #bbb; }
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-top: 40rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.modal-cancel {
|
||||
@@ -597,10 +483,7 @@ onMounted(fetchTemplates)
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-cancel-text {
|
||||
font-size: 28rpx;
|
||||
color: #555;
|
||||
}
|
||||
.modal-cancel-text { font-size: 28rpx; color: #555; }
|
||||
|
||||
.modal-confirm {
|
||||
flex: 2;
|
||||
@@ -610,15 +493,7 @@ onMounted(fetchTemplates)
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&--loading {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-confirm-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #c9a87c;
|
||||
}
|
||||
.modal-confirm-text { font-size: 28rpx; font-weight: 700; color: #c9a87c; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user