Files
mp-pilates/packages/app/src/pages/admin/member-edit.vue
richarjiang 75ef5e94a6 refactor: 调整 admin 目录结构并精简个人中心快捷区
- 将 admin 相关的 store/utils 移入对应子目录(pages/admin/stores、pages/admin/utils)
- 更新 manifest.json、pages.json 路由与配置
- 个人中心 ProfileMenu 移除顶部「我的会员卡 / 我的预约」快捷卡片,与 UserCard 会员卡入口合并
- 「我的预约」下移至菜单列表,紧邻「个人信息」
- 清理 profile/index.vue 中不再使用的 bookingStore / 计算属性 / 透传
2026-09-09 14:36:34 +08:00

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>