feat: 支持管理员消息推送
This commit is contained in:
@@ -169,6 +169,21 @@
|
||||
<text class="arrow-text">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="list-item" @tap="handleIncreaseSubscriptionCount">
|
||||
<view class="item-left">
|
||||
<view class="item-icon-wrap icon--subscribe">
|
||||
<text class="item-icon-text">✦</text>
|
||||
</view>
|
||||
<view class="item-text-group">
|
||||
<text class="item-title">增加订阅次数</text>
|
||||
<text class="item-desc">当前剩余 {{ user?.adminBookingSubscriptionCount ?? 0 }} 次</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-arrow">
|
||||
<text class="arrow-text">{{ adminSubscribeLoading ? '...' : '›' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 40rpx" />
|
||||
@@ -177,17 +192,24 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import CustomNavBar from '../../components/CustomNavBar.vue'
|
||||
import { getSystemLayout } from '../../utils/system'
|
||||
import { useAdminStore } from '../../stores/admin'
|
||||
import { useUserStore } from '../../stores/user'
|
||||
import type { AdminStats } from '../../stores/admin'
|
||||
import { requestAdminBookingSubscriptionCount } from '../../utils/wechat-subscription'
|
||||
import { getErrorMessage } from '../../utils/auth'
|
||||
|
||||
const navBarHeight = ref('64px')
|
||||
|
||||
const adminStore = useAdminStore()
|
||||
const userStore = useUserStore()
|
||||
const { user } = storeToRefs(userStore)
|
||||
|
||||
const statsLoading = ref(false)
|
||||
const stats = ref<AdminStats>({ todayBookings: 0, totalOrders: 0, totalBookings: 0 })
|
||||
const adminSubscribeLoading = ref(false)
|
||||
|
||||
function navigate(path: string) {
|
||||
uni.navigateTo({ url: path })
|
||||
@@ -204,9 +226,32 @@ async function loadStats() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleIncreaseSubscriptionCount() {
|
||||
if (adminSubscribeLoading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
adminSubscribeLoading.value = true
|
||||
try {
|
||||
const profile = await requestAdminBookingSubscriptionCount()
|
||||
if (!profile) {
|
||||
uni.showToast({ title: '已取消本次授权', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
userStore.setProfile(profile)
|
||||
uni.showToast({ title: `订阅次数 +1,剩余 ${profile.adminBookingSubscriptionCount}`, icon: 'none' })
|
||||
} catch (err: unknown) {
|
||||
uni.showToast({ title: getErrorMessage(err, '订阅授权失败'), icon: 'none' })
|
||||
} finally {
|
||||
adminSubscribeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
navBarHeight.value = `${getSystemLayout().navBarHeight}px`
|
||||
loadStats()
|
||||
userStore.fetchProfile()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -347,6 +392,7 @@ onMounted(() => {
|
||||
.icon--card { background: linear-gradient(135deg, #C48E7E, #B47E6E); }
|
||||
.icon--flash-sale { background: linear-gradient(135deg, #D4A59A, #C08B7E); }
|
||||
.icon--studio { background: linear-gradient(135deg, #9E9E7E, #8E8E6E); }
|
||||
.icon--subscribe { background: linear-gradient(135deg, #5D8C8A, #476D72); }
|
||||
|
||||
.item-text-group {
|
||||
display: flex;
|
||||
|
||||
@@ -207,6 +207,11 @@
|
||||
<text class="action-btn-text">立即预约</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="slotData.isBookedByMe && slotData.myBookingId" class="action-bar">
|
||||
<view class="action-btn action-btn--disabled" @tap="goToMyBookingDetail">
|
||||
<text class="action-btn-text">{{ slotActionLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="slotData.status === TimeSlotStatus.FULL" class="action-bar">
|
||||
<view class="action-btn action-btn--disabled">
|
||||
<text class="action-btn-text">已约满</text>
|
||||
@@ -293,6 +298,14 @@ const canBook = computed(() => {
|
||||
return !isSlotPast(slotData.value.date, slotData.value.startTime)
|
||||
})
|
||||
|
||||
const slotActionLabel = computed(() => {
|
||||
if (slotData.value?.myBookingStatus === BookingStatus.PENDING_CONFIRMATION) {
|
||||
return '已预约待确认'
|
||||
}
|
||||
|
||||
return '已预约'
|
||||
})
|
||||
|
||||
const slotCapacityClass = computed(() => {
|
||||
if (!slotData.value) return ''
|
||||
const { bookedCount, capacity } = slotData.value
|
||||
@@ -346,6 +359,11 @@ async function loadSlotData() {
|
||||
|
||||
// ─── Slot mode: Booking flow ─────────────────────────────────────────────
|
||||
async function handleSlotBook() {
|
||||
if (slotData.value?.isBookedByMe) {
|
||||
goToMyBookingDetail()
|
||||
return
|
||||
}
|
||||
|
||||
if (!userStore.loggedIn) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
@@ -386,6 +404,11 @@ async function handleSlotBook() {
|
||||
showConfirmPopup.value = true
|
||||
}
|
||||
|
||||
function goToMyBookingDetail() {
|
||||
if (!slotData.value?.myBookingId) return
|
||||
uni.redirectTo({ url: `/pages/booking/detail?id=${slotData.value.myBookingId}` })
|
||||
}
|
||||
|
||||
async function onConfirmBooking(payload: { timeSlotId: string; membershipId: string }) {
|
||||
showConfirmPopup.value = false
|
||||
uni.showLoading({ title: '预约中...' })
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
import type { TimeSlotWithBookingStatus, MembershipWithCardType } from '@mp-pilates/shared'
|
||||
import { TIME_PERIODS } from '@mp-pilates/shared'
|
||||
import { BookingStatus, TIME_PERIODS } from '@mp-pilates/shared'
|
||||
import { useBookingStore } from '../../stores/booking'
|
||||
import { useUserStore } from '../../stores/user'
|
||||
import { getErrorMessage } from '../../utils/auth'
|
||||
@@ -196,6 +196,19 @@ function onSlotCardTap(slot: TimeSlotWithBookingStatus) {
|
||||
|
||||
// ─── Book flow ────────────────────────────────────────────
|
||||
async function onBookTap(slot: TimeSlotWithBookingStatus) {
|
||||
if (slot.isBookedByMe) {
|
||||
if (slot.myBookingId) {
|
||||
uni.navigateTo({ url: `/pages/booking/detail?id=${slot.myBookingId}` })
|
||||
return
|
||||
}
|
||||
|
||||
const title = slot.myBookingStatus === BookingStatus.PENDING_CONFIRMATION
|
||||
? '该时段已预约,等待老师确认'
|
||||
: '该时段已预约'
|
||||
uni.showToast({ title, icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Ensure logged in
|
||||
if (!userStore.loggedIn) {
|
||||
uni.showModal({
|
||||
|
||||
@@ -123,6 +123,7 @@ async function refreshData() {
|
||||
|
||||
if (userStore.loggedIn) {
|
||||
tasks.push(
|
||||
userStore.fetchProfile(),
|
||||
userStore.fetchMemberships(),
|
||||
bookingStore.fetchUpcomingBookings(),
|
||||
)
|
||||
|
||||
@@ -8,8 +8,14 @@
|
||||
:loading="loginLoading" :nav-bar-height="navBarHeight" @login="handleLogin" />
|
||||
|
||||
<!-- Menu section: always visible -->
|
||||
<ProfileMenu :is-admin="isAdmin" :require-auth="loggedIn" @clear-cache="handleClearCache" @about="handleAbout"
|
||||
@require-login="handleLogin" />
|
||||
<ProfileMenu
|
||||
:is-admin="isAdmin"
|
||||
:require-auth="loggedIn"
|
||||
:active-membership-count="activeMembershipCount"
|
||||
@clear-cache="handleClearCache"
|
||||
@about="handleAbout"
|
||||
@require-login="handleLogin"
|
||||
/>
|
||||
|
||||
<!-- Logout button: only when logged in -->
|
||||
<view v-if="loggedIn" class="profile-page__logout-wrap">
|
||||
@@ -19,7 +25,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShow, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useUserStore } from '../../stores/user'
|
||||
@@ -35,6 +41,10 @@ const { loggedIn, hasProfile, user, stats, memberships, isAdmin } = storeToRefs(
|
||||
const loginLoading = ref(false)
|
||||
const navBarHeight = ref(64)
|
||||
|
||||
const activeMembershipCount = computed(
|
||||
() => user.value?.activeMembershipCount ?? userStore.activeMemberships.length,
|
||||
)
|
||||
|
||||
// ─── 微信分享 ───────────────────────────────────────────────
|
||||
onShareAppMessage(() => {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user