feat(app): add membership edit modal with tab UI
- Replace detail-only modal with tab-based (detail/edit) modal - Detail tab shows membership card info, stats, and action buttons - Edit tab provides form to modify card type, remaining times, start/expire dates - Add loadDetailMembership, switchTab, initEditForm, onSaveMembership, onClearMembership functions - Add computed isTimeBasedCard for edit form conditional rendering - Append new styles for modal-header, tab-bar, membership-card, edit-form, etc. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -87,40 +87,187 @@
|
|||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Detail modal -->
|
<!-- Detail/Edit modal -->
|
||||||
<view v-if="showDetail && detailMember" class="modal-mask" @tap.self="showDetail = false">
|
<view v-if="showDetail && detailMember" class="modal-mask" @tap.self="closeModal">
|
||||||
<view class="modal">
|
<view class="modal">
|
||||||
<view class="detail-header">
|
<!-- modal-header -->
|
||||||
<view class="detail-avatar">
|
<view class="modal-header">
|
||||||
<image v-if="detailMember.avatarUrl" class="avatar-img" :src="detailMember.avatarUrl" mode="aspectFill" />
|
<view class="modal-close-btn" @tap="closeModal">
|
||||||
<view v-else class="avatar-placeholder avatar-placeholder--lg">
|
<text class="modal-close-icon">×</text>
|
||||||
<text class="avatar-text avatar-text--lg">{{ (detailMember.nickname || '?').slice(0, 1) }}</text>
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Tab bar -->
|
||||||
|
<view class="tab-bar">
|
||||||
|
<view
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ 'tab-item--active': activeTab === 'detail' }"
|
||||||
|
@tap="switchTab('detail')"
|
||||||
|
>
|
||||||
|
<text class="tab-text" :class="{ 'tab-text--active': activeTab === 'detail' }">详情</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ 'tab-item--active': activeTab === 'edit' }"
|
||||||
|
@tap="switchTab('edit')"
|
||||||
|
>
|
||||||
|
<text class="tab-text" :class="{ 'tab-text--active': activeTab === 'edit' }">编辑</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Loading overlay for edit tab -->
|
||||||
|
<view v-if="editLoading" class="edit-loading">
|
||||||
|
<text class="edit-loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Detail tab content -->
|
||||||
|
<view v-if="activeTab === 'detail'" class="tab-content">
|
||||||
|
<!-- User info -->
|
||||||
|
<view class="detail-header">
|
||||||
|
<view class="detail-avatar">
|
||||||
|
<image v-if="detailMember.avatarUrl" class="avatar-img" :src="detailMember.avatarUrl" mode="aspectFill" />
|
||||||
|
<view v-else class="avatar-placeholder avatar-placeholder--lg">
|
||||||
|
<text class="avatar-text avatar-text--lg">{{ (detailMember.nickname || '?').slice(0, 1) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="detail-name">{{ detailMember.nickname || '未知用户' }}</text>
|
||||||
|
<text class="detail-openid" @tap="copyOpenid(detailMember.openid)">
|
||||||
|
{{ detailMember.openid }}
|
||||||
|
</text>
|
||||||
|
<text class="detail-phone">{{ detailMember.phone || '未绑定手机' }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<view class="detail-stats">
|
||||||
|
<view class="detail-stat">
|
||||||
|
<text class="detail-stat-value">{{ detailMember.totalBookings }}</text>
|
||||||
|
<text class="detail-stat-label">总预约</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-stat">
|
||||||
|
<text class="detail-stat-value">{{ detailMember.completedBookings }}</text>
|
||||||
|
<text class="detail-stat-label">已完成</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-stat">
|
||||||
|
<text class="detail-stat-value">{{ detailMember.cancelledBookings }}</text>
|
||||||
|
<text class="detail-stat-label">已取消</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="detail-name">{{ detailMember.nickname || '未知用户' }}</text>
|
|
||||||
<text class="detail-openid" @tap="copyOpenid(detailMember.openid)">
|
|
||||||
{{ detailMember.openid }}
|
|
||||||
</text>
|
|
||||||
<text class="detail-phone">{{ detailMember.phone || '未绑定手机' }}</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="detail-stats">
|
<!-- Membership card -->
|
||||||
<view class="detail-stat">
|
<view v-if="detailMembership" class="membership-card">
|
||||||
<text class="detail-stat-value">{{ detailMember.totalBookings }}</text>
|
<view class="membership-card-header">
|
||||||
<text class="detail-stat-label">总预约</text>
|
<text class="membership-card-name">{{ detailMembership.cardTypeName }}</text>
|
||||||
|
<text class="membership-card-badge" :class="'badge--' + detailMembership.status.toLowerCase()">
|
||||||
|
{{ statusLabel(detailMembership.status) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="membership-card-info">
|
||||||
|
<view class="membership-info-item">
|
||||||
|
<text class="membership-info-label">剩余次数</text>
|
||||||
|
<text class="membership-info-value">{{ detailMembership.remainingTimes ?? '不限' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="membership-info-item">
|
||||||
|
<text class="membership-info-label">开始日期</text>
|
||||||
|
<text class="membership-info-value">{{ formatDate(detailMembership.startDate) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="membership-info-item">
|
||||||
|
<text class="membership-info-label">到期日期</text>
|
||||||
|
<text class="membership-info-value">{{ formatDate(detailMembership.expireDate) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="detail-stat">
|
|
||||||
<text class="detail-stat-value">{{ detailMember.completedBookings }}</text>
|
<!-- No membership -->
|
||||||
<text class="detail-stat-label">已完成</text>
|
<view v-else class="no-membership">
|
||||||
|
<text class="no-membership-text">暂无会员卡</text>
|
||||||
|
<view class="no-membership-btn" @tap="goEdit">
|
||||||
|
<text class="no-membership-btn-text">去开卡</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="detail-stat">
|
|
||||||
<text class="detail-stat-value">{{ detailMember.cancelledBookings }}</text>
|
<!-- Danger zone -->
|
||||||
<text class="detail-stat-label">已取消</text>
|
<view v-if="detailMembership" class="danger-zone">
|
||||||
|
<view class="danger-btn" @tap="onClearMembership">
|
||||||
|
<text class="danger-btn-text">解除会员卡</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="modal-close" @tap="showDetail = false">
|
<!-- Edit tab content -->
|
||||||
<text class="modal-close-text">关闭</text>
|
<view v-if="activeTab === 'edit'" class="tab-content">
|
||||||
|
<view class="edit-form">
|
||||||
|
<!-- Card type -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">卡类型</text>
|
||||||
|
<picker
|
||||||
|
class="form-picker"
|
||||||
|
mode="selector"
|
||||||
|
:value="editForm.cardTypeIndex"
|
||||||
|
:range="editCardTypes"
|
||||||
|
range-key="name"
|
||||||
|
@change="onCardTypeChangeForEdit"
|
||||||
|
>
|
||||||
|
<view class="form-picker-inner">
|
||||||
|
<text class="form-picker-text">{{ editCardTypes[editForm.cardTypeIndex]?.name || '请选择' }}</text>
|
||||||
|
<text class="form-picker-arrow">▾</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Remaining times (for TIMES/TRIAL) -->
|
||||||
|
<view v-if="isTimeBasedCard" class="form-item">
|
||||||
|
<text class="form-label">剩余次数</text>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
type="number"
|
||||||
|
v-model="editForm.remainingTimes"
|
||||||
|
placeholder="请输入剩余次数"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Start date -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">开始日期</text>
|
||||||
|
<picker
|
||||||
|
class="form-picker"
|
||||||
|
mode="date"
|
||||||
|
:value="editForm.startDate"
|
||||||
|
@change="onStartDateChange"
|
||||||
|
>
|
||||||
|
<view class="form-picker-inner">
|
||||||
|
<text class="form-picker-text">{{ editForm.startDate || '请选择' }}</text>
|
||||||
|
<text class="form-picker-arrow">▾</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Expire date -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">到期日期</text>
|
||||||
|
<picker
|
||||||
|
class="form-picker"
|
||||||
|
mode="date"
|
||||||
|
:value="editForm.expireDate"
|
||||||
|
@change="onExpireDateChange"
|
||||||
|
>
|
||||||
|
<view class="form-picker-inner">
|
||||||
|
<text class="form-picker-text">{{ editForm.expireDate || '请选择' }}</text>
|
||||||
|
<text class="form-picker-arrow">▾</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Submit button -->
|
||||||
|
<view class="edit-submit">
|
||||||
|
<view
|
||||||
|
class="submit-btn"
|
||||||
|
:class="{ 'submit-btn--disabled': editSubmitting }"
|
||||||
|
@tap="onSaveMembership"
|
||||||
|
>
|
||||||
|
<text class="submit-btn-text">{{ editSubmitting ? '保存中...' : '保存' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -128,7 +275,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { onReachBottom } from '@dcloudio/uni-app'
|
import { onReachBottom } from '@dcloudio/uni-app'
|
||||||
import CustomNavBar from '../../components/CustomNavBar.vue'
|
import CustomNavBar from '../../components/CustomNavBar.vue'
|
||||||
import { getSystemLayout } from '../../utils/system'
|
import { getSystemLayout } from '../../utils/system'
|
||||||
@@ -151,6 +298,26 @@ const hasMore = ref(false)
|
|||||||
const showDetail = ref(false)
|
const showDetail = ref(false)
|
||||||
const detailMember = ref<MemberSummary | null>(null)
|
const detailMember = ref<MemberSummary | null>(null)
|
||||||
|
|
||||||
|
const activeTab = ref<'detail' | 'edit'>('detail')
|
||||||
|
const detailMembership = ref<any>(null)
|
||||||
|
const editLoading = ref(false)
|
||||||
|
const editSubmitting = ref(false)
|
||||||
|
const editCardTypes = ref<any[]>([])
|
||||||
|
|
||||||
|
const editForm = ref({
|
||||||
|
cardTypeIndex: 0,
|
||||||
|
cardTypeId: '',
|
||||||
|
remainingTimes: null as number | null,
|
||||||
|
startDate: '',
|
||||||
|
expireDate: '',
|
||||||
|
manuallyEditedExpire: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isTimeBasedCard = computed(() => {
|
||||||
|
const card = editCardTypes.value[editForm.value.cardTypeIndex]
|
||||||
|
return card && (card.type === 'TIMES' || card.type === 'TRIAL')
|
||||||
|
})
|
||||||
|
|
||||||
const LIMIT = 20
|
const LIMIT = 20
|
||||||
|
|
||||||
const cardTypeOptions = [
|
const cardTypeOptions = [
|
||||||
@@ -225,6 +392,9 @@ onReachBottom(() => {
|
|||||||
function openDetail(m: MemberSummary) {
|
function openDetail(m: MemberSummary) {
|
||||||
detailMember.value = m
|
detailMember.value = m
|
||||||
showDetail.value = true
|
showDetail.value = true
|
||||||
|
detailMembership.value = null
|
||||||
|
activeTab.value = 'detail'
|
||||||
|
loadDetailMembership(m.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyOpenid(openid: string) {
|
function copyOpenid(openid: string) {
|
||||||
@@ -234,6 +404,171 @@ function copyOpenid(openid: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr: string): string {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
return dateStr.slice(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
ACTIVE: '有效',
|
||||||
|
EXPIRED: '已过期',
|
||||||
|
USED_UP: '已用完',
|
||||||
|
}
|
||||||
|
return map[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDetailMembership(userId: string) {
|
||||||
|
detailMembership.value = null
|
||||||
|
editLoading.value = true
|
||||||
|
try {
|
||||||
|
const result = await adminStore.getUserMembership(userId)
|
||||||
|
detailMembership.value = result.membership
|
||||||
|
} catch {
|
||||||
|
} finally {
|
||||||
|
editLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(tab: 'detail' | 'edit') {
|
||||||
|
activeTab.value = tab
|
||||||
|
if (tab === 'edit') {
|
||||||
|
if (!editCardTypes.value.length) {
|
||||||
|
adminStore.fetchCardTypes().then((types) => {
|
||||||
|
editCardTypes.value = types
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!detailMembership.value) {
|
||||||
|
loadDetailMembership(detailMember.value!.userId).then(() => {
|
||||||
|
initEditForm()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
initEditForm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initEditForm() {
|
||||||
|
const m = detailMembership.value
|
||||||
|
const types = editCardTypes.value
|
||||||
|
if (m) {
|
||||||
|
const idx = types.findIndex((t: any) => t.id === m.cardTypeId)
|
||||||
|
editForm.value = {
|
||||||
|
cardTypeIndex: idx >= 0 ? idx : 0,
|
||||||
|
cardTypeId: m.cardTypeId,
|
||||||
|
remainingTimes: m.remainingTimes,
|
||||||
|
startDate: m.startDate.slice(0, 10),
|
||||||
|
expireDate: m.expireDate.slice(0, 10),
|
||||||
|
manuallyEditedExpire: false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
editForm.value = {
|
||||||
|
cardTypeIndex: 0,
|
||||||
|
cardTypeId: types[0]?.id || '',
|
||||||
|
remainingTimes: types[0]?.totalTimes ?? null,
|
||||||
|
startDate: formatDate2(new Date()),
|
||||||
|
expireDate: calculateExpireDate(formatDate2(new Date()), types[0]?.durationDays ?? 30),
|
||||||
|
manuallyEditedExpire: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate2(date: Date): string {
|
||||||
|
const y = date.getFullYear()
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const d = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${d}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateExpireDate(startDate: string, durationDays: number): string {
|
||||||
|
const d = new Date(startDate)
|
||||||
|
d.setDate(d.getDate() + durationDays)
|
||||||
|
return formatDate2(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCardTypeChangeForEdit(e: { detail: { value: number } }) {
|
||||||
|
const idx = e.detail.value
|
||||||
|
const cardType = editCardTypes.value[idx]
|
||||||
|
editForm.value.cardTypeIndex = idx
|
||||||
|
editForm.value.cardTypeId = cardType.id
|
||||||
|
if (cardType.totalTimes != null) {
|
||||||
|
editForm.value.remainingTimes = cardType.totalTimes
|
||||||
|
}
|
||||||
|
if (!editForm.value.manuallyEditedExpire) {
|
||||||
|
editForm.value.startDate = formatDate2(new Date())
|
||||||
|
editForm.value.expireDate = calculateExpireDate(formatDate2(new Date()), cardType.durationDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStartDateChange(e: { detail: { value: string } }) {
|
||||||
|
editForm.value.startDate = e.detail.value
|
||||||
|
if (!editForm.value.manuallyEditedExpire) {
|
||||||
|
const cardType = editCardTypes.value[editForm.value.cardTypeIndex]
|
||||||
|
if (cardType) {
|
||||||
|
editForm.value.expireDate = calculateExpireDate(e.detail.value, cardType.durationDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExpireDateChange(e: { detail: { value: string } }) {
|
||||||
|
editForm.value.expireDate = e.detail.value
|
||||||
|
editForm.value.manuallyEditedExpire = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function goEdit() {
|
||||||
|
switchTab('edit')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSaveMembership() {
|
||||||
|
if (editSubmitting.value) return
|
||||||
|
const userId = detailMember.value?.userId
|
||||||
|
if (!userId) return
|
||||||
|
editSubmitting.value = true
|
||||||
|
try {
|
||||||
|
await adminStore.updateUserMembership(userId, {
|
||||||
|
cardTypeId: editForm.value.cardTypeId,
|
||||||
|
remainingTimes: editForm.value.remainingTimes,
|
||||||
|
startDate: editForm.value.startDate,
|
||||||
|
expireDate: editForm.value.expireDate,
|
||||||
|
})
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||||
|
activeTab.value = 'detail'
|
||||||
|
await loadDetailMembership(userId)
|
||||||
|
loadMembers(false)
|
||||||
|
} catch {
|
||||||
|
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
editSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onClearMembership() {
|
||||||
|
const userId = detailMember.value?.userId
|
||||||
|
if (!userId) return
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认解除',
|
||||||
|
content: '确定要解除该用户的会员卡吗?',
|
||||||
|
confirmColor: '#e64329',
|
||||||
|
success: async (res) => {
|
||||||
|
if (!res.confirm) return
|
||||||
|
try {
|
||||||
|
await adminStore.deleteUserMembership(userId)
|
||||||
|
uni.showToast({ title: '已解除', icon: 'success' })
|
||||||
|
showDetail.value = false
|
||||||
|
loadMembers(false)
|
||||||
|
} catch {
|
||||||
|
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
showDetail.value = false
|
||||||
|
activeTab.value = 'detail'
|
||||||
|
detailMembership.value = null
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => loadMembers(true))
|
onMounted(() => loadMembers(true))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -546,4 +881,270 @@ onMounted(() => loadMembers(true))
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-close-text { font-size: 28rpx; color: $text-secondary; }
|
.modal-close-text { font-size: 28rpx; color: $text-secondary; }
|
||||||
|
|
||||||
|
/* ── Modal header ─────────────────────────── */
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: $bg-page;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-icon {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tab bar ──────────────────────────────── */
|
||||||
|
.tab-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
height: 80rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: $bg-page;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item--active {
|
||||||
|
background: $brand-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-text--active {
|
||||||
|
color: $accent-color;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tab content ──────────────────────────── */
|
||||||
|
.tab-content {
|
||||||
|
min-height: 400rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 400rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-loading-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Membership card ──────────────────────── */
|
||||||
|
.membership-card {
|
||||||
|
background: $bg-page;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
padding: 28rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-card-name {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $brand-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-card-badge {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--active {
|
||||||
|
color: #52c41a;
|
||||||
|
background: rgba(#52c41a, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--expired {
|
||||||
|
color: #999;
|
||||||
|
background: rgba(#999, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--used_up {
|
||||||
|
color: #e64329;
|
||||||
|
background: rgba(#e64329, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-card-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-info-label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $text-hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-info-value {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $text-primary;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── No membership ────────────────────────── */
|
||||||
|
.no-membership {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
padding: 48rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-membership-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-membership-btn {
|
||||||
|
background: $brand-color;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
padding: 20rpx 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-membership-btn-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $accent-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Danger zone ─────────────────────────── */
|
||||||
|
.danger-zone {
|
||||||
|
margin-top: 32rpx;
|
||||||
|
padding-top: 32rpx;
|
||||||
|
border-top: 1rpx solid $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(#e64329, 0.08);
|
||||||
|
border-radius: $radius-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #e64329;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Edit form ───────────────────────────── */
|
||||||
|
.edit-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
height: 80rpx;
|
||||||
|
background: $bg-page;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-picker {
|
||||||
|
height: 80rpx;
|
||||||
|
background: $bg-page;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-picker-inner {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-picker-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-picker-arrow {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: $text-hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Edit submit ──────────────────────────── */
|
||||||
|
.edit-submit {
|
||||||
|
margin-top: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
background: $brand-color;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn--disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $accent-color;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user