feat: implement full navigation and PRD P2-P7 pages

- U1: 我的 XXX 根据登录态分流(未登录→auth,已登录→role-library/creator-center)
- U2: 新增角色库页(P2),含角色卡片列表与空态
- U3: 新增角色详情页(P3),含付款态切换
- U4: 新增关于 Eternal AI 页(P5),含 FAQ 折叠
- U5: 重构创作者入驻页(P6)为微信联系引导
- U6: 人设蒸馏表单重新定位为创作者中心-角色编辑
- U7: 新增创作者管理中心(P7),含角色/收入/我的 三 tab
- U8: 新增底部 tabBar 导航(首页/蒸馏前任/我的)
- U9: 统一 showView 路由、history 返回、localStorage 状态持久化
This commit is contained in:
chiguyong 2026-06-20 18:18:39 +08:00
parent d9d6404218
commit 7725cf1f65
3 changed files with 1401 additions and 173 deletions

533
app.js
View File

@ -1,31 +1,351 @@
(() => {
'use strict';
const landing = document.getElementById('landing');
const creator = document.getElementById('creator');
const auth = document.getElementById('auth');
const distill = document.getElementById('distill');
// --- U9: Unified state management ---
const STORAGE_KEY = 'eternal_ai_state';
const defaultState = {
isLoggedIn: false,
isCreator: false,
account: null,
boundCreator: null,
libraryName: '我的 [XXX]',
creatorName: '',
roles: [],
income: { balance: 0, records: [] },
};
function loadState() {
try {
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
return { ...defaultState, ...saved };
} catch {
return { ...defaultState };
}
}
function saveState() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
const state = loadState();
// --- Mock data for role library (U2) ---
const mockRoles = [
{
id: 'role_001',
name: '云朵',
avatar: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20girl%20soft%20pastel%20portrait%20gentle%20smile&image_size=square',
desc: '温柔如云的女孩,总是轻声细语地陪伴你。',
price: 29.9,
status: 'running',
},
{
id: 'role_002',
name: '星河',
avatar: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20boy%20starry%20eyes%20cool%20portrait&image_size=square',
desc: '嘴硬心软的少年,嘴上不饶人却总在关键时刻出现。',
price: 39.9,
status: 'running',
},
{
id: 'role_003',
name: '月见',
avatar: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20girl%20moonlight%20mysterious%20elegant%20portrait&image_size=square',
desc: '神秘而优雅,像月光一样忽远忽近的存在。',
price: 19.9,
status: 'stopped',
},
];
const mockIncome = {
balance: 1280.50,
records: [
{ time: '2026-06-18 14:30', amount: 23.92, role: '云朵' },
{ time: '2026-06-15 09:12', amount: 31.92, role: '星河' },
{ time: '2026-06-10 20:05', amount: 15.92, role: '月见' },
],
};
// --- DOM references ---
const views = {
landing: document.getElementById('landing'),
auth: document.getElementById('auth'),
'role-library': document.getElementById('role-library'),
'role-detail': document.getElementById('role-detail'),
distill: document.getElementById('distill'),
about: document.getElementById('about'),
onboarding: document.getElementById('onboarding'),
'creator-center': document.getElementById('creator-center'),
creator: document.getElementById('creator'),
};
const form = document.getElementById('character-form');
const resultPanel = document.getElementById('result-panel');
const previewCode = document.querySelector('#preview-code code');
const systemPromptInput = document.getElementById('system-prompt');
const views = { landing, creator, auth, distill };
const steps = Array.from(document.querySelectorAll('.form-step'));
const dots = Array.from(document.querySelectorAll('.stepper__dot'));
const dots = Array.from(document.querySelectorAll('#creator .stepper__dot'));
let currentStep = 0;
let generatedSoul = '';
let generatedConfig = '';
let activePreview = 'soul';
let activeAuthTab = 'login';
let activeCenterTab = 'roles';
let currentRole = null;
let viewHistory = ['landing'];
function showView(name) {
// --- U9: Unified showView with history and tab-bar sync ---
function showView(name, trackHistory = true) {
Object.entries(views).forEach(([key, el]) => {
if (el) el.classList.toggle('active', key === name);
});
if (trackHistory && viewHistory[viewHistory.length - 1] !== name) {
viewHistory.push(name);
}
window.scrollTo({ top: 0, behavior: 'smooth' });
updateTabBar(name);
}
function goBack() {
if (viewHistory.length > 1) {
viewHistory.pop();
const prev = viewHistory[viewHistory.length - 1];
showView(prev, false);
} else {
showView('landing', false);
}
}
// --- U8: Tab bar ---
function updateTabBar(viewName) {
const tabMap = {
landing: 'tab-home',
distill: 'tab-distill',
'role-library': 'tab-mine',
'creator-center': 'tab-mine',
};
const activeTab = tabMap[viewName] || 'tab-home';
document.querySelectorAll('.tab-bar__item').forEach((item) => {
item.classList.toggle('active', item.dataset.tabAction === activeTab);
});
}
function handleTabAction(action) {
if (action === 'tab-home') {
showView('landing');
} else if (action === 'tab-distill') {
showView('distill');
} else if (action === 'tab-mine') {
if (!state.isLoggedIn) {
switchAuthTab('login');
showView('auth');
} else if (state.isCreator) {
showView('creator-center');
renderCreatorCenter();
} else {
renderRoleLibrary();
showView('role-library');
}
}
}
// --- U1: Landing card state ---
function updateLandingCard() {
const nameEl = document.getElementById('library-name');
const descEl = document.getElementById('characters-desc');
const btnEl = document.getElementById('characters-btn');
const tabMineLabel = document.getElementById('tab-mine-label');
if (state.isLoggedIn) {
nameEl.textContent = state.libraryName || '我的角色库';
if (state.isCreator) {
descEl.textContent = '管理你的角色和收入';
btnEl.textContent = '进入管理中心';
tabMineLabel.textContent = '管理';
} else {
descEl.textContent = state.boundCreator ? '查看你的专属角色' : '寻找你的专属创作者';
btnEl.textContent = '进入角色库';
tabMineLabel.textContent = '我的';
}
} else {
nameEl.textContent = '我的 [XXX]';
descEl.textContent = '登录后管理你的角色';
btnEl.textContent = '登录 / 注册';
tabMineLabel.textContent = '我的';
}
}
// --- Auth ---
function switchAuthTab(tab) {
activeAuthTab = tab;
document.querySelectorAll('.auth-tab').forEach((t) => {
t.classList.toggle('active', t.dataset.tab === tab);
});
document.querySelectorAll('.auth-form').forEach((f) => {
f.classList.toggle('active', f.dataset.form === tab);
});
}
function validatePasswordMatch(formEl) {
const pwd = formEl.querySelector('[name="password"]');
const confirm = formEl.querySelector('[name="confirmPassword"]');
if (!pwd || !confirm) return true;
if (pwd.value !== confirm.value) {
confirm.setCustomValidity('两次输入的密码不一致');
confirm.reportValidity();
return false;
}
confirm.setCustomValidity('');
return true;
}
function login(account) {
state.isLoggedIn = true;
state.account = account;
state.boundCreator = state.boundCreator || { name: '云朵', roles: mockRoles };
saveState();
updateLandingCard();
}
function logout() {
state.isLoggedIn = false;
state.isCreator = false;
state.account = null;
saveState();
updateLandingCard();
showView('landing');
}
// --- U2: Role Library ---
function renderRoleLibrary() {
const listEl = document.getElementById('role-list');
const emptyEl = document.getElementById('library-empty');
const titleEl = document.getElementById('library-title');
titleEl.textContent = state.libraryName || '我的角色库';
if (!state.boundCreator) {
listEl.innerHTML = '';
emptyEl.hidden = false;
return;
}
emptyEl.hidden = true;
listEl.innerHTML = mockRoles
.map(
(role) => `
<article class="role-card" data-role-id="${role.id}">
<img class="role-card__avatar" src="${role.avatar}" alt="${role.name}" />
<div class="role-card__info">
<h3 class="role-card__name">${role.name}</h3>
<p class="role-card__desc">${role.desc}</p>
<span class="role-card__price">¥${role.price}</span>
</div>
</article>`
)
.join('');
}
// --- U3: Role Detail ---
function renderRoleDetail(roleId) {
const role = mockRoles.find((r) => r.id === roleId);
if (!role) return;
currentRole = role;
document.getElementById('detail-name').textContent = role.name;
document.getElementById('detail-hero').style.backgroundImage = `url(${role.avatar})`;
document.getElementById('detail-role-name').textContent = role.name;
document.getElementById('detail-role-desc').textContent = role.desc;
document.getElementById('detail-price').innerHTML = `<span class="detail-price__value">¥${role.price}</span><span class="detail-price__unit">/ 月</span>`;
document.getElementById('detail-actions-pre').hidden = false;
document.getElementById('detail-paid').hidden = true;
}
function payRole() {
document.getElementById('detail-actions-pre').hidden = true;
const paidEl = document.getElementById('detail-paid');
paidEl.hidden = false;
document.getElementById('detail-qr').innerHTML = '<div class="qr-placeholder">扫码连接<br/>AI 角色</div>';
document.getElementById('detail-avatar').style.backgroundImage = `url(${currentRole.avatar})`;
}
// --- U4: About FAQ ---
function toggleFaq(button) {
const item = button.closest('.faq-item');
const answer = item.querySelector('.faq-a');
const icon = button.querySelector('.faq-icon');
const isOpen = answer.style.display === 'block';
answer.style.display = isOpen ? 'none' : 'block';
icon.textContent = isOpen ? '+' : '';
}
// --- U7: Creator Center ---
function renderCreatorCenter() {
renderCreatorRoles();
renderIncome();
renderSettings();
}
function renderCreatorRoles() {
const listEl = document.getElementById('creator-role-list');
const roles = state.roles.length > 0 ? state.roles : mockRoles;
listEl.innerHTML = roles
.map(
(role) => `
<article class="role-card" data-role-id="${role.id}">
<img class="role-card__avatar" src="${role.avatar}" alt="${role.name}" />
<div class="role-card__info">
<h3 class="role-card__name">${role.name}</h3>
<span class="role-card__status role-card__status--${role.status}">${role.status === 'running' ? '运行中' : '已停止'}</span>
</div>
<button class="btn btn--small btn--outline" type="button" data-action="edit-role" data-role-id="${role.id}">编辑</button>
</article>`
)
.join('');
}
function renderIncome() {
const income = state.income.balance > 0 ? state.income : mockIncome;
document.getElementById('income-balance').textContent = `¥ ${income.balance.toFixed(2)}`;
const listEl = document.getElementById('income-list');
if (income.records.length === 0) {
listEl.innerHTML = '<p class="income-empty">暂无流水记录</p>';
return;
}
listEl.innerHTML = income.records
.map(
(r) => `
<div class="income-record">
<div class="income-record__info">
<span class="income-record__role">${r.role}</span>
<span class="income-record__time">${r.time}</span>
</div>
<span class="income-record__amount">+¥${r.amount.toFixed(2)}</span>
</div>`
)
.join('');
}
function renderSettings() {
document.getElementById('settings-name').value = state.creatorName || '';
document.getElementById('settings-library').value = state.libraryName === '我的 [XXX]' ? '' : state.libraryName;
}
function switchCenterTab(tab) {
activeCenterTab = tab;
document.querySelectorAll('.center-tab').forEach((t) => {
t.classList.toggle('active', t.dataset.centerTab === tab);
});
document.querySelectorAll('.center-panel').forEach((p) => {
p.classList.toggle('active', p.id === `center-${tab}`);
});
const labels = { roles: '我的角色', income: '收入', settings: '我的' };
document.getElementById('center-tab-label').textContent = labels[tab] || '我的角色';
}
// --- Creator form (U6: preserved from original) ---
function updateStep(index) {
steps.forEach((step, i) => {
step.classList.toggle('active', i === index);
@ -255,47 +575,59 @@ ${data.secrets || '无'}
});
}
function switchAuthTab(tab) {
activeAuthTab = tab;
document.querySelectorAll('.auth-tab').forEach((t) => {
t.classList.toggle('active', t.dataset.tab === tab);
});
document.querySelectorAll('.auth-form').forEach((f) => {
f.classList.toggle('active', f.dataset.form === tab);
});
}
function validatePasswordMatch(formEl) {
const pwd = formEl.querySelector('[name="password"]');
const confirm = formEl.querySelector('[name="confirmPassword"]');
if (!pwd || !confirm) return true;
if (pwd.value !== confirm.value) {
confirm.setCustomValidity('两次输入的密码不一致');
confirm.reportValidity();
return false;
}
confirm.setCustomValidity('');
return true;
}
// Event delegation
// --- Event delegation ---
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-action], [data-tab], [data-download]');
// FAQ toggle (U4)
const faqBtn = e.target.closest('.faq-q');
if (faqBtn) {
e.preventDefault();
toggleFaq(faqBtn);
return;
}
// Role card click (U2/U3)
const roleCard = e.target.closest('.role-card');
if (roleCard && !e.target.closest('[data-action]')) {
e.preventDefault();
const roleId = roleCard.dataset.roleId;
renderRoleDetail(roleId);
showView('role-detail');
return;
}
// Tab bar (U8)
const tabItem = e.target.closest('[data-tab-action]');
if (tabItem) {
e.preventDefault();
handleTabAction(tabItem.dataset.tabAction);
return;
}
const target = e.target.closest('[data-action], [data-tab], [data-download], [data-center-tab]');
if (!target) return;
const action = target.dataset.action;
if (action === 'open-creator') {
// Center tab switching (U7)
if (target.dataset.centerTab) {
e.preventDefault();
resetCreator();
showView('creator');
switchCenterTab(target.dataset.centerTab);
return;
}
if (action === 'open-auth') {
// Landing card: open characters (U1)
if (action === 'open-characters') {
e.preventDefault();
if (!state.isLoggedIn) {
switchAuthTab('login');
showView('auth');
} else if (state.isCreator) {
showView('creator-center');
renderCreatorCenter();
} else {
renderRoleLibrary();
showView('role-library');
}
return;
}
@ -305,12 +637,86 @@ ${data.secrets || '无'}
return;
}
if (action === 'back') {
if (action === 'open-about') {
e.preventDefault();
showView('landing');
showView('about');
return;
}
if (action === 'open-onboarding') {
e.preventDefault();
showView('onboarding');
return;
}
if (action === 'back') {
e.preventDefault();
goBack();
return;
}
if (action === 'back-to-library') {
e.preventDefault();
renderRoleLibrary();
showView('role-library');
return;
}
if (action === 'back-to-center') {
e.preventDefault();
showView('creator-center');
renderCreatorCenter();
return;
}
if (action === 'pay') {
e.preventDefault();
payRole();
return;
}
if (action === 'pay-distill') {
e.preventDefault();
alert('下单成功!请添加客服微信完成后续流程。');
return;
}
if (action === 'contact-wechat') {
e.preventDefault();
alert('客服微信号EternalAI_Service');
return;
}
if (action === 'new-role') {
e.preventDefault();
resetCreator();
showView('creator');
return;
}
if (action === 'edit-role') {
e.preventDefault();
resetCreator();
showView('creator');
return;
}
if (action === 'logout') {
e.preventDefault();
logout();
return;
}
if (action === 'download-avatar') {
e.preventDefault();
if (currentRole) {
download(currentRole.name + '_avatar.png', '');
window.open(currentRole.avatar, '_blank');
}
return;
}
// Creator form navigation
if (action === 'next') {
e.preventDefault();
if (validateStep(currentStep) && currentStep < steps.length - 1) {
@ -339,6 +745,7 @@ ${data.secrets || '无'}
return;
}
// Tab switching (auth tabs and preview tabs)
if (target.dataset.tab) {
e.preventDefault();
if (target.closest('.auth-tabs')) {
@ -351,6 +758,7 @@ ${data.secrets || '无'}
return;
}
// Download buttons
if (target.dataset.download) {
e.preventDefault();
if (target.dataset.download === 'soul') {
@ -362,25 +770,60 @@ ${data.secrets || '无'}
}
});
// Auth form submissions
// --- Auth form submissions ---
document.querySelectorAll('.auth-form').forEach((authForm) => {
authForm.addEventListener('submit', (e) => {
e.preventDefault();
if (!validatePasswordMatch(authForm)) return;
const formData = new FormData(authForm);
const data = Object.fromEntries(formData.entries());
const action = authForm.dataset.form === 'login' ? '登录' : '注册';
alert(`${action}成功:${data.account}`);
showView('landing');
login(data.account);
// After login, go to role library or creator center
if (state.isCreator) {
showView('creator-center');
renderCreatorCenter();
} else {
renderRoleLibrary();
showView('role-library');
}
});
});
// Update auto-generated system prompt as user types
// --- Settings form (U7) ---
document.getElementById('settings-form').addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData.entries());
state.creatorName = data.creatorName || '';
state.libraryName = data.libraryName || '我的 [XXX]';
saveState();
updateLandingCard();
alert('设置已保存');
});
// --- Withdraw form (U7) ---
document.getElementById('withdraw-form').addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData.entries());
const amount = parseFloat(data.amount);
if (amount > state.income.balance && amount > mockIncome.balance) {
alert('提现金额超过可提现余额');
return;
}
alert(`提现申请已提交:${data.method === 'wechat' ? '微信' : '支付宝'} ¥${amount.toFixed(2)}\n平台负责人将手动审核转账。`);
e.target.reset();
});
// --- Creator form input ---
form.addEventListener('input', () => {
updateSystemPromptPreview();
});
// Initial state
// --- Initialize ---
updateStep(0);
updateSystemPromptPreview();
updateLandingCard();
updateTabBar('landing');
})();

