diff --git a/miniprogram/docs/superpowers/plans/2026-06-08-merged-venue-miniapp.md b/miniprogram/docs/superpowers/plans/2026-06-08-merged-venue-miniapp.md new file mode 100644 index 0000000..33850ac --- /dev/null +++ b/miniprogram/docs/superpowers/plans/2026-06-08-merged-venue-miniapp.md @@ -0,0 +1,1683 @@ +# Merged Venue Miniapp Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a new merged uni-app WeChat mini program under `场馆小程序/miniprogram` that lets users choose and switch between ice venue and comprehensive venue while keeping both Django backends, auth tokens, APIs, and business pages isolated. + +**Architecture:** The app uses one public venue selection shell and two isolated modules: `src/modules/ice` and `src/modules/comprehensive`. Route pages live under `src/pages/ice/*` and `src/pages/comprehensive/*`, while each module imports only its own API client and state. No backend code is changed. + +**Tech Stack:** uni-app, Vue 3, Vite 5, Pinia, uview-plus, dayjs, WeChat mini program target. + +--- + +## File Structure Map + +- Create `miniprogram/package.json`: merged project dependency and script manifest copied from the existing miniapp projects. +- Create `miniprogram/package-lock.json`: copied from the comprehensive project first, then refreshed by `npm install` if needed. +- Create `miniprogram/vite.config.js`: Vite + uni plugin config with two API env variables and two H5 proxy prefixes. +- Create `miniprogram/.env`: production-like default backend URLs for both venues. +- Create `miniprogram/.env.example`: local development URL examples for both venues. +- Create `miniprogram/src/main.js`: Vue app bootstrap with Pinia and uview-plus. +- Create `miniprogram/src/App.vue`: merged global styles and app lifecycle logging. +- Create `miniprogram/src/manifest.json`: WeChat app manifest using `wx516b3e0d9940c788`. +- Create `miniprogram/src/uni.scss`: shared SCSS imports. +- Create `miniprogram/src/styles/variables.scss`: shared design variables and mixins. +- Create `miniprogram/src/pages.json`: merged page registry, no global `tabBar` in first version. +- Create `miniprogram/src/stores/venue.js`: public Pinia store for current venue selection. +- Create `miniprogram/src/utils/venue.js`: venue constants, storage helpers, target home paths, and navigation helpers. +- Create `miniprogram/src/pages/venue-select/index.vue`: first-entry venue selector and switch page. +- Create `miniprogram/src/modules/comprehensive/api/*`: comprehensive backend API modules. +- Create `miniprogram/src/modules/comprehensive/stores/session.js`: comprehensive session store with dedicated store id. +- Create `miniprogram/src/modules/comprehensive/utils/*`: comprehensive business utility modules. +- Create `miniprogram/src/pages/comprehensive/*`: comprehensive venue route pages. +- Create `miniprogram/src/modules/ice/api/*`: ice backend API modules. +- Create `miniprogram/src/components/ice/StatePanel.vue`: ice module state panel component. +- Create `miniprogram/src/pages/ice/*`: ice venue route pages. +- Create `miniprogram/src/static/comprehensive/tab/*`: comprehensive tab image assets for future module navigation. +- Create `miniprogram/src/static/ice/tab/*`: ice tab image assets for future module navigation. + +## Source Roots + +- Existing comprehensive miniapp: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/综合场馆小程序/miniprogram` +- Existing ice miniapp: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/冰场馆小程序/miniprogram` +- New merged miniapp: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram` + +## Implementation Tasks + +### Task 1: Scaffold the Merged Miniapp Shell + +**Files:** +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/package.json` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/package-lock.json` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/vite.config.js` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/.env` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/.env.example` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/main.js` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/App.vue` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/manifest.json` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/uni.scss` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/styles/variables.scss` + +- [ ] **Step 1: Copy baseline project metadata** + +Run: + +```bash +cp '/Volumes/ExternalStorageA/project/2026.5/体育场馆/综合场馆小程序/miniprogram/package.json' '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/package.json' +cp '/Volumes/ExternalStorageA/project/2026.5/体育场馆/综合场馆小程序/miniprogram/package-lock.json' '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/package-lock.json' +``` + +Expected: both files exist in `场馆小程序/miniprogram`. + +- [ ] **Step 2: Update merged package metadata** + +Edit `miniprogram/package.json` to: + +```json +{ + "name": "merged-venue-miniapp", + "version": "0.1.0", + "description": "冰场馆与综合场馆合并版微信小程序端", + "type": "module", + "scripts": { + "dev:mp-weixin": "uni -p mp-weixin", + "build:mp-weixin": "uni build -p mp-weixin", + "dev:h5": "uni", + "build:h5": "uni build" + }, + "dependencies": { + "@dcloudio/uni-app": "3.0.0-alpha-5000120260211001", + "@dcloudio/uni-components": "3.0.0-alpha-5000120260211001", + "@dcloudio/uni-h5": "3.0.0-alpha-5000120260211001", + "@dcloudio/uni-mp-weixin": "3.0.0-alpha-5000120260211001", + "dayjs": "^1.11.21", + "pinia": "^2.3.1", + "uview-plus": "^3.8.37", + "vue": "^3.4.0" + }, + "devDependencies": { + "@dcloudio/uni-automator": "3.0.0-alpha-5000120260211001", + "@dcloudio/uni-cli-shared": "3.0.0-alpha-5000120260211001", + "@dcloudio/uni-stacktracey": "3.0.0-alpha-5000120260211001", + "@dcloudio/vite-plugin-uni": "3.0.0-alpha-5000120260211001", + "sass": "^1.83.0", + "vite": "^5.2.0" + } +} +``` + +Expected: package name and description identify the merged app; dependencies remain identical to the two existing miniapps. + +- [ ] **Step 3: Create environment files** + +Create `miniprogram/.env`: + +```dotenv +VITE_ICE_API_BASE_URL=https://venue.test.chatgqt.top +VITE_COMPREHENSIVE_API_BASE_URL=https://venue2.test.chatgqt.top +``` + +Create `miniprogram/.env.example`: + +```dotenv +VITE_ICE_API_BASE_URL=http://127.0.0.1:8000 +VITE_COMPREHENSIVE_API_BASE_URL=http://127.0.0.1:8001 +``` + +Expected: no old single `VITE_API_BASE_URL` remains in these files. + +- [ ] **Step 4: Create Vite config** + +Create `miniprogram/vite.config.js`: + +```js +import { fileURLToPath, URL } from 'node:url' +import { defineConfig, loadEnv } from 'vite' +import uniPlugin from '@dcloudio/vite-plugin-uni' + +const uni = typeof uniPlugin === 'function' ? uniPlugin : uniPlugin.default + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + const iceApiBase = env.VITE_ICE_API_BASE_URL || 'http://127.0.0.1:8000' + const comprehensiveApiBase = env.VITE_COMPREHENSIVE_API_BASE_URL || 'http://127.0.0.1:8001' + + return { + plugins: [uni()], + optimizeDeps: { + include: ['uview-plus'], + }, + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + css: { + preprocessorOptions: { + scss: { + api: 'modern-compiler', + }, + }, + }, + server: { + proxy: { + '/ice-api': { + target: iceApiBase, + changeOrigin: true, + secure: false, + rewrite: (path) => path.replace(/^\/ice-api/, ''), + }, + '/comprehensive-api': { + target: comprehensiveApiBase, + changeOrigin: true, + secure: false, + rewrite: (path) => path.replace(/^\/comprehensive-api/, ''), + }, + }, + }, + } +}) +``` + +Expected: H5 development can proxy both backends; WeChat builds still use env URLs directly. + +- [ ] **Step 5: Create app bootstrap** + +Create `miniprogram/src/main.js`: + +```js +import { createSSRApp } from 'vue' +import App from './App.vue' +import * as Pinia from 'pinia' +import uviewPlus from 'uview-plus' +import 'uview-plus/index.scss' + +export function createApp() { + const app = createSSRApp(App) + app.use(Pinia.createPinia()) + app.use(uviewPlus) + return { + app, + Pinia + } +} +``` + +Expected: Pinia and uview-plus are globally registered. + +- [ ] **Step 6: Create manifest** + +Create `miniprogram/src/manifest.json`: + +```json +{ + "name": "场馆服务", + "appid": "", + "description": "冰场馆与综合场馆合并版会员服务小程序", + "versionName": "0.1.0", + "versionCode": "100", + "transformPx": false, + "mp-weixin": { + "appid": "wx516b3e0d9940c788", + "setting": { + "urlCheck": false, + "es6": true, + "postcss": true, + "minified": true + }, + "usingComponents": true + }, + "vueVersion": "3" +} +``` + +Expected: WeChat appid is `wx516b3e0d9940c788`. + +- [ ] **Step 7: Create global SCSS files** + +Create `miniprogram/src/uni.scss`: + +```scss +@use "@/styles/variables.scss" as *; +@import "uview-plus/theme.scss"; +``` + +Create `miniprogram/src/styles/variables.scss`: + +```scss +$bg-color-main: #FFFFFF; +$bg-color-soft: #F8FAFC; +$bg-color-hover: #F1F5F9; +$bg-color-panel: #EFF6FF; + +$text-color-main: #0F172A; +$text-color-regular: #334155; +$text-color-muted: #64748B; +$text-color-light: #94A3B8; + +$color-primary: #0EA5E9; +$color-primary-light: #E0F2FE; +$color-success: #10B981; +$color-success-light: #D1FAE5; +$color-warning: #F59E0B; +$color-warning-light: #FEF3C7; +$color-danger: #EF4444; +$color-danger-light: #FEE2E2; + +$border-color: #E2E8F0; +$border-radius-sm: 6rpx; +$border-radius-base: 12rpx; +$border-radius-lg: 20rpx; + +$spacing-xs: 8rpx; +$spacing-sm: 16rpx; +$spacing-md: 24rpx; +$spacing-lg: 32rpx; +$spacing-xl: 48rpx; + +@mixin minimal-card { + background: $bg-color-main; + border: 1rpx solid $border-color; + border-radius: $border-radius-base; + padding: $spacing-md; + box-shadow: 0 2rpx 8rpx rgba(15, 23, 42, 0.03); +} + +@mixin text-ellipsis { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +``` + +Expected: SCSS variables used by both old projects are available. + +- [ ] **Step 8: Create global App component** + +Create `miniprogram/src/App.vue`: + +```vue + + + +``` + +Expected: common global classes used by the comprehensive project remain available, and ice pages still get the shared variables. + +- [ ] **Step 9: Commit shell** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序' +git add miniprogram/package.json miniprogram/package-lock.json miniprogram/vite.config.js miniprogram/.env miniprogram/.env.example miniprogram/src/main.js miniprogram/src/App.vue miniprogram/src/manifest.json miniprogram/src/uni.scss miniprogram/src/styles/variables.scss +git commit -m "feat: scaffold merged miniapp shell" +``` + +Expected: commit succeeds with only shell files staged. + +### Task 2: Add Public Venue Selection State and Page + +**Files:** +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/utils/venue.js` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/stores/venue.js` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/pages/venue-select/index.vue` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/pages.json` + +- [ ] **Step 1: Create venue utility module** + +Create `miniprogram/src/utils/venue.js`: + +```js +export const CURRENT_VENUE_KEY = 'CURRENT_VENUE' + +export const VENUE_KEYS = Object.freeze({ + ICE: 'ice', + COMPREHENSIVE: 'comprehensive' +}) + +export const VENUE_OPTIONS = Object.freeze([ + { + key: VENUE_KEYS.ICE, + name: '冰场馆', + description: '课程预约、会员卡与滑冰服务', + homePath: '/pages/ice/home/index' + }, + { + key: VENUE_KEYS.COMPREHENSIVE, + name: '综合场馆', + description: '课程、包场、门票、订单与余额', + homePath: '/pages/comprehensive/home/index' + } +]) + +export const isVenueKey = (value) => { + return value === VENUE_KEYS.ICE || value === VENUE_KEYS.COMPREHENSIVE +} + +export const getVenueOption = (venueKey) => { + return VENUE_OPTIONS.find((item) => item.key === venueKey) || null +} + +export const getStoredVenue = () => { + const venue = uni.getStorageSync(CURRENT_VENUE_KEY) + return isVenueKey(venue) ? venue : '' +} + +export const setStoredVenue = (venueKey) => { + if (!isVenueKey(venueKey)) { + uni.removeStorageSync(CURRENT_VENUE_KEY) + return '' + } + + uni.setStorageSync(CURRENT_VENUE_KEY, venueKey) + return venueKey +} + +export const navigateToVenueHome = (venueKey, options = {}) => { + const venue = getVenueOption(venueKey) + if (!venue) { + uni.reLaunch({ url: '/pages/venue-select/index' }) + return + } + + const navigate = options.redirect === true ? uni.redirectTo : uni.reLaunch + navigate({ url: venue.homePath }) +} +``` + +Expected: utility has a single source of truth for venue keys and home paths. + +- [ ] **Step 2: Create venue Pinia store** + +Create `miniprogram/src/stores/venue.js`: + +```js +import { defineStore } from 'pinia' +import { getStoredVenue, getVenueOption, navigateToVenueHome, setStoredVenue } from '@/utils/venue' + +export const useVenueStore = defineStore('currentVenue', { + state: () => ({ + currentVenue: '' + }), + getters: { + currentVenueOption: (state) => getVenueOption(state.currentVenue), + hasSelectedVenue: (state) => Boolean(getVenueOption(state.currentVenue)) + }, + actions: { + init() { + this.currentVenue = getStoredVenue() + return this.currentVenue + }, + selectVenue(venueKey) { + this.currentVenue = setStoredVenue(venueKey) + return this.currentVenue + }, + enterVenue(venueKey) { + const selectedVenue = this.selectVenue(venueKey) + navigateToVenueHome(selectedVenue) + } + } +}) +``` + +Expected: selection persists and can navigate to module home. + +- [ ] **Step 3: Create venue selection page** + +Create `miniprogram/src/pages/venue-select/index.vue`: + +```vue + + + + + +``` + +Expected: first page gives two concrete choices and persists selection before navigation. + +- [ ] **Step 4: Create initial pages registry** + +Create `miniprogram/src/pages.json`: + +```json +{ + "easycom": { + "autoscan": true, + "custom": { + "^u--(.*)": "uview-plus/components/u-$1/u-$1.vue", + "^up-(.*)": "uview-plus/components/u-$1/u-$1.vue", + "^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue" + } + }, + "pages": [ + { + "path": "pages/venue-select/index", + "style": { + "navigationBarTitleText": "选择场馆", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black" + } + } + ], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F8FAFC", + "backgroundColorBottom": "#F8FAFC", + "backgroundColorTop": "#FFFFFF" + } +} +``` + +Expected: app has a valid first page before business modules are migrated. + +- [ ] **Step 5: Verify selection shell builds** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram' +npm install +npm run build:mp-weixin +``` + +Expected: build succeeds or fails only because `/pages/ice/home/index` and `/pages/comprehensive/home/index` do not exist yet after tapping; compile-time should not require those target files until pages are registered. If compile fails for missing dependencies, fix dependency installation before continuing. + +- [ ] **Step 6: Commit selection shell** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序' +git add miniprogram/src/utils/venue.js miniprogram/src/stores/venue.js miniprogram/src/pages/venue-select/index.vue miniprogram/src/pages.json miniprogram/package-lock.json +git commit -m "feat: add venue selection shell" +``` + +Expected: commit succeeds with public shell files and any refreshed lockfile. + +### Task 3: Migrate Comprehensive Venue Module + +**Files:** +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/modules/comprehensive/api/*.js` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/modules/comprehensive/stores/session.js` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/modules/comprehensive/utils/*.js` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/pages/comprehensive/**` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/static/comprehensive/tab/*.png` +- Modify: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/pages.json` + +- [ ] **Step 1: Copy comprehensive source files into namespaced locations** + +Run: + +```bash +mkdir -p '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/modules/comprehensive' +mkdir -p '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/pages/comprehensive' +mkdir -p '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/static/comprehensive' +cp -R '/Volumes/ExternalStorageA/project/2026.5/体育场馆/综合场馆小程序/miniprogram/src/api' '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/modules/comprehensive/api' +cp -R '/Volumes/ExternalStorageA/project/2026.5/体育场馆/综合场馆小程序/miniprogram/src/stores' '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/modules/comprehensive/stores' +cp -R '/Volumes/ExternalStorageA/project/2026.5/体育场馆/综合场馆小程序/miniprogram/src/utils' '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/modules/comprehensive/utils' +cp -R '/Volumes/ExternalStorageA/project/2026.5/体育场馆/综合场馆小程序/miniprogram/src/pages/.' '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/pages/comprehensive/' +cp -R '/Volumes/ExternalStorageA/project/2026.5/体育场馆/综合场馆小程序/miniprogram/src/static/tab' '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/static/comprehensive/tab' +``` + +Expected: comprehensive pages exist under `src/pages/comprehensive`, and API/store/utils exist under `src/modules/comprehensive`. + +- [ ] **Step 2: Update comprehensive request base URL** + +Edit `miniprogram/src/modules/comprehensive/api/request.js` so the top constants are: + +```js +const KEY_TOKEN = 'COMPREHENSIVE_VENUE_TOKEN' + +const API_BASE_URL = import.meta.env.VITE_COMPREHENSIVE_API_BASE_URL || 'http://127.0.0.1:8001' +``` + +Expected: `VITE_API_BASE_URL` is not used in this module. + +- [ ] **Step 3: Rename comprehensive session store id and imports** + +Edit `miniprogram/src/modules/comprehensive/stores/session.js`: + +```js +import { defineStore } from 'pinia' +import { authApi, authStorage } from '@/modules/comprehensive/api/auth' +import { memberApi } from '@/modules/comprehensive/api/member' + +export const useComprehensiveSessionStore = defineStore('comprehensiveVenueSession', { + state: () => ({ + isReady: false, + isLoggedIn: false, + member: null + }), + actions: { + async init() { + this.isLoggedIn = authStorage.isLoggedIn() + if (this.isLoggedIn) { + try { + this.member = await memberApi.getMemberMe() + } catch (error) { + if ([40001, 40002, 401, 403].includes(error.code) || [401, 403].includes(error.statusCode)) { + authStorage.clear() + this.member = null + this.isLoggedIn = false + } + } + } + this.isReady = true + }, + async login() { + const loginResult = await loginByWechat() + const data = await authApi.wechatLogin({ code: loginResult.code }) + this.member = data.member + this.isLoggedIn = true + }, + logout() { + authApi.logout() + this.member = null + this.isLoggedIn = false + }, + async refreshMember() { + if (!authStorage.isLoggedIn()) return + this.member = await memberApi.getMemberMe() + this.isLoggedIn = true + } + } +}) + +const loginByWechat = () => new Promise((resolve, reject) => { + uni.login({ + provider: 'weixin', + success: (res) => { + if (!res.code) { + reject(new Error('微信登录未返回 code')) + return + } + resolve(res) + }, + fail: reject + }) +}) +``` + +Expected: no `@/api` imports and no `defineStore('session')` remain in this file. + +- [ ] **Step 4: Rewrite comprehensive page imports** + +Run these replacements from `miniprogram`: + +```bash +perl -pi -e "s#@/api/#@/modules/comprehensive/api/#g" src/pages/comprehensive/**/*.vue +perl -pi -e "s#@/stores/session#@/modules/comprehensive/stores/session#g" src/pages/comprehensive/**/*.vue +perl -pi -e "s#@/utils/#@/modules/comprehensive/utils/#g" src/pages/comprehensive/**/*.vue +perl -pi -e "s#useSessionStore#useComprehensiveSessionStore#g" src/pages/comprehensive/**/*.vue +``` + +Expected: comprehensive pages import from `@/modules/comprehensive/*`. + +- [ ] **Step 5: Rewrite comprehensive route strings** + +Run these replacements from `miniprogram`: + +```bash +perl -pi -e "s#/pages/home#/pages/comprehensive/home#g" src/pages/comprehensive/**/*.vue +perl -pi -e "s#/pages/courses#/pages/comprehensive/courses#g" src/pages/comprehensive/**/*.vue +perl -pi -e "s#/pages/bookings#/pages/comprehensive/bookings#g" src/pages/comprehensive/**/*.vue +perl -pi -e "s#/pages/profile#/pages/comprehensive/profile#g" src/pages/comprehensive/**/*.vue +perl -pi -e "s#/pages/activities#/pages/comprehensive/activities#g" src/pages/comprehensive/**/*.vue +perl -pi -e "s#/pages/private#/pages/comprehensive/private#g" src/pages/comprehensive/**/*.vue +perl -pi -e "s#/pages/tickets#/pages/comprehensive/tickets#g" src/pages/comprehensive/**/*.vue +perl -pi -e "s#/pages/orders#/pages/comprehensive/orders#g" src/pages/comprehensive/**/*.vue +perl -pi -e "s#/pages/pay#/pages/comprehensive/pay#g" src/pages/comprehensive/**/*.vue +``` + +Expected: comprehensive pages no longer navigate to unprefixed old routes. + +- [ ] **Step 6: Add comprehensive switch entry to profile menu** + +Edit `miniprogram/src/pages/comprehensive/profile/index.vue`. + +Add one item to `menuItems` after the personal info item: + +```js +{ title: '切换场馆', url: '/pages/venue-select/index', theme: 'profile-theme', icon: 'profile' } +``` + +Expected: logged-in comprehensive users can navigate to the venue selection page. If the page shows no menu when logged out, the venue selector remains available by manually opening the first page; Task 7 will also validate direct switching. + +- [ ] **Step 7: Register comprehensive pages** + +Modify `miniprogram/src/pages.json` by appending these page entries after `pages/venue-select/index`: + +```json +{ + "path": "pages/comprehensive/home/index", + "style": { + "navigationBarTitleText": "首页", + "enablePullDownRefresh": true + } +}, +{ + "path": "pages/comprehensive/courses/index", + "style": { + "navigationBarTitleText": "课程", + "enablePullDownRefresh": true + } +}, +{ + "path": "pages/comprehensive/bookings/index", + "style": { + "navigationBarTitleText": "我的预约", + "enablePullDownRefresh": true + } +}, +{ + "path": "pages/comprehensive/profile/index", + "style": { + "navigationBarTitleText": "会员中心" + } +}, +{ + "path": "pages/comprehensive/activities/index", + "style": { + "navigationBarTitleText": "活动公告", + "enablePullDownRefresh": true + } +}, +{ + "path": "pages/comprehensive/activities/detail", + "style": { + "navigationBarTitleText": "活动详情" + } +}, +{ + "path": "pages/comprehensive/courses/detail", + "style": { + "navigationBarTitleText": "课程详情" + } +}, +{ + "path": "pages/comprehensive/courses/confirm", + "style": { + "navigationBarTitleText": "确认课程预约" + } +}, +{ + "path": "pages/comprehensive/private/index", + "style": { + "navigationBarTitleText": "包场预约" + } +}, +{ + "path": "pages/comprehensive/private/confirm", + "style": { + "navigationBarTitleText": "确认包场" + } +}, +{ + "path": "pages/comprehensive/tickets/index", + "style": { + "navigationBarTitleText": "篮球门票" + } +}, +{ + "path": "pages/comprehensive/pay/index", + "style": { + "navigationBarTitleText": "订单支付" + } +}, +{ + "path": "pages/comprehensive/orders/detail", + "style": { + "navigationBarTitleText": "订单详情" + } +}, +{ + "path": "pages/comprehensive/bookings/course-detail", + "style": { + "navigationBarTitleText": "课程预约详情" + } +}, +{ + "path": "pages/comprehensive/bookings/private-detail", + "style": { + "navigationBarTitleText": "包场预约详情" + } +}, +{ + "path": "pages/comprehensive/profile/balance", + "style": { + "navigationBarTitleText": "余额流水", + "enablePullDownRefresh": true + } +}, +{ + "path": "pages/comprehensive/profile/tickets", + "style": { + "navigationBarTitleText": "我的门票", + "enablePullDownRefresh": true + } +}, +{ + "path": "pages/comprehensive/profile/orders", + "style": { + "navigationBarTitleText": "我的订单", + "enablePullDownRefresh": true + } +}, +{ + "path": "pages/comprehensive/profile/detail", + "style": { + "navigationBarTitleText": "个人信息" + } +} +``` + +Expected: all comprehensive pages from the old `pages.json` are registered with the `pages/comprehensive` prefix. + +- [ ] **Step 8: Verify comprehensive module references** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram' +rg -n "@/api|@/stores/session|@/utils/" src/pages/comprehensive src/modules/comprehensive +rg -n "'/pages/(home|courses|bookings|profile|activities|private|tickets|orders|pay)|\"/pages/(home|courses|bookings|profile|activities|private|tickets|orders|pay)" src/pages/comprehensive +rg -n "VITE_API_BASE_URL" src/modules/comprehensive +``` + +Expected: all three commands return no matches. + +- [ ] **Step 9: Build after comprehensive migration** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram' +npm run build:mp-weixin +``` + +Expected: build succeeds, or fails only because ice pages have not been registered yet. If the failure references comprehensive imports, fix those references before continuing. + +- [ ] **Step 10: Commit comprehensive module** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序' +git add miniprogram/src/modules/comprehensive miniprogram/src/pages/comprehensive miniprogram/src/static/comprehensive miniprogram/src/pages.json +git commit -m "feat: migrate comprehensive venue module" +``` + +Expected: commit succeeds with comprehensive module files. + +### Task 4: Migrate Ice Venue Module + +**Files:** +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/modules/ice/api/*.js` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/components/ice/StatePanel.vue` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/pages/ice/**` +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/static/ice/tab/*.png` +- Modify: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/pages.json` + +- [ ] **Step 1: Copy ice source files into namespaced locations** + +Run: + +```bash +mkdir -p '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/modules/ice' +mkdir -p '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/pages/ice' +mkdir -p '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/components/ice' +mkdir -p '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/static/ice' +cp -R '/Volumes/ExternalStorageA/project/2026.5/体育场馆/冰场馆小程序/miniprogram/src/api' '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/modules/ice/api' +cp '/Volumes/ExternalStorageA/project/2026.5/体育场馆/冰场馆小程序/miniprogram/src/components/StatePanel.vue' '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/components/ice/StatePanel.vue' +cp -R '/Volumes/ExternalStorageA/project/2026.5/体育场馆/冰场馆小程序/miniprogram/src/pages/.' '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/pages/ice/' +cp -R '/Volumes/ExternalStorageA/project/2026.5/体育场馆/冰场馆小程序/miniprogram/src/static/tab' '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/static/ice/tab' +``` + +Expected: ice pages exist under `src/pages/ice`, API under `src/modules/ice/api`, and state panel under `src/components/ice`. + +- [ ] **Step 2: Update ice request base URL** + +Edit `miniprogram/src/modules/ice/api/request.js` so the base URL constant is: + +```js +const API_BASE_URL = import.meta.env.VITE_ICE_API_BASE_URL || 'http://127.0.0.1:8000' +``` + +Expected: `VITE_API_BASE_URL` is not used in this module. + +- [ ] **Step 3: Rewrite ice page imports** + +Run these replacements from `miniprogram`: + +```bash +perl -pi -e "s#@/api/#@/modules/ice/api/#g" src/pages/ice/**/*.vue +perl -pi -e "s#@/components/StatePanel.vue#@/components/ice/StatePanel.vue#g" src/pages/ice/**/*.vue +``` + +Expected: ice pages import from `@/modules/ice/api/*` and `@/components/ice/StatePanel.vue`. + +- [ ] **Step 4: Rewrite ice route strings** + +Run these replacements from `miniprogram`: + +```bash +perl -pi -e "s#/pages/home#/pages/ice/home#g" src/pages/ice/**/*.vue +perl -pi -e "s#/pages/courses#/pages/ice/courses#g" src/pages/ice/**/*.vue +perl -pi -e "s#/pages/bookings#/pages/ice/bookings#g" src/pages/ice/**/*.vue +perl -pi -e "s#/pages/profile#/pages/ice/profile#g" src/pages/ice/**/*.vue +perl -pi -e "s#/pages/activities#/pages/ice/activities#g" src/pages/ice/**/*.vue +``` + +Expected: ice pages no longer navigate to unprefixed old routes. + +- [ ] **Step 5: Add ice switch entry to profile page** + +Edit `miniprogram/src/pages/ice/profile/index.vue`. + +Add a menu item in the logged-in menu group before the logout item: + +```vue + + + + + + + + + 切换场馆 + + + + + +``` + +If the page does not define `.profile-theme` or `.svg-profile-minimal`, use the existing `card-theme` icon container classes instead: + +```vue + + + + + +``` + +Expected: logged-in ice users can navigate to the venue selection page. + +- [ ] **Step 6: Register ice pages** + +Modify `miniprogram/src/pages.json` by appending these page entries after the comprehensive entries: + +```json +{ + "path": "pages/ice/home/index", + "style": { + "navigationBarTitleText": "首页", + "enablePullDownRefresh": true + } +}, +{ + "path": "pages/ice/courses/index", + "style": { + "navigationBarTitleText": "近期课程", + "enablePullDownRefresh": true + } +}, +{ + "path": "pages/ice/bookings/index", + "style": { + "navigationBarTitleText": "我的预约", + "enablePullDownRefresh": true + } +}, +{ + "path": "pages/ice/profile/index", + "style": { + "navigationBarTitleText": "会员中心" + } +}, +{ + "path": "pages/ice/bookings/detail", + "style": { + "navigationBarTitleText": "预约详情" + } +}, +{ + "path": "pages/ice/courses/detail", + "style": { + "navigationBarTitleText": "课程详情" + } +}, +{ + "path": "pages/ice/profile/cards", + "style": { + "navigationBarTitleText": "我的会员卡", + "enablePullDownRefresh": true + } +}, +{ + "path": "pages/ice/profile/card-history", + "style": { + "navigationBarTitleText": "会员卡使用流水", + "enablePullDownRefresh": true + } +}, +{ + "path": "pages/ice/profile/detail", + "style": { + "navigationBarTitleText": "个人信息" + } +}, +{ + "path": "pages/ice/activities/detail", + "style": { + "navigationBarTitleText": "活动详情" + } +} +``` + +Expected: all ice pages from the old `pages.json` are registered with the `pages/ice` prefix. + +- [ ] **Step 7: Verify ice module references** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram' +rg -n "@/api|@/components/StatePanel.vue" src/pages/ice src/modules/ice +rg -n "'/pages/(home|courses|bookings|profile|activities)|\"/pages/(home|courses|bookings|profile|activities)" src/pages/ice +rg -n "VITE_API_BASE_URL" src/modules/ice +``` + +Expected: all three commands return no matches. + +- [ ] **Step 8: Build after ice migration** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram' +npm run build:mp-weixin +``` + +Expected: build succeeds. If build fails, fix the reported import, route registration, or SCSS variable error before continuing. + +- [ ] **Step 9: Commit ice module** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序' +git add miniprogram/src/modules/ice miniprogram/src/pages/ice miniprogram/src/components/ice miniprogram/src/static/ice miniprogram/src/pages.json +git commit -m "feat: migrate ice venue module" +``` + +Expected: commit succeeds with ice module files. + +### Task 5: Finalize Cross-Module Navigation and Safety Checks + +**Files:** +- Modify: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/pages/venue-select/index.vue` +- Modify: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/src/utils/venue.js` +- Modify: module pages only if checks reveal stale routes or stale imports. + +- [ ] **Step 1: Ensure venue selector can return to stored current venue** + +Update `miniprogram/src/pages/venue-select/index.vue` to keep the existing card UI and add a continue action when a venue is already selected: + +```vue + +``` + +Add script code: + +```js +const selectedVenue = computed(() => venueStore.currentVenueOption) + +const continueCurrentVenue = () => { + if (!venueStore.currentVenue) return + venueStore.enterVenue(venueStore.currentVenue) +} +``` + +Add styles: + +```scss +.current-card { + display: flex; + flex-direction: column; + gap: 12rpx; + margin-bottom: 32rpx; + padding: 28rpx; + border-radius: $border-radius-lg; + background: $bg-color-main; + border: 1rpx solid $border-color; +} + +.current-label { + color: $text-color-muted; + font-size: 22rpx; + font-weight: 700; +} + +.current-name { + color: $text-color-main; + font-size: 32rpx; + font-weight: 900; +} + +.continue-btn { + margin-top: 12rpx; +} +``` + +Expected: users who return to the selector can continue into the stored venue without changing it. + +- [ ] **Step 2: Run stale import checks** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram' +rg -n "@/api|@/stores/session|@/utils/(date|money|status)|@/components/StatePanel.vue" src/pages src/modules +``` + +Expected: no matches. If matches appear, rewrite them to module-specific imports: + +```text +@/modules/ice/api/ +@/modules/comprehensive/api/ +@/modules/comprehensive/utils/ +@/components/ice/StatePanel.vue +``` + +- [ ] **Step 3: Run stale route checks** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram' +rg -n "'/pages/(home|courses|bookings|profile|activities|private|tickets|orders|pay)|\"/pages/(home|courses|bookings|profile|activities|private|tickets|orders|pay)|`/pages/(home|courses|bookings|profile|activities|private|tickets|orders|pay)" src/pages +``` + +Expected: no matches. If matches appear, prefix with `ice` or `comprehensive` based on the file path. + +- [ ] **Step 4: Run API base URL checks** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram' +rg -n "VITE_API_BASE_URL|const API_BASE_URL" src/modules +``` + +Expected output includes only: + +```text +src/modules/ice/api/request.js:const API_BASE_URL = import.meta.env.VITE_ICE_API_BASE_URL || 'http://127.0.0.1:8000' +src/modules/comprehensive/api/request.js:const API_BASE_URL = import.meta.env.VITE_COMPREHENSIVE_API_BASE_URL || 'http://127.0.0.1:8001' +``` + +- [ ] **Step 5: Run token isolation checks** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram' +rg -n "ICE_MEMBER_TOKEN|ICE_MEMBER_TOKEN_EXPIRES_AT|COMPREHENSIVE_VENUE_TOKEN" src/modules +``` + +Expected output includes: + +```text +src/modules/ice/api/request.js:const KEY_TOKEN = 'ICE_MEMBER_TOKEN' +src/modules/ice/api/request.js:const KEY_TOKEN_EXPIRES_AT = 'ICE_MEMBER_TOKEN_EXPIRES_AT' +src/modules/comprehensive/api/request.js:const KEY_TOKEN = 'COMPREHENSIVE_VENUE_TOKEN' +``` + +- [ ] **Step 6: Build final merged app** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram' +npm run build:mp-weixin +``` + +Expected: build succeeds and outputs `dist/build/mp-weixin`. + +- [ ] **Step 7: Commit safety pass** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序' +git add miniprogram +git commit -m "fix: finalize merged venue navigation" +``` + +Expected: commit succeeds if files changed. If no files changed after checks, skip this commit and record that no safety-pass commit was needed. + +### Task 6: Manual WeChat Mini Program Smoke Test + +**Files:** +- No source files should be modified unless the smoke test finds a defect. + +- [ ] **Step 1: Open generated mini program in WeChat DevTools** + +Open this directory in WeChat DevTools: + +```text +/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram/dist/build/mp-weixin +``` + +Expected: project opens with appid `wx516b3e0d9940c788`. + +- [ ] **Step 2: Smoke test venue selector** + +In the simulator: + +```text +1. Open first page. +2. Confirm "选择场馆" appears. +3. Tap "冰场馆". +4. Confirm URL is pages/ice/home/index. +5. Navigate back to pages/venue-select/index. +6. Tap "综合场馆". +7. Confirm URL is pages/comprehensive/home/index. +``` + +Expected: both venue cards navigate to the correct module home page. + +- [ ] **Step 3: Smoke test ice module navigation** + +Open these paths in WeChat DevTools page path input: + +```text +pages/ice/home/index +pages/ice/courses/index +pages/ice/bookings/index +pages/ice/profile/index +pages/ice/profile/cards +``` + +Expected: each page loads without a JavaScript import error. Network errors from unavailable backend are acceptable during local smoke testing if request URLs point to `https://venue.test.chatgqt.top`. + +- [ ] **Step 4: Smoke test comprehensive module navigation** + +Open these paths in WeChat DevTools page path input: + +```text +pages/comprehensive/home/index +pages/comprehensive/courses/index +pages/comprehensive/bookings/index +pages/comprehensive/profile/index +pages/comprehensive/private/index +pages/comprehensive/tickets/index +pages/comprehensive/pay/index?order_id=1 +``` + +Expected: each page loads without a JavaScript import error. The pay page may show an order loading error for `order_id=1`; it must not call the ice backend. + +- [ ] **Step 5: Inspect request domains** + +Use WeChat DevTools network panel: + +```text +1. Open pages/ice/home/index. +2. Confirm requests go to https://venue.test.chatgqt.top. +3. Open pages/comprehensive/home/index. +4. Confirm requests go to https://venue2.test.chatgqt.top. +``` + +Expected: requests never cross backend domains. + +- [ ] **Step 6: Commit smoke-test fixes** + +If source fixes were required, run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序' +git add miniprogram +git commit -m "fix: address merged miniapp smoke test issues" +``` + +Expected: commit succeeds only if smoke testing found and fixed defects. + +### Task 7: Final Verification and Delivery Notes + +**Files:** +- Create: `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/docs/merged-miniapp-verification.md` + +- [ ] **Step 1: Run final command verification** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram' +npm run build:mp-weixin +rg -n "@/api|@/stores/session|@/utils/(date|money|status)|@/components/StatePanel.vue" src/pages src/modules +rg -n "'/pages/(home|courses|bookings|profile|activities|private|tickets|orders|pay)|\"/pages/(home|courses|bookings|profile|activities|private|tickets|orders|pay)|`/pages/(home|courses|bookings|profile|activities|private|tickets|orders|pay)" src/pages +rg -n "VITE_API_BASE_URL" src/modules .env .env.example vite.config.js +``` + +Expected: + +```text +npm run build:mp-weixin exits 0 +stale import rg exits 1 with no matches +stale route rg exits 1 with no matches +VITE_API_BASE_URL rg exits 1 with no matches +``` + +- [ ] **Step 2: Write verification note** + +Create `docs/merged-miniapp-verification.md`: + +```markdown +# 合并版场馆小程序验证记录 + +## 构建 + +- 命令:`npm run build:mp-weixin` +- 结果:通过 +- 产物:`miniprogram/dist/build/mp-weixin` + +## 路由隔离 + +- 检查旧的无前缀页面路径:通过 +- 冰场馆页面前缀:`pages/ice` +- 综合场馆页面前缀:`pages/comprehensive` + +## API 隔离 + +- 冰场馆 baseURL:`VITE_ICE_API_BASE_URL` +- 综合场馆 baseURL:`VITE_COMPREHENSIVE_API_BASE_URL` +- 旧 `VITE_API_BASE_URL`:未使用 + +## 登录态隔离 + +- 冰场馆 token:`ICE_MEMBER_TOKEN` +- 冰场馆 token 过期时间:`ICE_MEMBER_TOKEN_EXPIRES_AT` +- 综合场馆 token:`COMPREHENSIVE_VENUE_TOKEN` + +## 上线前人工检查 + +- 微信 request 合法域名包含 `venue.test.chatgqt.top` +- 微信 request 合法域名包含 `venue2.test.chatgqt.top` +- 综合场馆支付仍由综合场馆后端生成支付参数 +``` + +If any command fails, replace `结果:通过` with the exact failing command and actual status, then fix the issue before final delivery. + +- [ ] **Step 3: Commit verification note** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序' +git add docs/merged-miniapp-verification.md +git commit -m "docs: record merged miniapp verification" +``` + +Expected: commit succeeds with the verification record. + +- [ ] **Step 4: Final git status check** + +Run: + +```bash +cd '/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序' +git status --short +``` + +Expected: no uncommitted files except intentionally ignored build output if `.gitignore` is added later. If build output appears and should not be committed, add a project `.gitignore` that ignores: + +```gitignore +miniprogram/node_modules/ +miniprogram/dist/ +.DS_Store +``` + +Commit `.gitignore` with: + +```bash +git add .gitignore +git commit -m "chore: ignore generated miniapp files" +``` + +Expected: working tree is clean or only contains intentionally untracked files reported to the user. + +## Plan Self-Review + +Spec coverage: + +- New project under `场馆小程序/miniprogram`: Task 1. +- First-entry venue choice and in-app switching: Tasks 2, 5, and 6. +- Isolated Django backends and API clients: Tasks 1, 3, 4, and 5. +- Token isolation: Tasks 3, 4, 5, and 7. +- Route prefixing: Tasks 3, 4, and 5. +- No backend/admin/CI merge: all tasks operate only under `场馆小程序`. +- Build and smoke verification: Tasks 5, 6, and 7. + +Placeholder scan: the plan intentionally avoids unspecified implementation slots. Each migration step includes exact copy commands, rewrite commands, file paths, expected checks, and commit commands. + +Type consistency: + +- Venue key values are `ice` and `comprehensive` across `VENUE_KEYS`, storage, routes, and page prefixes. +- Store exports are `useVenueStore` and `useComprehensiveSessionStore`. +- Environment variables are `VITE_ICE_API_BASE_URL` and `VITE_COMPREHENSIVE_API_BASE_URL`. +- Token keys are `ICE_MEMBER_TOKEN`, `ICE_MEMBER_TOKEN_EXPIRES_AT`, and `COMPREHENSIVE_VENUE_TOKEN`. diff --git a/miniprogram/docs/superpowers/specs/2026-06-08-merged-venue-miniapp-design.md b/miniprogram/docs/superpowers/specs/2026-06-08-merged-venue-miniapp-design.md new file mode 100644 index 0000000..4f9d3a6 --- /dev/null +++ b/miniprogram/docs/superpowers/specs/2026-06-08-merged-venue-miniapp-design.md @@ -0,0 +1,412 @@ +# 合并版场馆小程序设计 + +## 背景 + +当前有两套独立系统: + +- 冰场馆小程序:前端为 uni-app 微信小程序,后端为 Django,部署在 `https://venue.test.chatgqt.top/`。 +- 综合场馆小程序:前端为 uni-app 微信小程序,后端为 Django,部署在 `https://venue2.test.chatgqt.top/`。 + +两个项目的后端、数据库、Django admin、CI/CD 都保持独立。本次只合并小程序端,在 `/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序` 下新增合并版小程序项目。 + +两套旧小程序当前都配置同一个微信小程序 `appid`:`wx516b3e0d9940c788`。合并版继续沿用该 `appid`,不引入新的微信小程序身份。 + +## 目标 + +构建一个合并版 uni-app 微信小程序: + +- 用户首次进入时选择“冰场馆”或“综合场馆”。 +- 小程序内可以切换当前场馆。 +- 两个场馆的业务页面、登录态、API 请求和后端数据完全隔离。 +- 不合并、不改造两个 Django 后端、数据库、Django admin 和后端 CI/CD。 +- 新项目能独立安装依赖并构建微信小程序产物。 + +## 非目标 + +- 不统一两个后端接口协议。 +- 不迁移或合并会员数据、订单数据、课程数据、会员卡数据。 +- 不改造 Django admin。 +- 不新增统一后台或网关服务。 +- 不强制保留旧小程序的底部四个 Tab 结构。 +- 不重做业务页面视觉设计,只做必要的入口、切换和隔离改造。 + +## 推荐方案 + +采用“模块隔离合并”方案。 + +新项目保留两个业务模块: + +- `ice`:冰场馆模块。 +- `comprehensive`:综合场馆模块。 + +每个模块保留自己的页面、API、状态管理、业务工具和资源。公共层只负责场馆选择、当前场馆持久化、跨模块跳转和基础应用配置。 + +该方案的核心取舍是:牺牲一部分目录重复,换取后端隔离、登录态隔离、支付隔离和后续维护清晰度。 + +## 工程结构 + +新项目目录: + +```text +/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/ + miniprogram/ + package.json + vite.config.js + .env + .env.example + src/ + App.vue + main.js + manifest.json + pages.json + uni.scss + pages/ + venue-select/ + index.vue + ice/ + home/ + courses/ + bookings/ + profile/ + activities/ + comprehensive/ + home/ + courses/ + bookings/ + profile/ + activities/ + private/ + tickets/ + orders/ + pay/ + modules/ + ice/ + api/ + stores/ + utils/ + comprehensive/ + api/ + stores/ + utils/ + components/ + stores/ + venue.js + utils/ + venue.js + styles/ + variables.scss + static/ + ice/ + comprehensive/ +``` + +`pages` 放小程序实际路由页面,`modules` 放模块专属 API、状态和工具。页面通过模块专属路径导入业务能力,避免复用同名全局 API。 + +## 路由设计 + +`pages.json` 的第一个页面为: + +```text +pages/venue-select/index +``` + +旧页面迁移时统一加场馆前缀: + +```text +pages/ice/home/index +pages/ice/courses/index +pages/ice/courses/detail +pages/ice/bookings/index +pages/ice/bookings/detail +pages/ice/profile/index +pages/ice/profile/cards +pages/ice/profile/card-history +pages/ice/profile/detail +pages/ice/activities/detail + +pages/comprehensive/home/index +pages/comprehensive/courses/index +pages/comprehensive/courses/detail +pages/comprehensive/courses/confirm +pages/comprehensive/bookings/index +pages/comprehensive/bookings/course-detail +pages/comprehensive/bookings/private-detail +pages/comprehensive/profile/index +pages/comprehensive/profile/detail +pages/comprehensive/profile/orders +pages/comprehensive/profile/tickets +pages/comprehensive/profile/balance +pages/comprehensive/activities/index +pages/comprehensive/activities/detail +pages/comprehensive/private/index +pages/comprehensive/private/confirm +pages/comprehensive/tickets/index +pages/comprehensive/orders/detail +pages/comprehensive/pay/index +``` + +页面内所有 `uni.navigateTo`、`uni.redirectTo`、`uni.reLaunch`、`uni.switchTab` 目标都改为带模块前缀的路径。 + +由于用户没有要求保留旧 Tab,合并版可以先不配置全局 `tabBar`。模块内通过页面入口和按钮跳转完成导航。这样能避免一个小程序只能有一套全局 tabBar 带来的耦合。 + +## 场馆选择与切换 + +公共场馆状态使用 Pinia store 和本地存储: + +```text +CURRENT_VENUE_KEY = "CURRENT_VENUE" +``` + +可选值: + +```text +ice +comprehensive +``` + +流程: + +1. 用户首次进入 `pages/venue-select/index`。 +2. 如果本地已有 `CURRENT_VENUE`,页面可以展示当前场馆并提供继续进入或切换。 +3. 用户选择场馆后,写入 `CURRENT_VENUE`。 +4. 根据场馆跳转到对应首页: + - 冰场馆:`/pages/ice/home/index` + - 综合场馆:`/pages/comprehensive/home/index` +5. 小程序内的切换入口跳回 `pages/venue-select/index` 或直接调用公共切换方法。 +6. 切换当前场馆不清除另一套 token,用户切回原场馆后保持该后端的登录态。 + +建议在两个模块的“我的/会员中心”页面添加“切换场馆”入口,跳转到 `/pages/venue-select/index`。 + +## API 与环境变量 + +两个后端使用独立 baseURL: + +```text +VITE_ICE_API_BASE_URL=https://venue.test.chatgqt.top +VITE_COMPREHENSIVE_API_BASE_URL=https://venue2.test.chatgqt.top +``` + +冰场馆请求封装放在: + +```text +src/modules/ice/api/request.js +``` + +综合场馆请求封装放在: + +```text +src/modules/comprehensive/api/request.js +``` + +两个模块都可以继续使用后端现有响应协议 `{ code, data, message }`,但错误归一化逻辑不共用。这样可以保留冰场馆对 `40002` 资料完善、`40003` 绑定冲突等错误码的处理,也保留综合场馆上传、支付、订单相关的现有处理。 + +Vite H5 开发代理可配置两个前缀: + +```text +/ice-api -> VITE_ICE_API_BASE_URL +/comprehensive-api -> VITE_COMPREHENSIVE_API_BASE_URL +``` + +微信小程序构建时直接使用完整后端 URL。 + +## 登录态隔离 + +冰场馆保留: + +```text +ICE_MEMBER_TOKEN +ICE_MEMBER_TOKEN_EXPIRES_AT +``` + +综合场馆保留: + +```text +COMPREHENSIVE_VENUE_TOKEN +``` + +两个模块各自调用 `uni.login({ provider: "weixin" })`。登录得到的 code 只提交给当前模块对应后端: + +- 冰场馆:`https://venue.test.chatgqt.top/api/auth/wechat-login` +- 综合场馆:`https://venue2.test.chatgqt.top/api/auth/wechat-login` + +因为合并版沿用旧项目共同的 `appid`,不需要为新 `appid` 改两个后端微信登录配置。 + +## 支付隔离 + +综合场馆的 `pages/comprehensive/pay/index` 继续只使用综合场馆模块 API。微信支付参数由综合场馆后端生成,前端只调用 `uni.requestPayment`。 + +冰场馆模块不复用综合场馆订单和支付 API。任何支付相关跳转都必须保持在 `pages/comprehensive/*` 路由内。 + +## 样式与资源 + +全局 `styles/variables.scss` 采用两套项目共有变量,并以综合场馆版本为基础补齐冰场馆需要的变量和 mixin: + +- `$bg-color-main` +- `$bg-color-soft` +- `$bg-color-hover` +- `$bg-color-panel` +- `$text-color-*` +- `$color-primary` +- `$color-success` +- `$color-warning` +- `$color-danger` +- `$border-*` +- `$spacing-*` +- `minimal-card` +- `text-ellipsis` + +全局 `App.vue` 保留基础页面样式、按钮 reset、容器类、状态 badge、固定底部操作栏等公共规则。页面级样式随页面复制,减少重写成本。 + +静态资源按模块隔离: + +```text +src/static/ice/tab/* +src/static/comprehensive/tab/* +``` + +如果不配置全局 tabBar,旧 tab 图标只作为后续模块内导航备用资源。 + +## 文件迁移策略 + +复制时排除: + +- `node_modules` +- `dist` +- `.git` +- `.idea` +- `.DS_Store` +- 后端目录 +- 旧项目 `.env` 中的临时 frp 地址 + +优先以综合场馆小程序为工程基线,因为它已有 Pinia store、上传、支付、订单等更完整能力。冰场馆页面和 API 迁入独立模块。 + +所有旧的 `@/api/` 导入都改为模块路径: + +```text +@/modules/ice/api/ +@/modules/comprehensive/api/ +``` + +综合场馆 `defineStore("session")` 改为专属 ID: + +```text +defineStore("comprehensiveVenueSession") +``` + +如果冰场馆新增 store,也使用 `iceVenueSession`、`iceVenueProfile` 这类 `iceVenue` 前缀。 + +## 错误处理 + +场馆选择页: + +- 本地值不是 `ice` 或 `comprehensive` 时视为未选择。 +- 跳转失败时展示 toast,并停留在选择页。 + +API 请求: + +- 401 或后端登录过期错误只清理当前模块 token。 +- 网络错误只影响当前模块页面。 +- 切换场馆不触发任何后端请求,避免误清理另一模块登录态。 + +登录: + +- `uni.login` 未返回 code 时提示用户重试。 +- 后端返回绑定冲突、资料未完善等错误时沿用模块现有页面状态处理。 + +支付: + +- 支付失败或取消只影响综合场馆订单页面。 +- 不新增跨场馆支付状态。 + +## 测试与验收 + +基础验证: + +- 在 `场馆小程序/miniprogram` 执行 `npm install`。 +- 执行 `npm run build:mp-weixin`,确认能生成微信小程序产物。 + +路由验证: + +- 首次进入展示场馆选择页。 +- 选择冰场馆后进入 `/pages/ice/home/index`。 +- 选择综合场馆后进入 `/pages/comprehensive/home/index`。 +- 两个模块内主要页面跳转不再引用旧的 `/pages/home/index` 等无前缀路径。 + +API 验证: + +- 冰场馆页面请求使用 `VITE_ICE_API_BASE_URL`。 +- 综合场馆页面请求使用 `VITE_COMPREHENSIVE_API_BASE_URL`。 +- 冰场馆 token 不会出现在综合场馆请求头中。 +- 综合场馆 token 不会出现在冰场馆请求头中。 + +登录验证: + +- 冰场馆登录后写入 `ICE_MEMBER_TOKEN`。 +- 综合场馆登录后写入 `COMPREHENSIVE_VENUE_TOKEN`。 +- 切换场馆不会删除另一场馆 token。 + +支付验证: + +- 综合场馆支付页仍只调用综合场馆订单和支付 API。 +- 冰场馆页面没有误跳到综合支付页。 + +微信后台上线前检查: + +- 微信小程序 request 合法域名包含 `venue.test.chatgqt.top`。 +- 微信小程序 request 合法域名包含 `venue2.test.chatgqt.top`。 +- 如综合场馆支付上线,需要确认微信支付商户、回调和综合场馆后端配置保持现状。 + +## 难度 + +整体难度为中等偏上。 + +主要工作量不是新增业务,而是批量迁移: + +- 页面路径加模块前缀。 +- 页面内跳转路径同步修改。 +- API import 路径改为模块专属路径。 +- 两套样式和公共组件合并。 +- 验证两个模块的登录态、请求、支付不会串线。 + +## 风险与缓解 + +路由漏改: + +- 使用全文搜索检查旧路径:`/pages/home`、`/pages/courses`、`/pages/bookings`、`/pages/profile`、`/pages/pay`。 +- 构建后通过主要路径手动跳转验证。 + +API 串线: + +- 禁止使用全局 `src/api/request.js`。 +- 每个模块保留独立 `request.js` 和独立 env 变量。 +- 验证请求 URL 和 Authorization token。 + +登录态污染: + +- 保留并强化 token key 命名空间。 +- 401 只清理当前模块 token。 + +样式互相影响: + +- 公共样式只放基础 reset 和共用工具类。 +- 模块页面样式尽量保留在单文件组件内。 + +支付误接: + +- 支付页面只存在于 `pages/comprehensive/pay/index`。 +- 支付 API 只从 `src/modules/comprehensive/api/order.js` 导入。 + +微信后台配置遗漏: + +- 上线前将两个后端域名都加入合法域名。 +- 因沿用旧 `appid`,不新增后端微信登录配置变更。 + +## 交付结果 + +完成后应得到一个新的合并版小程序项目: + +```text +/Volumes/ExternalStorageA/project/2026.5/体育场馆/场馆小程序/miniprogram +``` + +它可以独立构建微信小程序。用户首次进入选择场馆,后续可在小程序内切换;两个场馆后端、登录态、支付和业务数据仍完全隔离。