feat: 支持管理员消息推送

This commit is contained in:
richarjiang
2026-04-12 22:18:34 +08:00
parent c60821c5ff
commit 6cee28bf66
26 changed files with 780 additions and 161 deletions

View File

@@ -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;

View File

@@ -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: '预约中...' })

View File

@@ -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({

View File

@@ -123,6 +123,7 @@ async function refreshData() {
if (userStore.loggedIn) {
tasks.push(
userStore.fetchProfile(),
userStore.fetchMemberships(),
bookingStore.fetchUpcomingBookings(),
)

View File

@@ -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 {