View File

@ -7,19 +7,19 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="styles.css?v=5" />
<link rel="stylesheet" href="styles.css?v=6" />
</head>
<body>
<main class="app">
<!-- Landing View -->
<!-- P1: Landing View -->
<section id="landing" class="view view--landing active">
<div class="cards">
<article class="card card--characters" data-action="open-auth">
<article class="card card--characters" data-action="open-characters">
<div class="card__frame" aria-hidden="true"></div>
<div class="card__content">
<h2 class="card__title">我的 [XXX ]</h2>
<p class="card__desc">登录后管理你的角色</p>
<button class="btn btn--outline" type="button">登录 / 注册</button>
<h2 class="card__title" id="library-name">我的 [XXX]</h2>
<p class="card__desc" id="characters-desc">登录后管理你的角色</p>
<button class="btn btn--outline" type="button" id="characters-btn">登录 / 注册</button>
</div>
</article>
@ -37,18 +37,368 @@
</div>
<footer class="landing-footer">
<a href="#about" class="footer-link">关于 Eternal AI →</a>
<a href="#creator" class="footer-link" data-action="open-creator">我是创作者,申请入驻 →</a>
<a href="#" class="footer-link" data-action="open-about">关于 Eternal AI →</a>
<a href="#" class="footer-link" data-action="open-onboarding">我是创作者,申请入驻 →</a>
</footer>
</section>
<!-- Creator View -->
<section id="creator" class="view view--creator">
<!-- Auth View (Login / Register) -->
<section id="auth" class="view view--auth">
<header class="creator-header">
<button class="icon-btn" type="button" data-action="back" aria-label="返回"></button>
<div class="creator-brand">
<span class="creator-brand__name">Eternal AI</span>
<span class="creator-brand__step">人设蒸馏器</span>
<span class="creator-brand__step">登录 / 注册</span>
</div>
<div class="stepper" aria-hidden="true"></div>
</header>
<div class="auth-tabs">
<button class="auth-tab active" type="button" data-tab="login">登录</button>
<button class="auth-tab" type="button" data-tab="register">注册</button>
</div>
<form id="login-form" class="auth-form active" autocomplete="off" data-form="login">
<div class="field-group">
<label class="field">
<span class="field__label">手机号 / 用户名</span>
<input class="field__input" name="account" type="text" placeholder="请输入手机号或用户名" required />
</label>
<label class="field">
<span class="field__label">密码</span>
<input class="field__input" name="password" type="password" placeholder="请输入密码" required />
</label>
</div>
<div class="form-actions">
<button class="btn btn--primary btn--block" type="submit">登录</button>
</div>
<p class="auth-hint">登录后可查看角色库、管理已订阅的角色。</p>
</form>
<form id="register-form" class="auth-form" autocomplete="off" data-form="register">
<div class="field-group">
<label class="field">
<span class="field__label">手机号 / 用户名</span>
<input class="field__input" name="account" type="text" placeholder="设置登录账号" required />
</label>
<label class="field">
<span class="field__label">密码</span>
<input class="field__input" name="password" type="password" placeholder="设置密码" required minlength="6" />
</label>
<label class="field">
<span class="field__label">确认密码</span>
<input class="field__input" name="confirmPassword" type="password" placeholder="再次输入密码" required />
</label>
</div>
<div class="form-actions">
<button class="btn btn--primary btn--block" type="submit">注册</button>
</div>
<p class="auth-hint">注册即代表同意《用户协议》和《隐私政策》。</p>
</form>
</section>
<!-- P2: Role Library View -->
<section id="role-library" class="view view--role-library">
<header class="creator-header">
<button class="icon-btn" type="button" data-action="back" aria-label="返回"></button>
<div class="creator-brand">
<span class="creator-brand__name" id="library-title">我的角色库</span>
<span class="creator-brand__step">选择一个角色开始对话</span>
</div>
<div class="stepper" aria-hidden="true"></div>
</header>
<div class="role-list" id="role-list"></div>
<div class="empty-state" id="library-empty" hidden>
<p class="empty-state__text">你还没有绑定专属创作者</p>
<p class="empty-state__hint">通过创作者的专属链接进入即可绑定</p>
</div>
</section>
<!-- P3: Role Detail View -->
<section id="role-detail" class="view view--role-detail">
<header class="creator-header">
<button class="icon-btn" type="button" data-action="back-to-library" aria-label="返回"></button>
<div class="creator-brand">
<span class="creator-brand__name" id="detail-name">角色详情</span>
<span class="creator-brand__step">了解 ta 并订阅</span>
</div>
<div class="stepper" aria-hidden="true"></div>
</header>
<div class="detail-page" id="detail-page">
<div class="detail-hero" id="detail-hero"></div>
<h2 class="detail-title" id="detail-role-name"></h2>
<p class="detail-desc" id="detail-role-desc"></p>
<div class="detail-price" id="detail-price"></div>
<div class="form-actions" id="detail-actions-pre">
<button class="btn btn--primary btn--block" type="button" data-action="pay">立即订阅</button>
</div>
<div class="detail-paid" id="detail-paid" hidden>
<div class="detail-qr" id="detail-qr"></div>
<p class="detail-paid__hint">扫码添加后,请将下方头像保存并设置为该联系人的备注头像,获得更完整的体验</p>
<div class="detail-avatar" id="detail-avatar"></div>
<button class="btn btn--outline btn--block" type="button" data-action="download-avatar">下载角色头像</button>
</div>
</div>
</section>
<!-- P4: Distill Ex View -->
<section id="distill" class="view view--distill-page">
<header class="creator-header">
<button class="icon-btn" type="button" data-action="back" aria-label="返回"></button>
<div class="creator-brand">
<span class="creator-brand__name">Eternal AI</span>
<span class="creator-brand__step">蒸馏前任</span>
</div>
<div class="stepper" aria-hidden="true"></div>
</header>
<div class="distill-page">
<a href="#" class="distill-quick" data-action="contact-wechat">没耐心?直接加微信定制沟通 →</a>
<div class="distill-section">
<h3 class="distill-section__title">什么是蒸馏前任?</h3>
<p class="distill-section__text">把你们曾经的聊天记录、语音习惯、相处细节交给我们,技术团队会将其蒸馏成一个可对话的 AI 前任。ta 会记得你们的暗号、说话节奏,甚至那些只有你们懂的小情绪。</p>
</div>
<div class="distill-section">
<h3 class="distill-section__title">适合谁?</h3>
<ul class="distill-list">
<li>想好好告别,却还没说完话的人</li>
<li>希望把一段关系以安全方式封存的人</li>
<li>想借 AI 完成自我疗愈、练习表达的人</li>
</ul>
</div>
<div class="distill-section">
<h3 class="distill-section__title">服务流程</h3>
<ol class="distill-steps">
<li><span></span> 下单付款</li>
<li><span></span> 客服指导导出聊天记录</li>
<li><span></span> 上传至指定云盘</li>
<li><span></span> 技术人员处理蒸馏</li>
<li><span></span> 完成后获得专属二维码和头像</li>
</ol>
</div>
<div class="distill-price">
<span class="distill-price__label">标准版</span>
<span class="distill-price__value">¥ 199</span>
<span class="distill-price__unit">/ 次</span>
</div>
<div class="form-actions">
<button class="btn btn--primary btn--block" type="button" data-action="pay-distill">立即下单</button>
</div>
<div class="distill-note">
<p>付款后请添加客服微信,我们将一对一指导你完成聊天记录导出与上传。</p>
</div>
<div class="distill-revenue">
<p>创作者可推广蒸馏前任服务获得分润,具体比例请在创作者管理中心配置。</p>
</div>
</div>
</section>
<!-- P5: About View -->
<section id="about" class="view view--about">
<header class="creator-header">
<button class="icon-btn" type="button" data-action="back" aria-label="返回"></button>
<div class="creator-brand">
<span class="creator-brand__name">关于 Eternal AI</span>
<span class="creator-brand__step">安全 · 隐私 · 信任</span>
</div>
<div class="stepper" aria-hidden="true"></div>
</header>
<div class="about-page">
<div class="about-section">
<h3 class="about-section__title">平台简介</h3>
<p class="about-section__text">Eternal AI 是一个 AI 陪伴平台,帮助创作者将人设设定部署为可对话的 AI 角色,让用户在微信中与 ta 交流。我们致力于在记忆与陪伴中,遇见更懂你的 AI。</p>
</div>
<div class="about-section">
<h3 class="about-section__title">连接方式说明</h3>
<p class="about-section__text">Eternal AI 基于微信 Claw 开放协议iLink非破解、非抓包在微信官方授权范围内实现 AI 联系人接入。你的微信账号安全不受影响。</p>
</div>
<div class="about-section">
<h3 class="about-section__title">安全类</h3>
<div class="faq-item">
<button class="faq-q" type="button">你们能看到我其他的微信聊天吗?<span class="faq-icon">+</span></button>
<div class="faq-a"><p>不能。我们只能看到你与 AI 角色的对话,无法访问你的其他聊天、朋友圈或通讯录。</p></div>
</div>
<div class="faq-item">
<button class="faq-q" type="button">会不会偷偷读我的朋友圈和通讯录?<span class="faq-icon">+</span></button>
<div class="faq-a"><p>不会。我们仅通过 Claw 协议与 AI 联系人通信,不具备读取你朋友圈或通讯录的能力。</p></div>
</div>
</div>
<div class="about-section">
<h3 class="about-section__title">隐私类</h3>
<div class="faq-item">
<button class="faq-q" type="button">我和 AI 说的话会被保存吗?<span class="faq-icon">+</span></button>
<div class="faq-a"><p>对话内容会保存在你的专属记忆库中,用于 AI 角色记住你们的对话上下文。仅你和你的 AI 角色可访问,我们不会人工查看。</p></div>
</div>
<div class="faq-item">
<button class="faq-q" type="button">蒸馏前任要上传聊天记录,这些数据安全吗?<span class="faq-icon">+</span></button>
<div class="faq-a"><p>上传的聊天记录仅用于训练你的专属 AI 前任,处理完成后原始数据将被删除。我们承诺不会将你的数据用于其他用途。</p></div>
</div>
</div>
<div class="about-section">
<h3 class="about-section__title">账号类</h3>
<div class="faq-item">
<button class="faq-q" type="button">用了会不会封我的微信号?<span class="faq-icon">+</span></button>
<div class="faq-a"><p>不会。Claw 协议在微信官方授权范围内运行,与正常添加联系人无异,不存在封号风险。</p></div>
</div>
<div class="faq-item">
<button class="faq-q" type="button">扫码之后对方能控制我的微信吗?<span class="faq-icon">+</span></button>
<div class="faq-a"><p>不能。AI 角色只是一个普通的微信联系人,只能与你收发消息,无法控制你的账号。</p></div>
</div>
</div>
<div class="about-section">
<h3 class="about-section__title">情感类</h3>
<div class="faq-item">
<button class="faq-q" type="button">我聊的内容会有人看吗?<span class="faq-icon">+</span></button>
<div class="faq-a"><p>不会有人工查看你的对话内容。除非你主动联系客服反馈问题,我们才会根据你的授权查看相关记录。</p></div>
</div>
<div class="faq-item">
<button class="faq-q" type="button">你们会把我的对话内容拿去训练 AI 吗?<span class="faq-icon">+</span></button>
<div class="faq-a"><p>不会。你的对话内容仅用于你自己的 AI 角色的记忆,绝不会用于训练其他模型或分享给第三方。</p></div>
</div>
</div>
</div>
</section>
<!-- P6: Creator Onboarding View -->
<section id="onboarding" class="view view--onboarding">
<header class="creator-header">
<button class="icon-btn" type="button" data-action="back" aria-label="返回"></button>
<div class="creator-brand">
<span class="creator-brand__name">创作者入驻</span>
<span class="creator-brand__step">部署 AI 角色,私域变现</span>
</div>
<div class="stepper" aria-hidden="true"></div>
</header>
<div class="onboarding-page">
<div class="onboarding-section">
<h3 class="onboarding-section__title">平台能为创作者提供什么</h3>
<ul class="onboarding-list">
<li>部署 AI 角色:提交人设设定,平台负责技术部署</li>
<li>资金中转:平台处理用户付款,按比例结算给创作者</li>
<li>私域变现:通过专属链接绑定粉丝,持续获得订阅收入</li>
</ul>
</div>
<div class="onboarding-section">
<h3 class="onboarding-section__title">合作模式</h3>
<p class="onboarding-section__text">创作者提交角色人设,平台部署上线。用户付款后,平台抽佣 20%80% 转给创作者。结算周期为月结。</p>
</div>
<div class="onboarding-section onboarding-contact">
<h3 class="onboarding-section__title">联系入驻</h3>
<div class="onboarding-qr" id="onboarding-qr"></div>
<p class="onboarding-wechat">微信扫码添加负责人</p>
<p class="onboarding-wechat-id">微信号EternalAI_Official</p>
<p class="onboarding-note">添加时请备注「创作者入驻」,我们会一对一沟通合作细节。</p>
</div>
</div>
</section>
<!-- P7: Creator Center View -->
<section id="creator-center" class="view view--creator-center">
<header class="creator-header">
<button class="icon-btn" type="button" data-action="back" aria-label="返回"></button>
<div class="creator-brand">
<span class="creator-brand__name">创作者管理中心</span>
<span class="creator-brand__step" id="center-tab-label">我的角色</span>
</div>
<div class="stepper" aria-hidden="true"></div>
</header>
<div class="center-tabs">
<button class="center-tab active" type="button" data-center-tab="roles">我的角色</button>
<button class="center-tab" type="button" data-center-tab="income">收入</button>
<button class="center-tab" type="button" data-center-tab="settings">我的</button>
</div>
<!-- Tab: Roles -->
<div class="center-panel active" id="center-roles">
<div class="role-list" id="creator-role-list"></div>
<div class="form-actions">
<button class="btn btn--primary btn--block" type="button" data-action="new-role">+ 新建角色</button>
</div>
</div>
<!-- Tab: Income -->
<div class="center-panel" id="center-income">
<div class="income-balance">
<span class="income-balance__label">可提现余额</span>
<span class="income-balance__value" id="income-balance">¥ 0.00</span>
</div>
<div class="income-section">
<h3 class="income-section__title">流水明细</h3>
<div class="income-list" id="income-list">
<p class="income-empty">暂无流水记录</p>
</div>
</div>
<div class="income-section">
<h3 class="income-section__title">申请提现</h3>
<form id="withdraw-form" class="withdraw-form" autocomplete="off">
<label class="field">
<span class="field__label">收款方式</span>
<select class="field__input" name="method" required>
<option value="wechat">微信</option>
<option value="alipay">支付宝</option>
</select>
</label>
<label class="field">
<span class="field__label">提现金额(¥)</span>
<input class="field__input" name="amount" type="number" min="1" step="0.01" placeholder="请输入金额" required />
</label>
<div class="form-actions">
<button class="btn btn--primary btn--block" type="submit">提交提现申请</button>
</div>
</form>
</div>
</div>
<!-- Tab: Settings -->
<div class="center-panel" id="center-settings">
<form id="settings-form" class="settings-form" autocomplete="off">
<label class="field">
<span class="field__label">创作者名字</span>
<input class="field__input" name="creatorName" type="text" placeholder="你的笔名" id="settings-name" />
</label>
<label class="field">
<span class="field__label">角色库名称(首页「我的 XXX」显示的文字</span>
<input class="field__input" name="libraryName" type="text" placeholder="如:云朵的后宫" id="settings-library" />
</label>
<div class="form-actions">
<button class="btn btn--primary btn--block" type="submit">保存设置</button>
</div>
</form>
<div class="form-actions">
<button class="btn btn--ghost btn--block" type="button" data-action="logout">退出登录</button>
</div>
</div>
</section>
<!-- Creator Role Edit View (formerly character creator) -->
<section id="creator" class="view view--creator">
<header class="creator-header">
<button class="icon-btn" type="button" data-action="back-to-center" aria-label="返回"></button>
<div class="creator-brand">
<span class="creator-brand__name">角色编辑</span>
<span class="creator-brand__step">生成配置文件</span>
</div>
<div class="stepper" aria-hidden="true">
<span class="stepper__dot active"></span>
@ -219,126 +569,28 @@
</div>
<div class="form-actions">
<button class="btn btn--ghost" type="button" data-action="reset">再创建一个</button>
<button class="btn btn--primary" type="button" data-action="back">返回首页</button>
</div>
</div>
</section>
<!-- Auth View (Login / Register) -->
<section id="auth" class="view view--auth">
<header class="creator-header">
<button class="icon-btn" type="button" data-action="back" aria-label="返回"></button>
<div class="creator-brand">
<span class="creator-brand__name">Eternal AI</span>
<span class="creator-brand__step">登录 / 注册</span>
</div>
<div class="stepper" aria-hidden="true"></div>
</header>
<div class="auth-tabs">
<button class="auth-tab active" type="button" data-tab="login">登录</button>
<button class="auth-tab" type="button" data-tab="register">注册</button>
</div>
<form id="login-form" class="auth-form active" autocomplete="off" data-form="login">
<div class="field-group">
<label class="field">
<span class="field__label">手机号 / 用户名</span>
<input class="field__input" name="account" type="text" placeholder="请输入手机号或用户名" required />
</label>
<label class="field">
<span class="field__label">密码</span>
<input class="field__input" name="password" type="password" placeholder="请输入密码" required />
</label>
</div>
<div class="form-actions">
<button class="btn btn--primary btn--block" type="submit">登录</button>
</div>
<p class="auth-hint">登录后可查看角色库、管理已订阅的角色。</p>
</form>
<form id="register-form" class="auth-form" autocomplete="off" data-form="register">
<div class="field-group">
<label class="field">
<span class="field__label">手机号 / 用户名</span>
<input class="field__input" name="account" type="text" placeholder="设置登录账号" required />
</label>
<label class="field">
<span class="field__label">密码</span>
<input class="field__input" name="password" type="password" placeholder="设置密码" required minlength="6" />
</label>
<label class="field">
<span class="field__label">确认密码</span>
<input class="field__input" name="confirmPassword" type="password" placeholder="再次输入密码" required />
</label>
</div>
<div class="form-actions">
<button class="btn btn--primary btn--block" type="submit">注册</button>
</div>
<p class="auth-hint">注册即代表同意《用户协议》和《隐私政策》。</p>
</form>
</section>
<!-- Distill Ex View -->
<section id="distill" class="view view--distill-page">
<header class="creator-header">
<button class="icon-btn" type="button" data-action="back" aria-label="返回"></button>
<div class="creator-brand">
<span class="creator-brand__name">Eternal AI</span>
<span class="creator-brand__step">蒸馏前任</span>
</div>
<div class="stepper" aria-hidden="true"></div>
</header>
<div class="distill-page">
<a href="#contact" class="distill-quick">没耐心?直接加微信定制沟通 →</a>
<div class="distill-section">
<h3 class="distill-section__title">什么是蒸馏前任?</h3>
<p class="distill-section__text">把你们曾经的聊天记录、语音习惯、相处细节交给我们,技术团队会将其蒸馏成一个可对话的 AI 前任。ta 会记得你们的暗号、说话节奏,甚至那些只有你们懂的小情绪。</p>
</div>
<div class="distill-section">
<h3 class="distill-section__title">适合谁?</h3>
<ul class="distill-list">
<li>想好好告别,却还没说完话的人</li>
<li>希望把一段关系以安全方式封存的人</li>
<li>想借 AI 完成自我疗愈、练习表达的人</li>
</ul>
</div>
<div class="distill-section">
<h3 class="distill-section__title">服务流程</h3>
<ol class="distill-steps">
<li><span></span> 下单付款</li>
<li><span></span> 客服指导导出聊天记录</li>
<li><span></span> 上传至指定云盘</li>
<li><span></span> 技术人员处理蒸馏</li>
<li><span></span> 完成后获得专属二维码和头像</li>
</ol>
</div>
<div class="distill-price">
<span class="distill-price__label">标准版</span>
<span class="distill-price__value">¥ 199</span>
<span class="distill-price__unit">/ 次</span>
</div>
<div class="form-actions">
<button class="btn btn--primary btn--block" type="button">立即下单</button>
</div>
<div class="distill-note">
<p>付款后请添加客服微信,我们将一对一指导你完成聊天记录导出与上传。</p>
</div>
<div class="distill-revenue">
<p>创作者可推广蒸馏前任服务获得分润,具体比例请在创作者管理中心配置。</p>
<button class="btn btn--primary" type="button" data-action="back-to-center">返回管理中心</button>
</div>
</div>
</section>
</main>
<!-- Tab Bar (U8) -->
<nav class="tab-bar" id="tab-bar">
<button class="tab-bar__item active" type="button" data-tab-action="tab-home">
<span class="tab-bar__icon"></span>
<span class="tab-bar__label">首页</span>
</button>
<button class="tab-bar__item" type="button" data-tab-action="tab-distill">
<span class="tab-bar__icon"></span>
<span class="tab-bar__label">蒸馏前任</span>
</button>
<button class="tab-bar__item" type="button" data-tab-action="tab-mine">
<span class="tab-bar__icon"></span>
<span class="tab-bar__label" id="tab-mine-label">我的</span>
</button>
</nav>
<script src="app.js"></script>
</body>
</html>

View File

@ -774,6 +774,539 @@ textarea.field__input--tall {
display: none !important;
}
/* Role Library (P2) */
.view--role-library,
.view--role-detail,
.view--about,
.view--onboarding,
.view--creator-center {
padding-top: 40vh;
}
.role-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.role-card {
display: flex;
align-items: center;
gap: 14px;
padding: 14px;
border-radius: var(--radius);
background: rgba(8, 9, 22, 0.55);
border: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
cursor: pointer;
transition: transform var(--transition), border-color var(--transition);
}
.role-card:hover {
transform: translateY(-2px);
border-color: rgba(124, 140, 255, 0.3);
}
.role-card__avatar {
width: 56px;
height: 56px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
border: 2px solid rgba(255, 255, 255, 0.15);
}
.role-card__info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.role-card__name {
font-family: "Noto Serif SC", serif;
font-size: 16px;
font-weight: 600;
color: #fff;
}
.role-card__desc {
font-size: 12px;
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.role-card__price {
font-size: 15px;
font-weight: 600;
color: var(--accent-soft);
}
.role-card__status {
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
display: inline-block;
width: fit-content;
}
.role-card__status--running {
background: rgba(80, 200, 120, 0.18);
color: #6ee7a0;
}
.role-card__status--stopped {
background: rgba(200, 80, 80, 0.18);
color: #f08a8a;
}
.empty-state {
text-align: center;
padding: 40px 20px;
}
.empty-state__text {
font-family: "Noto Serif SC", serif;
font-size: 17px;
color: var(--text);
margin-bottom: 8px;
}
.empty-state__hint {
font-size: 13px;
color: var(--text-muted);
line-height: 1.6;
}
/* Role Detail (P3) */
.detail-page {
display: flex;
flex-direction: column;
gap: 16px;
}
.detail-hero {
width: 100%;
height: 200px;
border-radius: var(--radius);
background-size: cover;
background-position: center;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.detail-title {
font-family: "Noto Serif SC", serif;
font-size: 24px;
font-weight: 700;
color: #fff;
}
.detail-desc {
font-size: 14px;
line-height: 1.7;
color: rgba(220, 225, 255, 0.85);
}
.detail-price {
display: flex;
align-items: baseline;
gap: 4px;
}
.detail-price__value {
font-size: 28px;
font-weight: 700;
color: #fff;
}
.detail-price__unit {
font-size: 13px;
color: var(--text-muted);
}
.detail-paid {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
padding: 20px;
border-radius: var(--radius);
background: rgba(8, 9, 22, 0.55);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.detail-qr {
width: 100%;
display: flex;
justify-content: center;
}
.qr-placeholder {
width: 160px;
height: 160px;
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, 0.08);
border: 2px dashed rgba(255, 255, 255, 0.2);
display: grid;
place-items: center;
text-align: center;
font-size: 14px;
color: var(--text-muted);
line-height: 1.6;
}
.detail-paid__hint {
font-size: 12px;
color: var(--text-muted);
text-align: center;
line-height: 1.6;
}
.detail-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background-size: cover;
background-position: center;
border: 2px solid rgba(255, 255, 255, 0.2);
}
/* About Page (P5) */
.about-page {
display: flex;
flex-direction: column;
gap: 18px;
}
.about-section {
padding: 18px;
border-radius: var(--radius);
background: rgba(8, 9, 22, 0.55);
border: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.about-section__title {
font-family: "Noto Serif SC", serif;
font-size: 16px;
font-weight: 600;
margin-bottom: 10px;
color: #fff;
}
.about-section__text {
font-size: 13px;
line-height: 1.75;
color: rgba(220, 225, 255, 0.85);
}
.faq-item {
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.faq-item:first-child {
border-top: none;
}
.faq-q {
width: 100%;
padding: 12px 0;
background: none;
border: none;
color: var(--text);
font-size: 13px;
font-family: inherit;
text-align: left;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.faq-icon {
font-size: 18px;
color: var(--accent);
flex-shrink: 0;
}
.faq-a {
display: none;
padding-bottom: 12px;
}
.faq-a p {
font-size: 12px;
line-height: 1.7;
color: var(--text-muted);
}
/* Onboarding Page (P6) */
.onboarding-page {
display: flex;
flex-direction: column;
gap: 18px;
}
.onboarding-section {
padding: 18px;
border-radius: var(--radius);
background: rgba(8, 9, 22, 0.55);
border: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.onboarding-section__title {
font-family: "Noto Serif SC", serif;
font-size: 16px;
font-weight: 600;
margin-bottom: 10px;
color: #fff;
}
.onboarding-section__text {
font-size: 13px;
line-height: 1.75;
color: rgba(220, 225, 255, 0.85);
}
.onboarding-list {
padding-left: 18px;
margin: 0;
font-size: 13px;
line-height: 1.75;
color: rgba(220, 225, 255, 0.85);
}
.onboarding-list li {
margin-bottom: 6px;
}
.onboarding-contact {
text-align: center;
}
.onboarding-qr {
width: 140px;
height: 140px;
margin: 0 auto 12px;
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, 0.08);
border: 2px dashed rgba(255, 255, 255, 0.2);
display: grid;
place-items: center;
}
.onboarding-qr::after {
content: "二维码";
font-size: 12px;
color: var(--text-muted);
}
.onboarding-wechat {
font-size: 14px;
color: var(--text);
margin-bottom: 4px;
}
.onboarding-wechat-id {
font-size: 13px;
color: var(--accent-soft);
margin-bottom: 8px;
}
.onboarding-note {
font-size: 12px;
color: var(--text-muted);
line-height: 1.6;
}
/* Creator Center (P7) */
.center-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
padding: 5px;
border-radius: var(--radius);
background: rgba(8, 9, 22, 0.55);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.center-tab {
flex: 1;
padding: 10px;
border-radius: var(--radius-sm);
border: none;
background: transparent;
color: var(--text-muted);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: color var(--transition), background var(--transition);
}
.center-tab.active {
background: rgba(255, 255, 255, 0.1);
color: var(--text);
}
.center-panel {
display: none;
flex-direction: column;
flex: 1;
gap: 16px;
}
.center-panel.active {
display: flex;
}
.income-balance {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 24px;
border-radius: var(--radius);
background: rgba(8, 9, 22, 0.55);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.income-balance__label {
font-size: 13px;
color: var(--text-muted);
}
.income-balance__value {
font-size: 36px;
font-weight: 700;
color: #fff;
}
.income-section {
padding: 18px;
border-radius: var(--radius);
background: rgba(8, 9, 22, 0.55);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.income-section__title {
font-family: "Noto Serif SC", serif;
font-size: 15px;
font-weight: 600;
margin-bottom: 12px;
color: #fff;
}
.income-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.income-record {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.income-record:last-child {
border-bottom: none;
}
.income-record__info {
display: flex;
flex-direction: column;
gap: 2px;
}
.income-record__role {
font-size: 13px;
color: var(--text);
}
.income-record__time {
font-size: 11px;
color: var(--text-muted);
}
.income-record__amount {
font-size: 14px;
font-weight: 600;
color: #6ee7a0;
}
.income-empty {
font-size: 13px;
color: var(--text-muted);
text-align: center;
padding: 12px;
}
.withdraw-form,
.settings-form {
display: flex;
flex-direction: column;
gap: 14px;
}
/* Tab Bar (U8) */
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
background: rgba(6, 6, 14, 0.92);
border-top: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
z-index: 100;
padding-bottom: env(safe-area-inset-bottom, 0);
}
.tab-bar__item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 10px 0 8px;
background: none;
border: none;
color: var(--text-muted);
font-size: 11px;
font-family: inherit;
cursor: pointer;
transition: color var(--transition);
}
.tab-bar__item.active {
color: var(--accent);
}
.tab-bar__icon {
font-size: 20px;
line-height: 1;
}
.tab-bar__label {
font-size: 10px;
}
/* Add bottom padding for tab bar */
.app {
padding-bottom: 72px;
}
/* Responsive */
@media (max-width: 420px) {
.app {