新朋友
老学员 / 边界状态
const cover = '/static/home/studio-morning.jpg' const membership = { id: 'preview-membership', userId: 'preview-user', cardTypeId: 'preview-card', remainingTimes: 8, totalTimes: 10, status: 'ACTIVE', startDate: '2026-10-01T00:00:00.000Z', expireDate: '2027-10-01T00:00:00.000Z', cardType: { id: 'preview-card', name: '私教课', type: 'TIMES', totalTimes: 10, price: 388000, validityDays: 365 }, } const booking = { id: 'preview-booking', membership, status: 'CONFIRMED', timeSlot: { id: 'preview-slot', date: '2026-10-26T00:00:00.000Z', startTime: '10:00:00', endTime: '11:00:00' }, } const story = { id: 'preview-story', displayName: '学员 A', title: '找到更舒展的自己', lessonCount: 12, achievement: '', background: '', frames: [{ kind: 'pair', beforeId: 'preview-before', afterId: 'preview-after', beforeUrl: cover, afterUrl: cover, caption: '' }], } export function createHomeFixture(scenario: string, path: string, count: number): { status?: number; data: unknown } { if (scenario === 'error' && ((path === '/membership/my' && count <= 2) || (path === '/booking/my/upcoming' && count === 1))) return { status: 503, data: null } const isNew = scenario === 'new' if (path === '/studio/info') return { data: { name: scenario === 'long' ? 'Focus Core 普拉提私教工作室' : 'Focus Core', logo: '', bannerUrl: null, ownerProfile: null } } if (path === '/user/profile') return { data: { id: 'preview-user', nickname: scenario === 'long' ? '这是一个特别特别长的学员昵称' : '晓晓', role: 'MEMBER', activeMembershipCount: isNew ? 0 : 1, inviteShareEligible: false, subscriptionMessageTemplates: {}, createdAt: '2026-01-01T00:00:00.000Z' } } if (path === '/membership/my') return { data: isNew ? [] : scenario === 'expired' ? [{ ...membership, status: 'EXPIRED', expireDate: '2026-10-01T00:00:00.000Z' }] : [membership] } if (path === '/booking/my/upcoming') return { data: ['new', 'empty', 'expired'].includes(scenario) ? [] : [booking] } if (path === '/teaching-cases') return { data: { items: scenario === 'empty' ? [] : [story], total: 1, page: 1, limit: 3 } } if (path.startsWith('/teaching-cases/')) return { data: story } if (path === '/auth/renew') return { data: { token: 'home-preview-token' } } if (path === '/invite/summary') return { data: { inviteCode: '', invitedCount: 0, rewards: [] } } if (path.includes('review')) return { data: { total: 0, averageRating: 0, items: [], distribution: {} } } if (path.includes('products')) return { data: { items: [], data: [], total: 0 } } if (path.includes('card-types')) return { data: [] } if (path.includes('/booking/preview-booking')) return { data: booking } return { data: [] } } export function fixtureBootstrap(scenario: string) { return `(() => { const scenario = ${JSON.stringify(scenario)}; const params = new URL(location.href).searchParams; const instance = params.get('instance') || String(Math.random()); const storage = new Map(); const api = { getItem: key => storage.has(String(key)) ? storage.get(String(key)) : null, setItem: (key, value) => storage.set(String(key), String(value)), removeItem: key => storage.delete(String(key)), clear: () => storage.clear(), key: index => Array.from(storage.keys())[index] || null, get length() { return storage.size; } }; if (scenario !== 'guest') { api.setItem('token', 'home-preview-token'); api.setItem('token_renewed_at', JSON.stringify({ type: 'number', data: 1792892460000 })); } Object.defineProperty(window, 'localStorage', { value: api }); const RealDate = Date; const previewTime = new RealDate('2026-10-25T01:41:00.000Z').getTime(); window.Date = class extends RealDate { constructor(...args) { super(...(args.length ? args : [previewTime])); } static now() { return previewTime; } }; const originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, rawUrl, ...args) { const url = new URL(rawUrl, location.href); if (url.hostname === 'focus.richarjiang.com' && url.pathname.startsWith('/api')) { rawUrl = '/__home-fixture-api?' + new URLSearchParams({ path: url.pathname.slice(4), scenario, instance }); } return originalOpen.call(this, method, rawUrl, ...args); }; })();` } export const previewPage = `
实际 Vue 页面与真实交互 · 以下均为隔离的模拟数据,案例图片仅用于检查布局
新朋友
老学员 / 边界状态