feat: 支持新版的首页 UI

This commit is contained in:
richarjiang
2026-10-25 21:15:15 +08:00
parent 4ca32c22f9
commit d93cc921db
30 changed files with 827 additions and 223 deletions

View File

@@ -0,0 +1,79 @@
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 = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>首页设计验收</title>
<style>
*{box-sizing:border-box}body{margin:0;background:#f0eeea;color:#403c36;font-family:-apple-system,BlinkMacSystemFont,'PingFang SC',sans-serif}header{padding:26px 30px 20px;position:sticky;top:0;z-index:50;background:#f0eeea}h1{font-size:21px;margin:0 0 10px;font-weight:500}p{font-size:13px;color:#8b817b;margin:0 0 16px}.controls{display:flex;gap:18px;align-items:center;font-size:13px}select,button{border:1px solid #d7d2ca;background:#fbf9f6;color:#403c36;border-radius:8px;padding:8px 12px;font:inherit}main{display:flex;gap:32px;padding:0 30px 30px;align-items:flex-start;flex-wrap:wrap}.screen{background:#fbf9f6;border-radius:18px;overflow:hidden;box-shadow:0 4px 24px #403c3608;position:relative}.caption{font-size:14px;margin:0 0 12px;color:#756a60}.status{height:44px;display:flex;align-items:center;justify-content:space-between;padding:0 24px;font-size:15px;font-weight:600;background:#fbf9f6}.status span:last-child{font-size:12px;letter-spacing:2px}iframe{display:block;width:100%;height:800px;border:0}.capsule{position:absolute;top:55px;right:16px;border:1px solid #ded9d1;border-radius:20px;height:32px;width:94px;display:flex;align-items:center;justify-content:center;gap:14px;color:#292a27;pointer-events:none}.capsule b{font-size:16px}.capsule i{height:16px;border-left:1px solid #ded9d1}.capsule em{font-size:21px;font-style:normal;line-height:1}.home-indicator{position:absolute;bottom:6px;left:calc(50% - 65px);width:130px;height:5px;border-radius:8px;background:#171817;pointer-events:none}
</style></head><body><header><h1>首页顶部 · 轻盈陪伴</h1><p>实际 Vue 页面与真实交互 · 以下均为隔离的模拟数据,案例图片仅用于检查布局</p><div class="controls"><label>右侧状态 <select id="scenario"><option value="member">老学员</option><option value="new">已登录新用户</option><option value="expired">会员卡已过期</option><option value="empty">暂无预约和案例</option><option value="error">首次请求失败</option><option value="loading">慢网络</option><option value="long">长昵称和馆名</option></select></label><label>屏幕宽度 <select id="width"><option>390</option><option>320</option><option>375</option><option>430</option></select></label><button id="reload">重新进入</button></div></header><main><section><p class="caption">新朋友</p><div class="screen"><div class="status"><span>9:41</span><span>▮▮▮ ◉ ▰</span></div><iframe title="新用户首页" id="guest"></iframe><div class="capsule"><b>•••</b><i></i><em>⊙</em></div><div class="home-indicator"></div></div></section><section><p class="caption">老学员 / 边界状态</p><div class="screen"><div class="status"><span>9:41</span><span>▮▮▮ ◉ ▰</span></div><iframe title="老学员首页" id="member"></iframe><div class="capsule"><b>•••</b><i></i><em>⊙</em></div><div class="home-indicator"></div></div></section></main><script>
const scenario=document.getElementById('scenario'),width=document.getElementById('width');
function load(){document.querySelectorAll('.screen').forEach(screen=>screen.style.width=width.value+'px');document.getElementById('guest').src='/?homeFixture=guest&instance='+Math.random();document.getElementById('member').src='/?homeFixture='+scenario.value+'&instance='+Math.random()}
scenario.onchange=load;width.onchange=load;document.getElementById('reload').onclick=load;load();
</script></body></html>`