- 将 admin 相关的 store/utils 移入对应子目录(pages/admin/stores、pages/admin/utils) - 更新 manifest.json、pages.json 路由与配置 - 个人中心 ProfileMenu 移除顶部「我的会员卡 / 我的预约」快捷卡片,与 UserCard 会员卡入口合并 - 「我的预约」下移至菜单列表,紧邻「个人信息」 - 清理 profile/index.vue 中不再使用的 bookingStore / 计算属性 / 透传
436 lines
12 KiB
Vue
436 lines
12 KiB
Vue
<template>
|
|
<view class="page" :style="{ paddingTop: navBarHeight }">
|
|
<CustomNavBar title="编辑资料" show-back />
|
|
|
|
<view v-if="pageLoading" class="loading-wrap">
|
|
<text class="loading-text">加载中...</text>
|
|
</view>
|
|
|
|
<view v-else class="form">
|
|
<view class="block">
|
|
<text class="block-title">基本信息</text>
|
|
<view class="field">
|
|
<text class="field-label">昵称</text>
|
|
<input class="field-input" v-model="profileForm.nickname" maxlength="32" placeholder="会员昵称" />
|
|
</view>
|
|
<view class="field">
|
|
<text class="field-label">手机号</text>
|
|
<input class="field-input" v-model="profileForm.phone" maxlength="20" type="number" placeholder="未绑定可手动填写" />
|
|
</view>
|
|
</view>
|
|
|
|
<view class="block">
|
|
<text class="block-title">{{ editingMembership ? '会员卡' : '开通会员卡' }}</text>
|
|
|
|
<view v-if="existingMemberships.length > 1" class="field">
|
|
<text class="field-label">选择卡片</text>
|
|
<picker
|
|
class="field-picker"
|
|
mode="selector"
|
|
:value="membershipIndex"
|
|
:range="membershipPickerLabels"
|
|
@change="onMembershipPick"
|
|
>
|
|
<view class="picker-inner">
|
|
<text class="picker-text">{{ membershipPickerLabels[membershipIndex] }}</text>
|
|
<text class="picker-arrow">▾</text>
|
|
</view>
|
|
</picker>
|
|
</view>
|
|
|
|
<view class="field">
|
|
<text class="field-label">卡类型</text>
|
|
<picker
|
|
class="field-picker"
|
|
mode="selector"
|
|
:value="editForm.cardTypeIndex"
|
|
:range="cardTypes"
|
|
range-key="name"
|
|
@change="onCardTypeChange"
|
|
>
|
|
<view class="picker-inner">
|
|
<text class="picker-text">{{ cardTypes[editForm.cardTypeIndex]?.name || '请选择' }}</text>
|
|
<text class="picker-arrow">▾</text>
|
|
</view>
|
|
</picker>
|
|
</view>
|
|
|
|
<view v-if="selectedCardType" class="field">
|
|
<text class="field-label">
|
|
{{ canLeaveTimesEmpty ? '剩余次数(留空不限次)' : '剩余次数' }}
|
|
</text>
|
|
<input
|
|
class="field-input"
|
|
type="number"
|
|
v-model="editForm.remainingTimes"
|
|
:placeholder="canLeaveTimesEmpty ? '留空表示不限次' : '请输入剩余次数'"
|
|
/>
|
|
</view>
|
|
|
|
<view class="field">
|
|
<text class="field-label">开始日期</text>
|
|
<picker class="field-picker" mode="date" :value="editForm.startDate" @change="onStartDateChange">
|
|
<view class="picker-inner">
|
|
<text class="picker-text">{{ editForm.startDate || '请选择' }}</text>
|
|
<text class="picker-arrow">▾</text>
|
|
</view>
|
|
</picker>
|
|
</view>
|
|
|
|
<view class="field">
|
|
<text class="field-label">到期日期</text>
|
|
<picker class="field-picker" mode="date" :value="editForm.expireDate" @change="onExpireDateChange">
|
|
<view class="picker-inner">
|
|
<text class="picker-text">{{ editForm.expireDate || '请选择' }}</text>
|
|
<text class="picker-arrow">▾</text>
|
|
</view>
|
|
</picker>
|
|
</view>
|
|
|
|
<view v-if="editingMembership" class="danger-link" @tap="onClearMembership">
|
|
<text class="danger-link-text">解除该会员卡</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view
|
|
class="save-btn"
|
|
:class="{ 'save-btn--disabled': submitting }"
|
|
@tap="onSave"
|
|
>
|
|
<text class="save-btn-text">{{ submitting ? '保存中...' : '保存' }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|
import type { AdminMemberDetail, CardType, MembershipWithCardType } from '@mp-pilates/shared'
|
|
import CustomNavBar from '../../components/CustomNavBar.vue'
|
|
import { getSystemLayout } from '../../utils/system'
|
|
import { formatDateLocal } from '../../utils/format'
|
|
import { getErrorMessage } from '../../utils/auth'
|
|
import { useAdminStore } from './stores/admin'
|
|
|
|
const adminStore = useAdminStore()
|
|
const navBarHeight = ref('64px')
|
|
const userId = ref('')
|
|
const pageLoading = ref(true)
|
|
const submitting = ref(false)
|
|
const detail = ref<AdminMemberDetail | null>(null)
|
|
const cardTypes = ref<CardType[]>([])
|
|
const membershipIndex = ref(0)
|
|
|
|
const profileForm = ref({
|
|
nickname: '',
|
|
phone: '',
|
|
})
|
|
|
|
const editForm = ref({
|
|
membershipId: '' as string | '',
|
|
cardTypeIndex: 0,
|
|
cardTypeId: '',
|
|
remainingTimes: null as number | string | null,
|
|
startDate: '',
|
|
expireDate: '',
|
|
manuallyEditedExpire: false,
|
|
})
|
|
|
|
const existingMemberships = computed(() => detail.value?.memberships ?? [])
|
|
const editingMembership = computed(() => existingMemberships.value[membershipIndex.value] ?? null)
|
|
|
|
const membershipPickerLabels = computed(() =>
|
|
existingMemberships.value.map((item) => `${item.cardType.name} · ${item.status}`),
|
|
)
|
|
|
|
const selectedCardType = computed(() => cardTypes.value[editForm.value.cardTypeIndex] ?? null)
|
|
const canLeaveTimesEmpty = computed(() => {
|
|
const cardType = selectedCardType.value
|
|
const membership = editingMembership.value
|
|
if (!cardType || cardType.type !== 'DURATION') return false
|
|
|
|
return cardType.totalTimes === null || (
|
|
membership?.cardTypeId === cardType.id && membership.remainingTimes === null
|
|
)
|
|
})
|
|
|
|
function calculateExpireDate(startDate: string, durationDays: number): string {
|
|
const d = new Date(startDate)
|
|
d.setDate(d.getDate() + durationDays)
|
|
return formatDateLocal(d)
|
|
}
|
|
|
|
function applyMembership(membership: MembershipWithCardType | null) {
|
|
const types = cardTypes.value
|
|
if (membership) {
|
|
const idx = types.findIndex((item) => item.id === membership.cardTypeId)
|
|
editForm.value = {
|
|
membershipId: membership.id,
|
|
cardTypeIndex: idx >= 0 ? idx : 0,
|
|
cardTypeId: membership.cardTypeId,
|
|
remainingTimes: membership.remainingTimes,
|
|
startDate: membership.startDate.slice(0, 10),
|
|
expireDate: membership.expireDate.slice(0, 10),
|
|
manuallyEditedExpire: false,
|
|
}
|
|
return
|
|
}
|
|
|
|
editForm.value = {
|
|
membershipId: '',
|
|
cardTypeIndex: 0,
|
|
cardTypeId: types[0]?.id || '',
|
|
remainingTimes: types[0]?.totalTimes ?? null,
|
|
startDate: formatDateLocal(new Date()),
|
|
expireDate: calculateExpireDate(formatDateLocal(new Date()), types[0]?.durationDays ?? 30),
|
|
manuallyEditedExpire: false,
|
|
}
|
|
}
|
|
|
|
function onMembershipPick(e: { detail: { value: number } }) {
|
|
membershipIndex.value = Number(e.detail.value)
|
|
applyMembership(existingMemberships.value[membershipIndex.value] ?? null)
|
|
}
|
|
|
|
function onCardTypeChange(e: { detail: { value: number } }) {
|
|
const idx = Number(e.detail.value)
|
|
const cardType = cardTypes.value[idx]
|
|
if (!cardType) return
|
|
|
|
editForm.value.cardTypeIndex = idx
|
|
editForm.value.cardTypeId = cardType.id
|
|
editForm.value.remainingTimes = cardType.totalTimes
|
|
if (!editForm.value.manuallyEditedExpire) {
|
|
editForm.value.startDate = formatDateLocal(new Date())
|
|
editForm.value.expireDate = calculateExpireDate(formatDateLocal(new Date()), cardType.durationDays)
|
|
}
|
|
}
|
|
|
|
function onStartDateChange(e: { detail: { value: string } }) {
|
|
editForm.value.startDate = e.detail.value
|
|
if (!editForm.value.manuallyEditedExpire) {
|
|
const cardType = cardTypes.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 parseRemainingTimes(): number | null | undefined {
|
|
const rawValue = editForm.value.remainingTimes
|
|
if (rawValue === null || (typeof rawValue === 'string' && rawValue.trim() === '')) {
|
|
return null
|
|
}
|
|
|
|
const remainingTimes = Number(rawValue)
|
|
if (!Number.isInteger(remainingTimes) || remainingTimes < 0) return undefined
|
|
return remainingTimes
|
|
}
|
|
|
|
async function loadPage() {
|
|
pageLoading.value = true
|
|
try {
|
|
const [member, types] = await Promise.all([
|
|
adminStore.fetchMemberDetail(userId.value),
|
|
cardTypes.value.length ? Promise.resolve(cardTypes.value) : adminStore.fetchCardTypes(),
|
|
])
|
|
detail.value = member
|
|
cardTypes.value = [...types]
|
|
profileForm.value = {
|
|
nickname: member.user.nickname || '',
|
|
phone: member.user.phone || '',
|
|
}
|
|
membershipIndex.value = 0
|
|
applyMembership(member.memberships[0] ?? null)
|
|
} catch (err: unknown) {
|
|
uni.showToast({ title: getErrorMessage(err, '加载失败'), icon: 'none' })
|
|
} finally {
|
|
pageLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function onSave() {
|
|
if (submitting.value || !userId.value) return
|
|
if (!editForm.value.cardTypeId) {
|
|
uni.showToast({ title: '请选择卡类型', icon: 'none' })
|
|
return
|
|
}
|
|
|
|
const remainingTimes = parseRemainingTimes()
|
|
if (remainingTimes === undefined) {
|
|
uni.showToast({ title: '剩余次数需为非负整数', icon: 'none' })
|
|
return
|
|
}
|
|
if (remainingTimes === null && !canLeaveTimesEmpty.value) {
|
|
uni.showToast({ title: '请输入剩余次数', icon: 'none' })
|
|
return
|
|
}
|
|
|
|
submitting.value = true
|
|
try {
|
|
await adminStore.updateMemberProfile(userId.value, {
|
|
nickname: profileForm.value.nickname.trim(),
|
|
phone: profileForm.value.phone.trim(),
|
|
})
|
|
await adminStore.updateUserMembership(userId.value, {
|
|
...(editForm.value.membershipId ? { membershipId: editForm.value.membershipId } : {}),
|
|
cardTypeId: editForm.value.cardTypeId,
|
|
remainingTimes,
|
|
startDate: editForm.value.startDate,
|
|
expireDate: editForm.value.expireDate,
|
|
})
|
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
|
setTimeout(() => uni.navigateBack(), 400)
|
|
} catch (err: unknown) {
|
|
uni.showToast({ title: getErrorMessage(err, '保存失败'), icon: 'none' })
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
function onClearMembership() {
|
|
if (!userId.value) return
|
|
uni.showModal({
|
|
title: '确认解除',
|
|
content: '确定要解除当前这张会员卡吗?其他卡不受影响。',
|
|
confirmColor: '#C47A7A',
|
|
success: async (res) => {
|
|
if (!res.confirm) return
|
|
const membershipId = editForm.value.membershipId
|
|
if (!membershipId) {
|
|
uni.showToast({ title: '没有可解除的会员卡', icon: 'none' })
|
|
return
|
|
}
|
|
try {
|
|
await adminStore.deleteUserMembership(userId.value, membershipId)
|
|
uni.showToast({ title: '已解除', icon: 'success' })
|
|
setTimeout(() => uni.navigateBack(), 400)
|
|
} catch (err: unknown) {
|
|
uni.showToast({ title: getErrorMessage(err, '操作失败'), icon: 'none' })
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
onLoad((query) => {
|
|
userId.value = String(query?.userId || '')
|
|
})
|
|
|
|
onMounted(() => {
|
|
navBarHeight.value = `${getSystemLayout().navBarHeight}px`
|
|
loadPage()
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.page {
|
|
min-height: 100vh;
|
|
background: $bg-page;
|
|
padding-bottom: 80rpx;
|
|
}
|
|
|
|
.loading-wrap {
|
|
padding: 80rpx 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.loading-text {
|
|
font-size: 26rpx;
|
|
color: $text-hint;
|
|
}
|
|
|
|
.form {
|
|
padding: 24rpx;
|
|
}
|
|
|
|
.block {
|
|
background: $bg-card;
|
|
border-radius: 20rpx;
|
|
padding: 8rpx 24rpx 16rpx;
|
|
margin-bottom: 20rpx;
|
|
border: 1rpx solid rgba(180, 160, 130, 0.12);
|
|
}
|
|
|
|
.block-title {
|
|
display: block;
|
|
padding: 20rpx 0 8rpx;
|
|
font-size: 22rpx;
|
|
letter-spacing: 3rpx;
|
|
color: $text-hint;
|
|
}
|
|
|
|
.field {
|
|
padding: 20rpx 0;
|
|
border-bottom: 1rpx solid rgba(180, 160, 130, 0.1);
|
|
|
|
&:last-of-type { border-bottom: none; }
|
|
}
|
|
|
|
.field-label {
|
|
display: block;
|
|
font-size: 24rpx;
|
|
color: $text-hint;
|
|
margin-bottom: 12rpx;
|
|
}
|
|
|
|
.field-input {
|
|
height: 64rpx;
|
|
font-size: 30rpx;
|
|
color: $text-primary;
|
|
}
|
|
|
|
.field-picker {
|
|
display: block;
|
|
}
|
|
|
|
.picker-inner {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
height: 64rpx;
|
|
}
|
|
|
|
.picker-text {
|
|
font-size: 30rpx;
|
|
color: $text-primary;
|
|
}
|
|
|
|
.picker-arrow {
|
|
font-size: 24rpx;
|
|
color: $text-hint;
|
|
}
|
|
|
|
.danger-link {
|
|
padding: 20rpx 0 8rpx;
|
|
}
|
|
|
|
.danger-link-text {
|
|
font-size: 24rpx;
|
|
color: $error-color;
|
|
}
|
|
|
|
.save-btn {
|
|
margin-top: 12rpx;
|
|
height: 88rpx;
|
|
border-radius: 18rpx;
|
|
background: $brand-color;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.save-btn--disabled { opacity: 0.5; }
|
|
|
|
.save-btn-text {
|
|
font-size: 30rpx;
|
|
font-weight: 700;
|
|
color: #fff8f0;
|
|
}
|
|
</style>
|