Files
note-book/index.html
2026-05-19 11:46:42 +08:00

3114 lines
136 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>家庭记账 · 液态玻璃设计</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif;
background: #FFFFFF;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
position: relative;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
top: -200px;
right: -150px;
width: 500px;
height: 500px;
background: radial-gradient(circle, rgba(255,107,53,0.06) 0%, transparent 70%);
border-radius: 50%;
pointer-events: none;
z-index: 0;
}
body::after {
content: '';
position: fixed;
bottom: -200px;
left: -150px;
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(255,107,53,0.04) 0%, transparent 70%);
border-radius: 50%;
pointer-events: none;
z-index: 0;
}
.page-title {
color: #1C1C1E;
font-size: 28px;
font-weight: 700;
letter-spacing: -0.5px;
margin-bottom: 6px;
text-align: center;
position: relative;
z-index: 1;
}
.page-subtitle {
color: #8E8E93;
font-size: 14px;
font-weight: 400;
margin-bottom: 32px;
text-align: center;
letter-spacing: 0.3px;
position: relative;
z-index: 1;
}
.page-subtitle span {
color: #FF6B35;
font-weight: 600;
}
.caption-badge {
margin-top: 24px;
padding: 6px 18px;
background: rgba(255,107,53,0.06);
border: 1px solid rgba(255,107,53,0.12);
border-radius: 20px;
color: #FF6B35;
font-size: 12px;
font-weight: 600;
letter-spacing: 1px;
backdrop-filter: blur(10px);
position: relative;
z-index: 1;
}
@keyframes liquidMorph {
0% { border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; transform: rotate(0deg) scale(1); }
25% { border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; }
50% { border-radius: 40% 60% 50% 50% / 60% 40% 60% 40%; transform: rotate(180deg) scale(1.05); }
75% { border-radius: 70% 30% 40% 60% / 30% 70% 30% 70%; }
100% { border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; transform: rotate(360deg) scale(1); }
}
@keyframes liquidMorph2 {
0% { border-radius: 50% 50% 60% 40% / 40% 60% 40% 60%; transform: rotate(0deg) scale(1); }
33% { border-radius: 40% 60% 30% 70% / 60% 30% 70% 30%; }
66% { border-radius: 60% 40% 70% 30% / 40% 60% 30% 70%; transform: rotate(240deg) scale(1.08); }
100% { border-radius: 50% 50% 60% 40% / 40% 60% 40% 60%; transform: rotate(360deg) scale(1); }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
@keyframes pulseOrange {
0%, 100% { box-shadow: 0 4px 20px rgba(255,107,53,0.2), 0 0 40px rgba(255,107,53,0.05); }
50% { box-shadow: 0 8px 40px rgba(255,107,53,0.4), 0 0 60px rgba(255,107,53,0.1); }
}
@keyframes fadeSlideUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes barGrow {
from { width: 0 !important; }
}
@keyframes shimmerOrange {
0% { transform: translateX(-100%) skewX(-15deg); }
100% { transform: translateX(300%) skewX(-15deg); }
}
.phone-wrapper {
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
z-index: 1;
}
.phone-wrapper:hover {
transform: scale(1.008);
}
.blob-layer {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
.blob-1 {
position: absolute;
top: -60px;
right: -50px;
width: 260px;
height: 240px;
background: linear-gradient(135deg, #FF6B35 0%, #FF9F5B 100%);
opacity: 0.1;
filter: blur(30px);
animation: liquidMorph 12s ease-in-out infinite;
}
.blob-2 {
position: absolute;
bottom: -40px;
left: -70px;
width: 200px;
height: 180px;
background: linear-gradient(135deg, #FF9F5B 0%, #FF6B35 100%);
opacity: 0.08;
filter: blur(35px);
animation: liquidMorph2 15s ease-in-out infinite;
}
.blob-3 {
position: absolute;
top: 30%;
left: 50%;
width: 120px;
height: 120px;
background: linear-gradient(135deg, #FF6B35, #FFB088);
opacity: 0.06;
filter: blur(25px);
border-radius: 50%;
animation: float 6s ease-in-out infinite;
}
@keyframes statusBarFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes cardHover {
0%, 100% { transform: translateY(0); box-shadow: 0 8px 32px rgba(255,107,53,0.08); }
50% { transform: translateY(-2px); box-shadow: 0 12px 40px rgba(255,107,53,0.12); }
}
@keyframes dockFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-2px); }
}
@keyframes liquidPulse {
0%, 100% {
box-shadow: 0 0 0 1px rgba(255,255,255,0.3) inset, 0 4px 16px rgba(255,107,53,0.35);
transform: scale(1);
}
50% {
box-shadow: 0 0 0 2px rgba(255,255,255,0.4) inset, 0 6px 24px rgba(255,107,53,0.5);
transform: scale(1.03);
}
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scanLine {
0% { top: 10%; }
50% { top: 85%; }
100% { top: 10%; }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
</style>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div class="page-title">家庭记账 · 液态玻璃设计</div>
<div class="page-subtitle">简约白 · <span>时尚橙</span> · iOS 26 Liquid Glass</div>
<div id="root"></div>
<div class="caption-badge">iOS 26 · Liquid Glass Concept</div>
<script type="text/babel">
const { useState, useEffect, useRef, useCallback, useMemo } = React;
/* ===== IosFrame (with overlay slots) ===== */
const iosFrameStyles = {
wrapper: {
display: 'inline-block',
padding: 12,
background: '#000',
borderRadius: 60,
boxShadow: '0 0 0 2px #1f2937, 0 20px 60px rgba(0,0,0,0.3)',
position: 'relative',
},
screen: {
position: 'relative',
borderRadius: 48,
overflow: 'hidden',
background: '#F2F2F7',
},
statusBar: {
position: 'absolute',
top: 0, left: 0, right: 0,
height: 54,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 32px 0 32px',
fontSize: 16,
fontWeight: 600,
fontFamily: '-apple-system, "SF Pro Text", sans-serif',
zIndex: 20,
pointerEvents: 'none',
},
dynamicIsland: {
position: 'absolute',
top: 12, left: '50%',
transform: 'translateX(-50%)',
width: 124, height: 36,
background: '#000',
borderRadius: 999,
zIndex: 30,
},
statusIcons: {
display: 'flex',
alignItems: 'center',
gap: 6,
},
signalIcon: {
display: 'flex',
alignItems: 'flex-end',
gap: 2,
height: 12,
},
signalBar: {
width: 3,
background: 'currentColor',
borderRadius: 1,
},
wifiIcon: {
width: 16,
height: 12,
position: 'relative',
},
batteryIcon: {
width: 26, height: 12,
border: '1.5px solid currentColor',
borderRadius: 3,
padding: 1,
position: 'relative',
opacity: 0.8,
},
batteryCap: {
position: 'absolute',
top: 3, right: -3,
width: 2, height: 6,
background: 'currentColor',
borderRadius: '0 1px 1px 0',
},
content: {
position: 'absolute',
top: 54, left: 0, right: 0,
bottom: 0,
overflow: 'visible',
},
homeIndicator: {
position: 'absolute',
bottom: 10,
left: '50%',
transform: 'translateX(-50%)',
width: 140, height: 5,
background: 'rgba(0,0,0,0.3)',
borderRadius: 999,
zIndex: 10,
},
homeIndicatorDark: {
background: 'rgba(255,255,255,0.5)',
},
};
function IosFrame({
children, width = 370, height = 780,
time = '10:28', battery = 78,
darkMode = false,
showStatusBar = true, showDynamicIsland = true, showHomeIndicator = true,
bottomOverlay, fabOverlay,
}) {
const textColor = darkMode ? '#fff' : '#000';
const homeIndicatorStyle = darkMode
? { ...iosFrameStyles.homeIndicator, ...iosFrameStyles.homeIndicatorDark }
: iosFrameStyles.homeIndicator;
return (
<div style={iosFrameStyles.wrapper}>
<div style={{
...iosFrameStyles.screen,
width, height,
background: darkMode ? '#000' : '#F2F2F7',
}}>
{showStatusBar && (
<div style={{ ...iosFrameStyles.statusBar, color: textColor }}>
<span>{time}</span>
<div style={iosFrameStyles.statusIcons}>
<div style={iosFrameStyles.signalIcon}>
<div style={{ ...iosFrameStyles.signalBar, height: 4 }} />
<div style={{ ...iosFrameStyles.signalBar, height: 6 }} />
<div style={{ ...iosFrameStyles.signalBar, height: 9 }} />
<div style={{ ...iosFrameStyles.signalBar, height: 11 }} />
</div>
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" style={{ color: textColor }}>
<path d="M8 11.5a1 1 0 100-2 1 1 0 000 2z" fill="currentColor" />
<path d="M3 7.5a7 7 0 0110 0" stroke="currentColor" strokeWidth="1.3" fill="none" strokeLinecap="round" />
<path d="M1 4.5a11 11 0 0114 0" stroke="currentColor" strokeWidth="1.3" fill="none" strokeLinecap="round" opacity="0.7" />
</svg>
<div style={iosFrameStyles.batteryIcon}>
<div style={{
width: `${battery}%`, height: '100%',
background: 'currentColor', borderRadius: 1, opacity: 0.9,
}} />
<div style={iosFrameStyles.batteryCap} />
</div>
</div>
</div>
)}
{showDynamicIsland && <div style={iosFrameStyles.dynamicIsland} />}
<div style={{
...iosFrameStyles.content,
bottom: showHomeIndicator ? 34 : 0,
}}>
{children}
</div>
{bottomOverlay && (
<div style={{
position: 'absolute',
bottom: showHomeIndicator ? 34 : 0,
left: 0, right: 0,
zIndex: 50,
}}>
{bottomOverlay}
</div>
)}
{fabOverlay && (
<div style={{
position: 'absolute', zIndex: 60, pointerEvents: 'none',
}}>
{fabOverlay}
</div>
)}
{showHomeIndicator && <div style={homeIndicatorStyle} />}
</div>
</div>
);
}
/* ===== Data ===== */
const CATEGORIES_EXPENSE = [
{ emoji: '\uD83C\uDF5C', name: '餐饮' },
{ emoji: '\uD83D\uDE95', name: '交通' },
{ emoji: '\uD83D\uDED2', name: '购物' },
{ emoji: '\uD83C\uDFE0', name: '居住' },
{ emoji: '\uD83D\uDC8A', name: '医疗' },
{ emoji: '\uD83C\uDFAE', name: '娱乐' },
{ emoji: '\uD83D\uDCDA', name: '教育' },
{ emoji: '\uD83D\uDCE6', name: '其他' },
];
const CATEGORIES_INCOME = [
{ emoji: '\uD83D\uDCB0', name: '工资' },
{ emoji: '\uD83D\uDCC8', name: '理财' },
{ emoji: '\uD83C\uDF93', name: '兼职' },
{ emoji: '\uD83E\uDD11', name: '红包' },
{ emoji: '\uD83C\uDFC6', name: '奖金' },
{ emoji: '\uD83D\uDCB2', name: '其他' },
];
const fmtAbs = (n) => '¥' + Math.abs(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
/* ===== Sub-Components ===== */
function OrangeHeader({ user }) {
var _bState = React.useState([]);
var bills = _bState[0];
var setBills = _bState[1];
React.useEffect(function() {
if (!user) return;
var result = window.__authApi.getBills(user.id);
if (result.success) setBills(result.data.bills);
}, [user]);
var now = new Date();
var thisMonth = now.getMonth() + 1;
var thisYear = now.getFullYear();
var monthBills = bills.filter(function(b) {
if (!b.date) return false;
var p = b.date.split('-');
return parseInt(p[0]) === thisYear && parseInt(p[1]) === thisMonth;
});
var totalIncome = monthBills.filter(function(b) { return b.type === 'income'; }).reduce(function(s, b) { return s + Number(b.amount); }, 0);
var totalExpense = monthBills.filter(function(b) { return b.type === 'expense'; }).reduce(function(s, b) { return s + Number(b.amount); }, 0);
var budgetRemaining = Math.max(totalIncome - totalExpense, 0);
var daysInMonth = new Date(thisYear, thisMonth, 0).getDate();
var daysLeft = daysInMonth - now.getDate();
return (
<div style={{
background: '#FFFFFF',
padding: '20px 16px 16px',
position: 'relative',
overflow: 'hidden',
}}>
<div className="blob-layer">
<div className="blob-1" />
<div className="blob-2" />
<div className="blob-3" />
</div>
<div style={{ position: 'relative', zIndex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
width: 44, height: 44, borderRadius: '50%',
background: 'rgba(255,255,255,0.7)',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
border: '2px solid rgba(255,107,53,0.25)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 18, fontWeight: 700, color: '#FF6B35',
boxShadow: '0 4px 16px rgba(255,107,53,0.1), 0 0 0 1px rgba(255,255,255,0.9) inset',
}}>
{user ? user.nickname.charAt(0) : '?'}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 20, fontWeight: 700, color: '#1C1C1E', letterSpacing: '-0.3px' }}>
{user ? '你好,' + user.nickname : '你好'}
</div>
<div style={{ fontSize: 12, color: '#8E8E93', marginTop: 2, fontWeight: 500 }}>
{new Date().getMonth() + 1}{new Date().getDate()} · {['日','一','二','三','四','五','六'][new Date().getDay()]}
</div>
</div>
</div>
<div style={{
display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8,
marginTop: 16,
}}>
{[
{ label: '本月收入', value: '¥' + totalIncome.toLocaleString('zh-CN', {minimumFractionDigits:2}), color: '#34C759' },
{ label: '本月支出', value: '¥' + totalExpense.toLocaleString('zh-CN', {minimumFractionDigits:2}), color: '#FF3B30' },
{ label: '预算剩余', value: '¥' + budgetRemaining.toLocaleString('zh-CN', {minimumFractionDigits:2}), color: '#FF6B35' },
].map(function(item, i) { return (
<div key={i} style={{
background: 'rgba(255,255,255,0.65)',
backdropFilter: 'blur(30px)',
WebkitBackdropFilter: 'blur(30px)',
borderRadius: 16,
padding: '12px 8px',
textAlign: 'center',
border: '1px solid rgba(255,255,255,0.8)',
boxShadow: '0 8px 32px rgba(255,107,53,0.08), 0 0 0 1px rgba(255,255,255,0.9) inset',
}}>
<div style={{ fontSize: 11, color: '#8E8E93', fontWeight: 500, marginBottom: 4 }}>
{item.label}
</div>
<div style={{
fontSize: item.value.length > 10 ? 13 : 15,
fontWeight: 700,
color: item.color,
}}>
{item.value}
</div>
</div>
);})}
</div>
<div style={{
marginTop: 12, textAlign: 'center',
fontSize: 11, color: '#8E8E93',
fontWeight: 500, letterSpacing: '0.3px',
}}>
本月还剩 {daysLeft}
</div>
</div>
</div>
);
}
function TabSwitcher({ activeTab, onSwitch, animating }) {
return (
<div style={{ padding: '12px 16px 8px', position: 'relative', zIndex: 5 }}>
<div style={{
background: 'rgba(255,255,255,0.65)',
backdropFilter: 'blur(30px)',
WebkitBackdropFilter: 'blur(30px)',
borderRadius: 14,
padding: 3,
display: 'flex',
border: '1px solid rgba(255,255,255,0.8)',
boxShadow: '0 4px 20px rgba(255,107,53,0.05)',
}}>
{['个人账本', '家庭账本'].map(tab => {
const key = tab === '个人账本' ? 'personal' : 'family';
const isActive = activeTab === key;
return (
<div
key={key}
onClick={() => !animating && onSwitch(key)}
style={{
flex: 1,
padding: '9px 12px',
borderRadius: 11,
textAlign: 'center',
fontSize: 13,
fontWeight: 600,
cursor: 'pointer',
transition: 'all 0.3s cubic-bezier(0.16, 1, 0.3, 1)',
background: isActive
? 'linear-gradient(135deg, #FF6B35 0%, #FF9F5B 100%)'
: 'transparent',
color: isActive ? '#fff' : '#8E8E93',
boxShadow: isActive ? '0 2px 12px rgba(255,107,53,0.25)' : 'none',
userSelect: 'none',
}}
>{tab}</div>
);
})}
</div>
</div>
);
}
function TransactionItem({ item, index }) {
const isIncome = item.amount > 0;
const iconBg = isIncome ? 'rgba(52,199,89,0.08)'
: item.category === '餐饮' ? 'rgba(255,107,53,0.08)'
: item.category === '交通' ? 'rgba(255,159,91,0.08)'
: item.category === '购物' ? 'rgba(255,107,53,0.06)'
: item.category === '医疗' ? 'rgba(255,59,48,0.06)'
: item.category === '工资' ? 'rgba(52,199,89,0.08)'
: 'rgba(142,142,147,0.08)';
return (
<div style={{
animation: `fadeSlideUp 0.35s ease ${index * 0.05}s both`,
}}>
<div style={{
background: 'rgba(255,255,255,0.65)',
backdropFilter: 'blur(30px)',
WebkitBackdropFilter: 'blur(30px)',
borderRadius: 16,
padding: '14px 14px',
margin: '0 16px 6px',
border: '1px solid rgba(255,255,255,0.8)',
boxShadow: '0 8px 32px rgba(255,107,53,0.06), 0 0 0 1px rgba(255,255,255,0.9) inset',
display: 'flex',
alignItems: 'center',
gap: 12,
}}>
<div style={{
width: 40, height: 40, borderRadius: '50%',
background: iconBg,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 18,
flexShrink: 0,
boxShadow: '0 2px 8px rgba(0,0,0,0.02)',
}}>
{item.emoji}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 14, fontWeight: 600, color: '#1C1C1E' }}>{item.name}</div>
<div style={{ fontSize: 11, color: '#8E8E93', marginTop: 2, fontWeight: 500 }}>
{item.category} · {item.time}
</div>
</div>
<div style={{
fontSize: 16,
fontWeight: 700,
color: isIncome ? '#34C759' : '#FF3B30',
whiteSpace: 'nowrap',
}}>
{item.amount > 0 ? '+' : ''}{fmtAbs(item.amount)}
</div>
</div>
</div>
);
}
function PersonalContent({ user }) {
var _billsState = React.useState([]);
var bills = _billsState[0];
var setBills = _billsState[1];
var loadBills = React.useCallback(function() {
if (!user) return;
var result = window.__authApi.getBills(user.id);
if (result.success) {
setBills(result.data.bills);
}
}, [user]);
React.useEffect(function() {
loadBills();
}, [loadBills]);
React.useEffect(function() {
var handler = function() { loadBills(); };
window.addEventListener('refreshBills', handler);
return function() { window.removeEventListener('refreshBills', handler); };
}, [loadBills]);
if (!user) {
return React.createElement('div', { style: { padding: '40px 16px', textAlign: 'center', color: '#8E8E93', fontSize: 14 }}, '请先登录以查看账单');
}
var todayStr = new Date().getFullYear() + '年' + (new Date().getMonth() + 1) + '月' + new Date().getDate() + '日';
var weekDays = ['日','一','二','三','四','五','六'];
var weekDay = weekDays[new Date().getDay()];
return React.createElement('div', null,
React.createElement('div', { style: { padding: '8px 16px 6px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }},
React.createElement('div', { style: { fontSize: 15, fontWeight: 700, color: '#1C1C1E' }}, '今日 · ' + new Date().getMonth() + '月' + new Date().getDate() + '日'),
React.createElement('div', { style: { fontSize: 12, color: '#FF6B35', fontWeight: 600, cursor: 'pointer', transition: 'color 0.15s ease' }}, '查看全部 →')
),
bills.length === 0
? React.createElement('div', { style: { padding: '40px 16px', textAlign: 'center', color: '#8E8E93', fontSize: 13 }}, '暂无账单记录')
: bills.map(function(item, i) {
return React.createElement(TransactionItem, {
key: item.id,
item: {
emoji: item.emoji,
name: item.note || item.category,
category: item.category,
time: item.time,
amount: item.type === 'income' ? item.amount : -item.amount,
},
index: i,
});
}),
React.createElement('div', { style: { height: 20 }})
);
}
function FamilyContent({ user }) {
var _billsState = React.useState([]);
var bills = _billsState[0];
var setBills = _billsState[1];
var _familyState = React.useState(null);
var family = _familyState[0];
var setFamily = _familyState[1];
var _membersState = React.useState([]);
var members = _membersState[0];
var setMembers = _membersState[1];
var loadData = React.useCallback(function() {
if (!user) return;
// Get active family
var familiesResult = window.__authApi.getUserFamilies(user.id);
if (!familiesResult.success || familiesResult.data.families.length === 0) {
setBills([]);
setFamily(null);
setMembers([]);
return;
}
var activeFamily = familiesResult.data.families.find(function(f) { return f.id === user.activeFamilyId; }) || familiesResult.data.families[0];
setFamily(activeFamily);
setMembers(activeFamily.members || []);
// Get family bills
var billsResult = window.__authApi.getFamilyBills(activeFamily.id);
if (billsResult.success) {
setBills(billsResult.data.bills);
}
}, [user]);
React.useEffect(function() {
loadData();
}, [loadData]);
React.useEffect(function() {
var handler = function() { loadData(); };
window.addEventListener('refreshBills', handler);
return function() { window.removeEventListener('refreshBills', handler); };
}, [loadData]);
if (!user) {
return React.createElement('div', { style: { padding: '40px 16px', textAlign: 'center', color: '#8E8E93', fontSize: 14 }}, '请先登录以查看家庭账单');
}
if (!family) {
return React.createElement('div', { style: { padding: '40px 16px', textAlign: 'center', color: '#8E8E93', fontSize: 14 }}, '暂未加入任何家庭');
}
// Compute member rankings from bills
var memberTotals = {};
members.forEach(function(m) {
memberTotals[m.userId] = { userId: m.userId, nickname: m.nickname, total: 0, count: 0 };
});
var overallTotal = 0;
bills.forEach(function(b) {
overallTotal += Number(b.amount) || 0;
if (memberTotals[b.userId]) {
memberTotals[b.userId].total += Number(b.amount) || 0;
memberTotals[b.userId].count += 1;
}
});
var rankedMembers = Object.keys(memberTotals).map(function(k) { return memberTotals[k]; }).sort(function(a, b) { return b.total - a.total; });
var rankEmojis = ['🥇','🥈','🥉'];
var maxTotal = rankedMembers.length > 0 ? rankedMembers[0].total : 1;
// Recent family bills (last 5)
var recentBills = bills.slice(0, 5);
var glassCardStyle = {
background: 'rgba(255,255,255,0.65)',
backdropFilter: 'blur(30px)',
WebkitBackdropFilter: 'blur(30px)',
borderRadius: 14,
padding: '10px 14px',
marginBottom: 6,
border: '1px solid rgba(255,255,255,0.8)',
boxShadow: '0 4px 16px rgba(0,0,0,0.02)',
};
return React.createElement('div', null,
// Header
React.createElement('div', { style: { padding: '6px 16px 8px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }},
React.createElement('div', null,
React.createElement('div', { style: { fontSize: 15, fontWeight: 700, color: '#1C1C1E' }}, '家庭账单'),
React.createElement('div', { style: { fontSize: 11, color: '#8E8E93', fontWeight: 500, marginTop: 1 }},
React.createElement('span', { style: { color: '#FF6B35', fontWeight: 600 }}, family.name),
' · 本月累计 ¥' + overallTotal.toLocaleString('zh-CN', { minimumFractionDigits: 0 })
)
),
React.createElement('div', { style: { fontSize: 12, color: '#FF6B35', fontWeight: 600, background: 'rgba(255,107,53,0.08)', padding: '4px 10px', borderRadius: 8, cursor: 'pointer' }},
'详情 →'
)
),
// Member Rankings
React.createElement('div', { style: { padding: '0 16px', marginBottom: 4 }},
React.createElement('div', { style: { fontSize: 12, fontWeight: 600, color: '#8E8E93', marginBottom: 8, paddingLeft: 4 }}, '成员消费排行'),
rankedMembers.length === 0
? React.createElement('div', { style: { padding: '20px 16px', textAlign: 'center', color: '#8E8E93', fontSize: 13 }}, '暂无消费记录')
: rankedMembers.map(function(m, i) {
var isYou = m.userId === user.id;
var rankIcon = i < 3 ? rankEmojis[i] : String(i + 1);
var pct = maxTotal > 0 ? Math.round(m.total / maxTotal * 100) : 0;
return React.createElement('div', {
key: m.userId,
style: Object.assign({}, glassCardStyle, {
border: isYou ? '1px solid rgba(255,107,53,0.15)' : '1px solid rgba(255,255,255,0.8)',
borderLeft: isYou ? '3px solid #FF6B35' : '1px solid rgba(255,255,255,0.8)',
boxShadow: isYou ? '0 8px 32px rgba(255,107,53,0.08)' : '0 4px 16px rgba(0,0,0,0.02)',
})
},
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 }},
React.createElement('span', { style: { fontSize: 16, width: 22, textAlign: 'center' }}, rankIcon),
React.createElement('div', { style: {
width: 32, height: 32, borderRadius: '50%',
background: isYou ? 'linear-gradient(135deg, #FF6B35, #FF9F5B)' : 'rgba(142,142,147,0.1)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 15, flexShrink: 0,
}},
isYou ? '👤' : '🙋'
),
React.createElement('span', { style: { flex: 1, fontSize: 13, fontWeight: 600, color: isYou ? '#FF6B35' : '#1C1C1E' }},
m.nickname + (isYou ? '(你)' : '')
),
React.createElement('span', { style: { fontSize: 13, fontWeight: 700, color: '#1C1C1E' }},
'¥' + m.total.toLocaleString('zh-CN', { minimumFractionDigits: 0 })
),
React.createElement('span', { style: { fontSize: 11, color: '#8E8E93', fontWeight: 500 }},
pct + '%'
)
)
);
})
),
// Recent Family Bills
React.createElement('div', { style: { padding: '0 16px' }},
React.createElement('div', { style: { fontSize: 12, fontWeight: 600, color: '#8E8E93', marginBottom: 8, paddingLeft: 4, marginTop: 12 }}, '最近家庭记录'),
recentBills.length === 0
? React.createElement('div', { style: { padding: '20px 16px', textAlign: 'center', color: '#8E8E93', fontSize: 13 }}, '暂无家庭账单记录')
: recentBills.map(function(b, i) {
var member = members.find(function(m) { return m.userId === b.userId; });
var personName = member ? member.nickname : '未知';
var amt = b.type === 'expense' ? -Number(b.amount) : Number(b.amount);
return React.createElement('div', {
key: b.id,
style: Object.assign({}, glassCardStyle, { display: 'flex', alignItems: 'center', gap: 10 }),
},
React.createElement('span', { style: { fontSize: 18, width: 28, textAlign: 'center' }}, b.emoji || '📋'),
React.createElement('div', { style: { flex: 1 }},
React.createElement('div', { style: { fontSize: 13, fontWeight: 600, color: '#1C1C1E' }}, b.note || b.category),
React.createElement('div', { style: { fontSize: 11, color: '#8E8E93', fontWeight: 500, marginTop: 1 }},
personName + ' · ' + b.time
)
),
React.createElement('span', { style: { fontSize: 14, fontWeight: 700, color: amt > 0 ? '#34C759' : '#FF3B30', whiteSpace: 'nowrap' }},
(amt > 0 ? '+' : '') + '¥' + Math.abs(amt).toLocaleString('zh-CN', { minimumFractionDigits: 2 })
)
);
})
),
React.createElement('div', { style: { height: 20 }})
);
}
function FAB({ onClick }) {
return (
<div onClick={onClick} style={{
pointerEvents: 'auto',
width: 50, height: 50,
borderRadius: '50%',
background: 'linear-gradient(135deg, #FF6B35 0%, #FF9F5B 100%)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: '0 4px 20px rgba(255,107,53,0.25), 0 0 0 1px rgba(255,255,255,0.2)',
cursor: 'pointer',
animation: 'pulseOrange 3s ease-in-out infinite',
color: '#fff',
fontSize: 26,
fontWeight: 300,
lineHeight: 1,
userSelect: 'none',
}}>
+
</div>
);
}
function ActionSheet({ show, onManual, onScan, onCancel }) {
if (!show) return null;
return (
<div onClick={onCancel} style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
background: 'rgba(0,0,0,0.3)',
backdropFilter: 'blur(4px)',
WebkitBackdropFilter: 'blur(4px)',
zIndex: 200,
display: 'flex', alignItems: 'flex-end', justifyContent: 'center',
animation: 'fadeIn 0.2s ease',
}}>
<div onClick={e => e.stopPropagation()} style={{
background: 'rgba(255,255,255,0.9)',
backdropFilter: 'blur(40px)',
WebkitBackdropFilter: 'blur(40px)',
borderRadius: '16px 16px 0 0',
width: '100%', maxWidth: 370,
padding: '20px 16px 32px',
animation: 'slideUp 0.35s cubic-bezier(0.16, 1, 0.3, 1)',
border: '1px solid rgba(255,255,255,0.6)',
boxShadow: '0 -4px 32px rgba(0,0,0,0.1)',
}}>
{/* 手动记账 */}
<div onClick={onManual} style={{
display: 'flex', alignItems: 'center', gap: 14,
padding: '14px 16px', borderRadius: 12, cursor: 'pointer',
marginBottom: 6,
background: 'rgba(255,255,255,0.5)',
transition: 'background 0.15s ease',
}}>
<span style={{ fontSize: 26, flexShrink: 0 }}>📝</span>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 15, fontWeight: 600, color: '#1C1C1E' }}>手动记账</div>
<div style={{ fontSize: 12, color: '#8E8E93', fontWeight: 500, marginTop: 2 }}>手动输入账单信息</div>
</div>
</div>
{/* 扫描发票 */}
<div onClick={onScan} style={{
display: 'flex', alignItems: 'center', gap: 14,
padding: '14px 16px', borderRadius: 12, cursor: 'pointer',
background: 'rgba(255,107,53,0.04)',
border: '1px solid rgba(255,107,53,0.1)',
transition: 'background 0.15s ease',
}}>
<span style={{ fontSize: 26, flexShrink: 0 }}>📷</span>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 15, fontWeight: 600, color: '#FF6B35' }}>扫描发票</div>
<div style={{ fontSize: 12, color: '#8E8E93', fontWeight: 500, marginTop: 2 }}>拍照识别自动填入</div>
</div>
</div>
{/* 取消 */}
<div onClick={onCancel} style={{
textAlign: 'center', marginTop: 20, cursor: 'pointer',
padding: '10px 0',
}}>
<span style={{ fontSize: 15, fontWeight: 600, color: '#FF6B35' }}>取消</span>
</div>
</div>
</div>
);
}
function BottomTabBar() {
const [activeNav, setActiveNav] = React.useState('home');
const [familyLabel, setFamilyLabel] = React.useState('个人');
const tabs = [
{ key: 'home', icon: '\uD83C\uDFE0', label: '首页' },
{ key: 'stats', icon: '\uD83D\uDCCA', label: '统计' },
{ key: 'profile', icon: '\uD83D\uDC64', label: '我的' },
];
const [plusHover, setPlusHover] = React.useState(false);
React.useEffect(() => {
const handler = (e) => setActiveNav(e.detail);
window.addEventListener('navUpdate', handler);
return () => window.removeEventListener('navUpdate', handler);
}, []);
React.useEffect(() => {
function updateLabel() {
var token = localStorage.getItem('__auth_token');
if (token && window.__authApi) {
var curUser = window.__authApi.getUserByToken(token);
if (curUser && curUser.activeFamilyId) {
var family = window.__authApi.getFamilyById(curUser.activeFamilyId);
setFamilyLabel(family ? family.name : '个人');
return;
}
}
setFamilyLabel('个人');
}
updateLabel();
window.addEventListener('familyModeUpdate', updateLabel);
return () => window.removeEventListener('familyModeUpdate', updateLabel);
}, []);
const handleNav = (key) => {
setActiveNav(key);
window.dispatchEvent(new CustomEvent('navTo', { detail: key }));
};
return (
<div style={{
display: 'flex',
justifyContent: 'center',
padding: '0 16px',
}}>
<div style={{display: 'flex', alignItems: 'center', gap: 10}}>
<div style={{
display: 'inline-flex',
alignItems: 'center',
gap: 4,
background: 'rgba(255,255,255,0.5)',
backdropFilter: 'blur(40px)',
WebkitBackdropFilter: 'blur(40px)',
border: '1px solid rgba(255,255,255,0.6)',
borderRadius: 28,
padding: '6px 24px 6px 20px',
height: 56,
boxShadow: '0 8px 32px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06)',
animation: 'dockFloat 4s ease-in-out infinite',
}}>
<div key="family-selector" onClick={() => window.dispatchEvent(new CustomEvent('navTo', { detail: 'profile' }))} style={{
display: 'flex', alignItems: 'center', gap: 4,
padding: '4px 8px 4px 4px', cursor: 'pointer',
borderRight: '1px solid rgba(60,60,67,0.08)', marginRight: 4,
fontSize: 12, fontWeight: 600, color: '#FF6B35',
whiteSpace: 'nowrap', flexShrink: 0,
}}>
<span style={{fontSize: 14}}>🏠</span>
<span>{familyLabel}</span>
<span style={{fontSize: 10, marginLeft: 2, color: '#C7C7CC'}}></span>
</div>
{tabs.map((tab, i) => (
<div key={i} onClick={() => handleNav(tab.key)} style={{
display: 'flex', flexDirection: 'column', alignItems: 'center',
gap: 2, cursor: 'pointer', userSelect: 'none',
padding: '4px 12px',
transition: 'all 0.2s ease',
}}>
<span style={{ fontSize: 20, lineHeight: 1 }}>{tab.icon}</span>
<span style={{
fontSize: 10, fontWeight: activeNav === tab.key ? 600 : 500,
color: activeNav === tab.key ? '#FF6B35' : '#8E8E93',
letterSpacing: '0.2px',
}}>
{tab.label}
</span>
</div>
))}
</div>
<div
style={{
width: 36,
height: 36,
borderRadius: '50%',
background: 'rgba(255,107,53,0.06)',
backdropFilter: 'blur(40px)',
WebkitBackdropFilter: 'blur(40px)',
border: '1px solid rgba(255,255,255,0.6)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
userSelect: 'none',
transform: plusHover ? 'scale(1.05)' : 'scale(1)',
transition: 'all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)',
boxShadow: '0 8px 32px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06)',
}}
onMouseEnter={() => setPlusHover(true)}
onMouseLeave={() => setPlusHover(false)}
onClick={() => window.dispatchEvent(new CustomEvent('showActionSheet'))}
>
<span style={{ fontSize: 24, fontWeight: 300, color: '#FF6B35', lineHeight: 1 }}>+</span>
</div>
</div>
</div>
);
}
function AddBillScreen({ onClose, ocrData }) {
const [type, setType] = React.useState('expense');
const [amount, setAmount] = React.useState('');
const [category, setCategory] = React.useState('餐饮');
const [note, setNote] = React.useState('');
const [addToFamily, setAddToFamily] = React.useState(false);
React.useEffect(() => {
if (ocrData) {
setAmount(ocrData.amount);
setCategory(ocrData.category);
setNote(ocrData.items);
}
}, [ocrData]);
const merchantName = ocrData ? ocrData.merchant : null;
const categories = type === 'expense' ? CATEGORIES_EXPENSE : CATEGORIES_INCOME;
const accentColor = type === 'expense' ? '#FF6B35' : '#34C759';
const handleNumberInput = (num) => {
if (num === '.' && amount.includes('.')) return;
if (num === '.' && amount === '') { setAmount('0.'); return; }
if (amount.includes('.') && amount.split('.')[1].length >= 2) return;
if (amount === '0' && num !== '.') { setAmount(String(num)); return; }
setAmount(prev => prev + String(num));
};
const handleDelete = () => {
setAmount(prev => prev.slice(0, -1));
};
const handleSave = () => {
if (!amount || parseFloat(amount) <= 0) return;
var token = localStorage.getItem('__auth_token');
var currentUser = null;
if (token && window.__authApi) {
currentUser = window.__authApi.getUserByToken(token);
}
var familyId = null;
if (addToFamily && currentUser) {
familyId = currentUser.activeFamilyId || null;
}
var result = window.__authApi.addBill(currentUser ? currentUser.id : 'unknown', {
type: type,
amount: amount,
category: category,
note: note,
familyId: familyId,
});
if (result.success) {
onClose();
}
};
const displayAmount = amount || '0';
return (
<div style={{
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
background: '#F2F2F7',
zIndex: 100,
display: 'flex', flexDirection: 'column',
animation: 'slideUp 0.35s cubic-bezier(0.16, 1, 0.3, 1)',
}}>
{/* Header */}
<div style={{
padding: '18px 16px 12px',
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
background: 'rgba(255,255,255,0.85)',
backdropFilter: 'blur(30px)',
borderBottom: '1px solid rgba(60,60,67,0.06)',
}}>
<span onClick={onClose} style={{
fontSize: 16, color: '#8E8E93', fontWeight: 500, cursor: 'pointer',
padding: '4px 0',
}}>取消</span>
<span onClick={handleSave} style={{
fontSize: 16, color: accentColor, fontWeight: 600, cursor: 'pointer',
padding: '4px 0',
}}>保存</span>
</div>
{/* Type Toggle */}
<div style={{
display: 'flex', justifyContent: 'center',
padding: '16px 16px 8px',
}}>
<div style={{
background: 'rgba(118,118,128,0.12)',
borderRadius: 10, padding: 2,
display: 'flex', width: 200,
}}>
{['expense', 'income'].map(t => (
<div key={t} onClick={() => { setType(t); setCategory(t === 'expense' ? '餐饮' : '工资'); }}
style={{
flex: 1, padding: '6px 0', textAlign: 'center',
borderRadius: 8, cursor: 'pointer',
background: type === t ? '#fff' : 'transparent',
boxShadow: type === t ? '0 1px 4px rgba(0,0,0,0.06)' : 'none',
fontSize: 14, fontWeight: 600,
color: type === t ? (t === 'expense' ? '#FF6B35' : '#34C759') : '#8E8E93',
transition: 'all 0.2s ease',
}}
>{t === 'expense' ? '支出' : '收入'}</div>
))}
</div>
</div>
{merchantName && (
<div style={{
margin: '8px 16px 0', padding: '10px 14px',
background: '#FFF8F0', borderRadius: 10,
border: '1px solid #FFE4CC',
fontSize: 14, color: '#8B7355',
display: 'flex', alignItems: 'center', gap: 8,
}}>
<span style={{ fontSize: 16 }}>📄</span>
<span>来自发票<strong>{merchantName}</strong></span>
<span style={{ marginLeft: 'auto', fontSize: 12, color: '#FF6B35', fontWeight: 500 }}>OCR识别</span>
</div>
)}
{/* Amount Input Hero */}
<div style={{
padding: '12px 20px 8px',
display: 'flex', alignItems: 'center', justifyContent: 'center',
gap: 4,
}}>
<span style={{ fontSize: 28, fontWeight: 300, color: '#8E8E93', marginTop: 8 }}>¥</span>
<span style={{
fontSize: 48, fontWeight: 700, color: '#1C1C1E',
letterSpacing: '-1px',
fontVariantNumeric: 'tabular-nums',
}}>
{parseFloat(displayAmount).toLocaleString('zh-CN', {minimumFractionDigits: displayAmount.includes('.') ? displayAmount.split('.')[1].length : 0, maximumFractionDigits: 2})}
</span>
</div>
{/* Number Pad */}
<div style={{ padding: '8px 20px 4px', textAlign: 'center' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8, maxWidth: 300, margin: '0 auto' }}>
{[1,2,3,4,5,6,7,8,9,'.',0,'delete'].map((key, i) => (
<div key={i} onClick={() => key === 'delete' ? handleDelete() : handleNumberInput(key)}
style={{
height: 48, borderRadius: 12, cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: key === 'delete' ? 'rgba(255,107,53,0.08)' : 'rgba(255,255,255,0.7)',
backdropFilter: key === 'delete' ? 'none' : 'blur(10px)',
fontSize: key === 'delete' ? 20 : 22,
fontWeight: key === 'delete' ? 400 : 500,
color: key === 'delete' ? '#FF6B35' : '#1C1C1E',
userSelect: 'none',
transition: 'all 0.1s ease',
}}
>
{key === 'delete' ? '\u232B' : key === '.' ? '.' : key}
</div>
))}
</div>
</div>
{/* Category Grid */}
<div style={{ padding: '12px 16px 4px' }}>
<div style={{ fontSize: 13, fontWeight: 600, color: '#8E8E93', marginBottom: 8, paddingLeft: 4 }}>
选择分类
</div>
<div style={{
display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8,
}}>
{categories.map((cat, i) => (
<div key={i} onClick={() => setCategory(cat.name)}
style={{
padding: '10px 6px', borderRadius: 12, cursor: 'pointer',
textAlign: 'center',
background: category === cat.name ? `rgba(255,107,53,0.1)` : 'rgba(255,255,255,0.65)',
backdropFilter: 'blur(20px)',
border: category === cat.name ? `1.5px solid ${accentColor}` : '1px solid rgba(255,255,255,0.5)',
boxShadow: category === cat.name ? `0 2px 12px rgba(255,107,53,0.12)` : '0 1px 4px rgba(0,0,0,0.02)',
transition: 'all 0.2s ease',
}}
>
<div style={{ fontSize: 22, marginBottom: 2 }}>{cat.emoji}</div>
<div style={{
fontSize: 11, fontWeight: category === cat.name ? 600 : 500,
color: category === cat.name ? '#FF6B35' : '#8E8E93',
}}>{cat.name}</div>
</div>
))}
</div>
</div>
{/* Form Details */}
<div style={{ padding: '12px 16px', flex: 1 }}>
<div style={{
background: 'rgba(255,255,255,0.65)',
backdropFilter: 'blur(20px)',
borderRadius: 14,
overflow: 'hidden',
border: '1px solid rgba(255,255,255,0.5)',
boxShadow: '0 2px 12px rgba(0,0,0,0.04)',
}}>
{/* Note */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '12px 14px', borderBottom: '1px solid rgba(60,60,67,0.06)' }}>
<span style={{ fontSize: 16, flexShrink: 0 }}>📝</span>
<input
type="text"
placeholder="添加备注..."
value={note}
onChange={e => setNote(e.target.value)}
style={{
flex: 1, border: 'none', outline: 'none',
fontSize: 14, color: '#1C1C1E',
background: 'transparent',
fontFamily: 'inherit',
}}
/>
</div>
{/* Date */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '12px 14px', borderBottom: '1px solid rgba(60,60,67,0.06)' }}>
<span style={{ fontSize: 16, flexShrink: 0 }}>📅</span>
<span style={{ flex: 1, fontSize: 14, color: '#1C1C1E' }}>{new Date().getFullYear()}{new Date().getMonth() + 1}{new Date().getDate()}</span>
<span style={{ fontSize: 14, color: '#8E8E93' }}></span>
</div>
{/* Account */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '12px 14px', borderBottom: 'none' }}>
<span style={{ fontSize: 16, flexShrink: 0 }}>💳</span>
<span style={{ flex: 1, fontSize: 14, color: '#1C1C1E' }}>微信零钱</span>
<span style={{ fontSize: 14, color: '#8E8E93' }}></span>
</div>
</div>
{/* Family Toggle */}
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '12px 14px', marginTop: 8,
background: 'rgba(255,255,255,0.65)',
backdropFilter: 'blur(20px)',
borderRadius: 14,
border: '1px solid rgba(255,255,255,0.5)',
boxShadow: '0 2px 12px rgba(0,0,0,0.04)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 16 }}>🏠</span>
<span style={{ fontSize: 14, color: '#1C1C1E', fontWeight: 500 }}>添加到家庭账本</span>
</div>
<div onClick={() => setAddToFamily(!addToFamily)} style={{
width: 44, height: 26, borderRadius: 13, cursor: 'pointer',
background: addToFamily ? '#34C759' : 'rgba(120,120,128,0.2)',
padding: 2, transition: 'all 0.2s ease',
}}>
<div style={{
width: 22, height: 22, borderRadius: '50%',
background: '#fff',
boxShadow: '0 1px 4px rgba(0,0,0,0.1)',
marginLeft: addToFamily ? 20 : 0,
transition: 'all 0.2s ease',
}} />
</div>
</div>
</div>
</div>
);
}
/* ===== OCR Scan Page ===== */
function OCRScanPage({ onClose, onCaptured }) {
const [scanning, setScanning] = useState(false);
const [torchOn, setTorchOn] = useState(false);
const hasScanned = useRef(false);
const handleCapture = () => {
if (hasScanned.current) return;
hasScanned.current = true;
setScanning(true);
setTimeout(() => {
const mockResult = {
merchant: '星巴克咖啡',
amount: '68.00',
date: '2026-05-13',
category: '餐饮',
items: '美式咖啡×1、拿铁×1',
};
onCaptured(mockResult);
}, 1800);
};
return (
<div style={{
position: 'absolute', top: -54, left: 0, right: 0, bottom: 0,
background: '#000', zIndex: 100,
display: 'flex', flexDirection: 'column',
animation: 'fadeIn 0.2s ease',
}}>
{/* Top Bar */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '54px 16px 16px' }}>
<div onClick={onClose} style={{ fontSize: 22, color: '#fff', cursor: 'pointer', width: 32, textAlign: 'center' }}></div>
<div style={{ fontSize: 16, fontWeight: 600, color: '#fff' }}>扫描发票</div>
<div onClick={() => setTorchOn(!torchOn)} style={{ fontSize: 18, color: torchOn ? '#FF6B35' : '#fff', cursor: 'pointer', width: 32, textAlign: 'center' }}>🔦</div>
</div>
{/* Viewfinder Area */}
<div style={{ flex: 1, position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<svg width="100%" height="100%" viewBox="0 0 370 460" preserveAspectRatio="xMidYMid meet" style={{ position: 'absolute', top: 0, left: 0 }}>
<defs>
<mask id="scanMask_ocr">
<rect width="370" height="460" fill="white" />
<rect x="45" y="80" width="280" height="180" rx="8" fill="black" />
</mask>
</defs>
<rect width="370" height="460" fill="rgba(0,0,0,0.5)" mask="url(#scanMask_ocr)" />
</svg>
{/* Corner guides */}
<div style={{ position: 'absolute', left: 45, top: 80, width: 280, height: 180, pointerEvents: 'none' }}>
<div style={{ position: 'absolute', left: -2, top: -2 }}><div style={{ width: 24, height: 3, background: '#FF6B35', borderRadius: 2 }} /><div style={{ width: 3, height: 24, background: '#FF6B35', borderRadius: 2 }} /></div>
<div style={{ position: 'absolute', right: -2, top: -2 }}><div style={{ width: 24, height: 3, background: '#FF6B35', borderRadius: 2, marginLeft: 'auto' }} /><div style={{ width: 3, height: 24, background: '#FF6B35', borderRadius: 2, marginLeft: 'auto' }} /></div>
<div style={{ position: 'absolute', left: -2, bottom: -2 }}><div style={{ width: 24, height: 3, background: '#FF6B35', borderRadius: 2 }} /><div style={{ width: 3, height: 24, background: '#FF6B35', borderRadius: 2 }} /></div>
<div style={{ position: 'absolute', right: -2, bottom: -2 }}><div style={{ width: 24, height: 3, background: '#FF6B35', borderRadius: 2, marginLeft: 'auto' }} /><div style={{ width: 3, height: 24, background: '#FF6B35', borderRadius: 2, marginLeft: 'auto' }} /></div>
</div>
<div style={{ position: 'absolute', left: 0, right: 0, textAlign: 'center', top: 280, fontSize: 13, color: 'rgba(255,255,255,0.6)' }}>将发票置于框内</div>
{/* Scanning Overlay */}
{scanning && (
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 2, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.3)', zIndex: 1 }} />
<div style={{ position: 'absolute', left: 45, top: 160, width: 280, height: 2, background: 'linear-gradient(90deg, transparent, #FF6B35, transparent)', boxShadow: '0 0 8px #FF6B35, 0 0 16px #FF6B35', zIndex: 2, animation: 'scanLine 1.8s ease-in-out infinite' }} />
<div style={{ position: 'relative', zIndex: 3, textAlign: 'center' }}>
<div style={{ fontSize: 16, color: 'rgba(255,255,255,0.85)', marginBottom: 12 }}>正在识别发票内容...</div>
<div style={{ display: 'flex', justifyContent: 'center', gap: 6 }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#FF6B35', animation: 'pulse 0.8s ease infinite', animationDelay: '0s' }} />
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#FF6B35', animation: 'pulse 0.8s ease infinite', animationDelay: '0.2s' }} />
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#FF6B35', animation: 'pulse 0.8s ease infinite', animationDelay: '0.4s' }} />
</div>
</div>
</div>
)}
</div>
{/* Bottom Toolbar */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 40, padding: '16px 20px 32px', background: 'rgba(255,255,255,0.08)', backdropFilter: 'blur(30px)' }}>
<div onClick={handleCapture} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, cursor: 'pointer' }}>
<div style={{ width: 44, height: 44, borderRadius: 8, background: 'linear-gradient(135deg, rgba(255,255,255,0.15), rgba(255,255,255,0.05))', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20 }}>🖼</div>
<div style={{ fontSize: 11, color: 'rgba(255,255,255,0.6)' }}>相册</div>
</div>
<div onClick={handleCapture} style={{ width: 72, height: 72, borderRadius: '50%', background: 'rgba(255,255,255,0.15)', border: '4px solid rgba(255,255,255,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', boxShadow: scanning ? 'none' : '0 0 20px rgba(255,107,53,0.5)' }}>
<div style={{ width: 56, height: 56, borderRadius: '50%', background: '#FF6B35' }} />
</div>
<div style={{ width: 44 }} />
</div>
</div>
);
}
/* ===== OCR Result Preview ===== */
function OCRResultPreview({ data, onConfirm, onRetake }) {
return (
<div style={{
position: 'absolute',
top: -54,
left: 0,
right: 0,
bottom: 0,
background: '#F2F2F7',
zIndex: 100,
animation: 'fadeIn 0.2s ease',
display: 'flex',
flexDirection: 'column',
}}>
{/* Top bar */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '54px 16px 12px',
background: '#F2F2F7',
}}>
<button onClick={onRetake} style={{
background: 'transparent',
border: 'none',
color: '#FF6B35',
fontSize: 16,
cursor: 'pointer',
padding: '8px 0',
}}>重新扫描</button>
<div style={{ fontSize: 17, fontWeight: 600, color: '#000' }}>扫描结果</div>
<button onClick={() => onRetake()} style={{
background: 'transparent',
border: 'none',
color: '#FF6B35',
fontSize: 16,
cursor: 'pointer',
padding: '8px 0',
}}>取消</button>
</div>
{/* Content area */}
<div style={{ flex: 1, overflow: 'auto', paddingTop: 8 }}>
<div style={{
margin: 16,
background: '#fff',
borderRadius: 16,
boxShadow: '0 1px 3px rgba(0,0,0,0.08), 0 0 0 0.5px rgba(0,0,0,0.02)',
overflow: 'hidden',
}}>
{/* Header - success badge */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '24px 20px 8px',
gap: 8,
}}>
<div style={{
width: 22,
height: 22,
borderRadius: '50%',
background: '#34C759',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontSize: 13,
fontWeight: 700,
}}></div>
<span style={{ fontSize: 15, color: '#34C759', fontWeight: 500 }}>识别成功</span>
</div>
{/* Fields */}
<div style={{ padding: '8px 20px 20px' }}>
{/* 商家 */}
<div style={{ display: 'flex', alignItems: 'center', padding: '14px 0' }}>
<div style={{ fontSize: 14, color: '#8E8E93', width: 80, flexShrink: 0 }}>商家</div>
<div style={{ flex: 1, textAlign: 'right', fontSize: 16, color: '#000' }}>{data.merchant}</div>
</div>
<div style={{ height: 1, background: '#E5E5E5', marginLeft: 0 }} />
{/* 金额 */}
<div style={{ display: 'flex', alignItems: 'center', padding: '14px 0' }}>
<div style={{ fontSize: 14, color: '#8E8E93', width: 80, flexShrink: 0 }}>金额</div>
<div style={{ flex: 1, textAlign: 'right', fontSize: 16, color: '#FF6B35', fontWeight: 600 }}>¥{data.amount}</div>
</div>
<div style={{ height: 1, background: '#E5E5E5' }} />
{/* 日期 */}
<div style={{ display: 'flex', alignItems: 'center', padding: '14px 0' }}>
<div style={{ fontSize: 14, color: '#8E8E93', width: 80, flexShrink: 0 }}>日期</div>
<div style={{ flex: 1, textAlign: 'right', fontSize: 16, color: '#000' }}>{data.date}</div>
</div>
<div style={{ height: 1, background: '#E5E5E5' }} />
{/* 分类 */}
<div style={{ display: 'flex', alignItems: 'center', padding: '14px 0' }}>
<div style={{ fontSize: 14, color: '#8E8E93', width: 80, flexShrink: 0 }}>分类</div>
<div style={{ flex: 1, textAlign: 'right' }}>
<span style={{
display: 'inline-block',
background: '#FFF0E8',
color: '#FF6B35',
fontSize: 13,
padding: '2px 10px',
borderRadius: 10,
fontWeight: 500,
}}>{data.category}</span>
</div>
</div>
<div style={{ height: 1, background: '#E5E5E5' }} />
{/* 备注 */}
<div style={{ display: 'flex', alignItems: 'center', padding: '14px 0' }}>
<div style={{ fontSize: 14, color: '#8E8E93', width: 80, flexShrink: 0 }}>备注</div>
<div style={{ flex: 1, textAlign: 'right', fontSize: 16, color: '#000' }}>{data.items}</div>
</div>
</div>
</div>
</div>
{/* Bottom buttons */}
<div style={{
padding: '12px 32px',
paddingBottom: 'calc(12px + env(safe-area-inset-bottom, 16px))',
display: 'flex',
flexDirection: 'column',
gap: 12,
}}>
<button onClick={onRetake} style={{
width: '100%',
height: 48,
background: '#fff',
color: '#FF6B35',
border: '1px solid #FF6B35',
borderRadius: 25,
fontSize: 16,
fontWeight: 600,
cursor: 'pointer',
}}>重新拍摄</button>
<button onClick={() => onConfirm(data)} style={{
width: '100%',
height: 48,
background: '#FF6B35',
color: '#fff',
border: 'none',
borderRadius: 25,
fontSize: 16,
fontWeight: 600,
cursor: 'pointer',
}}>确认并入账</button>
</div>
</div>
);
}
/* ===== Placeholder Components (will be replaced) ===== */
function StatisticsContent({ user, activeTab }) {
const [monthOffset, setMonthOffset] = React.useState(0);
const [bills, setBills] = React.useState([]);
const loadBills = React.useCallback(() => {
if (!user) { setBills([]); return; }
if (activeTab === 'family') {
var fResult = window.__authApi.getUserFamilies(user.id);
if (fResult.success && fResult.data.families.length > 0) {
var af = fResult.data.families.find(function(f) { return f.id === user.activeFamilyId; }) || fResult.data.families[0];
var bResult = window.__authApi.getFamilyBills(af.id);
if (bResult.success) setBills(bResult.data.bills);
}
} else {
var result = window.__authApi.getBills(user.id);
if (result.success) setBills(result.data.bills);
}
}, [user, activeTab]);
React.useEffect(function() { loadBills(); }, [loadBills]);
React.useEffect(function() {
var h = function() { loadBills(); };
window.addEventListener('refreshBills', h);
return function() { window.removeEventListener('refreshBills', h); };
}, [loadBills]);
// Date calculations
const now = new Date();
const targetDate = new Date(now.getFullYear(), now.getMonth() - monthOffset, 1);
const targetYear = targetDate.getFullYear();
const targetMonth = targetDate.getMonth();
const monthNames = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
const label = targetYear + '年' + monthNames[targetMonth];
// Filter bills for selected month
const monthBills = bills.filter(function(b) {
if (!b.date) return false;
var parts = b.date.split('-');
if (parts.length < 2) return false;
return parseInt(parts[0]) === targetYear && parseInt(parts[1]) === (targetMonth + 1);
});
const totalIncome = monthBills.filter(function(b) { return b.type === 'income'; }).reduce(function(s, b) { return s + Number(b.amount); }, 0);
const totalExpense = monthBills.filter(function(b) { return b.type === 'expense'; }).reduce(function(s, b) { return s + Number(b.amount); }, 0);
const balance = totalIncome - totalExpense;
// Budget based on actual expense
const budgetUsed = totalExpense;
const budgetTotal = Math.max(totalExpense * 1.5, 5000);
const budgetPercent = budgetTotal > 0 ? Math.round(budgetUsed / budgetTotal * 100) : 0;
const ringRadius = 36;
const ringCirc = 2 * Math.PI * ringRadius;
const ringOffset = ringCirc * (1 - Math.min(budgetPercent, 100) / 100);
// Category breakdown from actual bills
var catColors = { '餐饮':'#FF6B35','交通':'#FF8F4F','购物':'#FFA878','居住':'#FFC4A0','医疗':'#FFDCC4','娱乐':'#FFE8D8','教育':'#FFF0E8','其他':'#FFF8F0', '工资':'#34C759','理财':'#34C759','兼职':'#34C759','红包':'#FF6B35','奖金':'#FF6B35' };
var catMap = {};
monthBills.filter(function(b) { return b.type === 'expense'; }).forEach(function(b) {
if (!catMap[b.category]) catMap[b.category] = { emoji: b.emoji || '📋', name: b.category, amount: 0, color: catColors[b.category] || '#FFE8D8' };
catMap[b.category].amount += Number(b.amount);
});
const categoryBreakdown = Object.keys(catMap).map(function(k) { return catMap[k]; }).sort(function(a, b) { return b.amount - a.amount; });
const maxCatAmount = categoryBreakdown.length > 0 ? categoryBreakdown[0].amount : 1;
// Daily trend
const daysInMonth = new Date(targetYear, targetMonth + 1, 0).getDate();
var dailyMap = {};
for (var d = 1; d <= daysInMonth; d++) { dailyMap[d] = 0; }
monthBills.filter(function(b) { return b.type === 'expense'; }).forEach(function(b) {
var day = parseInt((b.date || '').split('-')[2] || '1');
if (dailyMap[day] !== undefined) dailyMap[day] += Number(b.amount);
});
const dailyTrend = Object.keys(dailyMap).map(function(k) { return { label: k, amount: dailyMap[k] }; });
const maxDaily = Math.max.apply(null, dailyTrend.map(function(d) { return d.amount; }).concat([1]));
// Monthly comparison
var monthlyMap = {};
bills.filter(function(b) { return b.type === 'expense'; }).forEach(function(b) {
if (!b.date) return;
var parts = b.date.split('-');
var key = parseInt(parts[1]) + '月';
monthlyMap[key] = (monthlyMap[key] || 0) + Number(b.amount);
});
var monthlyComparison = [];
for (var i = 5; i >= 0; i--) {
var m = new Date(now.getFullYear(), now.getMonth() - i, 1);
var key = (m.getMonth() + 1) + '月';
monthlyComparison.push({ month: key, amount: monthlyMap[key] || 0 });
}
const maxMonthly = Math.max.apply(null, monthlyComparison.map(function(mc) { return mc.amount; }).concat([1]));
const prevIdx = monthlyComparison.length - 2;
const prevMonthAmount = prevIdx >= 0 ? monthlyComparison[prevIdx].amount : 0;
const changePercent = prevMonthAmount > 0 ? ((totalExpense - prevMonthAmount) / prevMonthAmount * 100).toFixed(1) : '0.0';
const isUp = totalExpense > prevMonthAmount;
const glassCardStyle = { background:'rgba(255,255,255,0.65)',backdropFilter:'blur(30px)',WebkitBackdropFilter:'blur(30px)',borderRadius:16,padding:'12px 8px',textAlign:'center',border:'1px solid rgba(255,255,255,0.8)',boxShadow:'0 8px 32px rgba(255,107,53,0.06), 0 0 0 1px rgba(255,255,255,0.9) inset' };
const arrowStyle = { fontSize:24,cursor:'pointer',color:'#FF6B35',padding:'4px 12px',userSelect:'none',fontWeight:300 };
return (
<div style={{ padding:'16px 16px 80px',overflowY:'auto',maxHeight:'100%',animation:'fadeSlideUp 0.35s ease' }}>
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:16}}>
<span onClick={function() { setMonthOffset(function(o) { return Math.min(o + 1, 0); }); }} style={Object.assign({}, arrowStyle, { visibility: monthOffset < 0 ? 'visible' : 'hidden' })}></span>
<span style={{fontSize:16,fontWeight:700,color:'#1C1C1E'}}>{label}</span>
<span onClick={function() { setMonthOffset(function(o) { return o - 1; }); }} style={arrowStyle}></span>
</div>
<div style={{display:'grid',gridTemplateColumns:'1fr 1fr 1fr',gap:8,marginBottom:16}}>
{[{label:'收入',value:'¥'+totalIncome.toLocaleString(),color:'#34C759'},{label:'支出',value:'¥'+totalExpense.toLocaleString(),color:'#FF3B30'},{label:'结余',value:'¥'+balance.toLocaleString(),color:'#FF6B35'}].map(function(item,i) { return (
<div key={i} style={glassCardStyle}>
<div style={{fontSize:11,color:'#8E8E93',fontWeight:500,marginBottom:4}}>{item.label}</div>
<div style={{fontSize:14,fontWeight:700,color:item.color}}>{item.value}</div>
</div>
);})}
</div>
<div style={Object.assign({}, glassCardStyle, {display:'flex',flexDirection:'column',alignItems:'center',padding:'20px 16px',marginBottom:16})}>
<div style={{position:'relative',width:90,height:90}}>
<svg width="90" height="90" viewBox="0 0 90 90">
<circle cx="45" cy="45" r={ringRadius} fill="none" stroke="rgba(142,142,147,0.12)" strokeWidth="6"/>
<circle cx="45" cy="45" r={ringRadius} fill="none" stroke="#FF6B35" strokeWidth="6" strokeDasharray={ringCirc} strokeDashoffset={ringOffset} strokeLinecap="round" transform="rotate(-90 45 45)" style={{transition:'stroke-dashoffset 1s ease'}}/>
</svg>
<div style={{position:'absolute',top:0,left:0,right:0,bottom:0,display:'flex',alignItems:'center',justifyContent:'center'}}>
<span style={{fontSize:20,fontWeight:700,color:'#FF6B35'}}>{budgetPercent}%</span>
</div>
</div>
<div style={{fontSize:12,color:'#8E8E93',fontWeight:500,marginTop:8}}>预算 ¥{budgetTotal.toLocaleString()} · 已用 ¥{budgetUsed.toLocaleString()}</div>
</div>
<div style={{marginBottom:16}}>
<div style={{fontSize:13,fontWeight:600,color:'#1C1C1E',marginBottom:10,paddingLeft:4}}>支出分类排行</div>
{categoryBreakdown.length === 0 ? (
<div style={{textAlign:'center',color:'#8E8E93',fontSize:13,padding:'20px 0'}}>暂无支出数据</div>
) : categoryBreakdown.map(function(cat,i) { return (
<div key={i} style={{display:'flex',alignItems:'center',gap:10,marginBottom:8}}>
<span style={{fontSize:18,width:28,textAlign:'center'}}>{cat.emoji}</span>
<span style={{fontSize:12,fontWeight:600,color:'#1C1C1E',width:32,flexShrink:0}}>{cat.name}</span>
<div style={{flex:1,height:6,background:'rgba(142,142,147,0.08)',borderRadius:3,overflow:'hidden'}}>
<div style={{width:Math.round(cat.amount/maxCatAmount*100)+'%',height:'100%',background:cat.color,borderRadius:3,animation:'barGrow 0.6s ease '+(i*0.05)+'s both'}}/>
</div>
<span style={{fontSize:12,fontWeight:600,color:'#1C1C1E',width:68,textAlign:'right',flexShrink:0}}>¥{cat.amount.toLocaleString()}</span>
</div>
);})}
</div>
<div style={{marginBottom:16}}>
<div style={{fontSize:13,fontWeight:600,color:'#1C1C1E',marginBottom:10,paddingLeft:4}}>每日消费趋势</div>
<div style={Object.assign({}, glassCardStyle, {padding:'16px 12px',display:'flex',alignItems:'flex-end',gap:2,height:140})}>
{dailyTrend.map(function(d,i) { return (
<div key={i} style={{flex:1,display:'flex',flexDirection:'column',alignItems:'center',height:'100%',justifyContent:'flex-end'}}>
<div style={{width:'100%',maxWidth:18,height:Math.max(d.amount/maxDaily*80, d.amount > 0 ? 4 : 0)+'px',background:d.amount > 0 ? 'linear-gradient(180deg, #FF6B35, #FF9F5B)' : 'transparent',borderRadius:'4px 4px 0 0',transition:'height 0.5s ease'}}/>
<span style={{fontSize:8,color:'#8E8E93',marginTop:4,fontWeight:500}}>{d.label}</span>
</div>
);})}
</div>
</div>
<div style={{marginBottom:24}}>
<div style={{fontSize:13,fontWeight:600,color:'#1C1C1E',marginBottom:10,paddingLeft:4}}>月度对比</div>
{monthlyComparison.map(function(mc,i) {
var isCurrent = i === monthlyComparison.length - 1;
return (
<div key={i} style={Object.assign({}, glassCardStyle, {display:'flex',alignItems:'center',gap:12,padding:'12px 14px',marginBottom:6,borderLeft: isCurrent ? '3px solid #FF6B35' : '1px solid rgba(255,255,255,0.8)'})}>
<span style={{fontSize:13,fontWeight:600,color:isCurrent?'#FF6B35':'#1C1C1E',width:36}}>{mc.month}</span>
<div style={{flex:1,height:6,background:'rgba(142,142,147,0.08)',borderRadius:3,overflow:'hidden'}}>
<div style={{width:Math.round(mc.amount/maxMonthly*100)+'%',height:'100%',background:isCurrent?'linear-gradient(90deg, #FF6B35, #FF9F5B)':'rgba(142,142,147,0.2)',borderRadius:3}}/>
</div>
<span style={{fontSize:13,fontWeight:600,color:isCurrent?'#FF6B35':'#1C1C1E',width:60,textAlign:'right',flexShrink:0}}>¥{mc.amount.toLocaleString()}</span>
{isCurrent && (
<span style={{fontSize:11,fontWeight:600,color: isUp ? '#FF3B30' : '#34C759',width:50,textAlign:'right'}}>{isUp ? '\u2191' : '\u2193'}{Math.abs(Number(changePercent))}%</span>
)}
</div>
);
})}
</div>
</div>
);
}
function ProfileContent({ user, onLogin, onLogout, onUserInfo, onFamilyManagement }) {
const settingsGroups = [
{
title: '账本管理',
items: [
{ emoji: '🏠', name: '我的家庭' },
{ emoji: '\uD83D\uDCD2', name: '账本管理' },
{ emoji: '\uD83D\uDCB0', name: '预算设置' },
{ emoji: '\uD83C\uDFF7\uFE0F', name: '分类管理' },
],
},
{
title: '系统设置',
items: [
{ emoji: '\uD83E\uDD16', name: 'AI 设置' },
{ emoji: '\uD83D\uDCE4', name: '数据导出' },
{ emoji: '\uD83D\uDD14', name: '提醒设置' },
],
},
{
title: '关于',
items: [
{ emoji: '\u2139\uFE0F', name: '关于' },
{ emoji: '\uD83D\uDCAC', name: '帮助与反馈' },
],
},
];
return (
<div style={{ padding: '16px 16px 80px', overflowY: 'auto', maxHeight: '100%', animation: 'fadeSlideUp 0.35s ease' }}>
{/* 1. User Info Card */}
{!user ? (
<div onClick={onLogin} style={{
background: 'rgba(255,255,255,0.65)', backdropFilter: 'blur(30px)',
borderRadius: 20, padding: '24px 20px', marginBottom: 16, cursor: 'pointer',
border: '1px solid rgba(255,255,255,0.8)',
boxShadow: '0 8px 32px rgba(255,107,53,0.08), 0 0 0 1px rgba(255,255,255,0.9) inset',
display: 'flex', alignItems: 'center', gap: 16,
}}>
<div style={{
width: 56, height: 56, borderRadius: '50%',
background: 'rgba(142,142,147,0.12)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 24, flexShrink: 0,
}}>👤</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 700, color: '#1C1C1E' }}>登录 / 注册</div>
<div style={{ fontSize: 12, color: '#8E8E93', fontWeight: 500, marginTop: 2 }}>
登录后可同步数据管理家庭账本
</div>
</div>
</div>
) : (
<div onClick={onUserInfo} style={{
background: 'rgba(255,255,255,0.65)', backdropFilter: 'blur(30px)',
borderRadius: 20, padding: '24px 20px', marginBottom: 16, cursor: 'pointer',
border: '1px solid rgba(255,255,255,0.8)',
boxShadow: '0 8px 32px rgba(255,107,53,0.08), 0 0 0 1px rgba(255,255,255,0.9) inset',
display: 'flex', alignItems: 'center', gap: 16,
}}>
<div style={{
width: 56, height: 56, borderRadius: '50%',
background: 'linear-gradient(135deg, #FF6B35, #FF9F5B)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 22, fontWeight: 700, color: '#fff',
flexShrink: 0,
boxShadow: '0 4px 16px rgba(255,107,53,0.25)',
}}>{user.nickname ? user.nickname.charAt(0) : '?'}</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 700, color: '#1C1C1E' }}>{user.nickname || '用户'}</div>
<div style={{ fontSize: 12, color: '#8E8E93', fontWeight: 500, marginTop: 2 }}>
{'ID: ' + (user.id || '') + ' · ' + (user.joinDate || '')}
</div>
<div style={{ fontSize: 12, color: (user.email ? '#8E8E93' : '#FF6B35'), fontWeight: 500, marginTop: 2 }}>
{user.email ? '邮箱已绑定' : '邮箱未绑定'}
</div>
<div style={{ fontSize: 12, color: '#FF6B35', fontWeight: 600, marginTop: 4 }}>
{(function() {
try {
var families = window.__authApi.getUserFamilies(user.id);
if (families.success && families.data.families.length > 0) {
var active = families.data.families.find(function(f) { return f.id === user.activeFamilyId; });
return (active ? active.name : families.data.families[0].name) + ' 🏠';
}
} catch(e) {}
return '暂无家庭 🏠';
})()}
</div>
</div>
<div style={{ fontSize: 18, color: '#C7C7CC' }}></div>
</div>
)}
{/* 2. Family Members Row */}
{user && (function() {
try {
var families = window.__authApi.getUserFamilies(user.id);
if (families.success && families.data.families.length > 0) {
var activeFamily = families.data.families.find(function(f) { return f.id === user.activeFamilyId; }) || families.data.families[0];
var members = activeFamily.members || [];
return React.createElement('div', { style: { marginBottom: 16 }},
React.createElement('div', { style: { fontSize: 12, fontWeight: 600, color: '#8E8E93', marginBottom: 8, paddingLeft: 4 }},
activeFamily.name + ' · 家庭成员'
),
React.createElement('div', { style: {
background: 'rgba(255,255,255,0.65)', backdropFilter: 'blur(30px)',
borderRadius: 16, padding: '14px 16px',
border: '1px solid rgba(255,255,255,0.8)',
boxShadow: '0 8px 32px rgba(255,107,53,0.06), 0 0 0 1px rgba(255,255,255,0.9) inset',
display: 'flex', gap: 12, overflowX: 'auto',
}},
members.map(function(m, i) {
var isYou = m.userId === user.id;
var initial = m.nickname ? m.nickname.charAt(0) : '?';
return React.createElement('div', { key: i, style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, minWidth: 48 }},
React.createElement('div', { style: {
width: 44, height: 44, borderRadius: '50%',
background: isYou ? 'linear-gradient(135deg, #FF6B35, #FF9F5B)' : 'rgba(142,142,147,0.1)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 20, fontWeight: 600, color: isYou ? '#fff' : '#8E8E93',
boxShadow: isYou ? '0 2px 8px rgba(255,107,53,0.2)' : 'none',
}}, initial),
React.createElement('span', { style: {
fontSize: 10, fontWeight: isYou ? 600 : 500,
color: isYou ? '#FF6B35' : '#8E8E93',
whiteSpace: 'nowrap',
}}, m.nickname + (isYou ? '(你)' : ''))
);
}),
React.createElement('div', { onClick: function() { if (onFamilyManagement) onFamilyManagement(); }, style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, minWidth: 48 }},
React.createElement('div', { style: {
width: 44, height: 44, borderRadius: '50%',
border: '1.5px dashed #FF6B35',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 20, color: '#FF6B35', cursor: 'pointer',
}}, '+'),
React.createElement('span', { style: { fontSize: 10, fontWeight: 500, color: '#FF6B35' }}, '邀请')
)
)
);
}
} catch(e) {}
return null;
})()}
{/* 3. Settings Groups */}
{settingsGroups.map((group, gi) => (
<div key={gi} style={{ marginBottom: 16 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: '#8E8E93', marginBottom: 8, paddingLeft: 4, textTransform: 'uppercase', letterSpacing: '0.5px' }}>
{group.title}
</div>
<div style={{
background: 'rgba(255,255,255,0.65)', backdropFilter: 'blur(30px)',
borderRadius: 14, overflow: 'hidden',
border: '1px solid rgba(255,255,255,0.8)',
boxShadow: '0 8px 32px rgba(255,107,53,0.06), 0 0 0 1px rgba(255,255,255,0.9) inset',
}}>
{group.items.map((item, ii) => (
<div key={ii} onClick={function() {
if (user && item.name === '我的家庭' && onFamilyManagement) {
onFamilyManagement();
}
}} style={{
display: 'flex', alignItems: 'center', gap: 12,
padding: '14px 16px',
borderBottom: ii < group.items.length - 1 ? '1px solid rgba(60,60,67,0.06)' : 'none',
cursor: 'pointer',
}}>
<span style={{ fontSize: 18, width: 28, textAlign: 'center' }}>{item.emoji}</span>
<span style={{ flex: 1, fontSize: 14, fontWeight: 500, color: '#1C1C1E' }}>{item.name}</span>
<span style={{ fontSize: 14, color: '#C7C7CC' }}></span>
</div>
))}
</div>
</div>
))}
{/* 4. Logout */}
{user && (
<div style={{ textAlign: 'center', padding: '8px 0 16px' }}>
<span onClick={onLogout} style={{
fontSize: 14, fontWeight: 600, color: '#FF3B30', cursor: 'pointer',
padding: '10px 40px', display: 'inline-block',
border: '1px solid rgba(255,59,48,0.2)', borderRadius: 12,
background: 'rgba(255,59,48,0.04)',
}}>退出登录</span>
</div>
)}
</div>
);
}
/* ===== Mock Auth API ===== */
window.__authApi = (function() {
const USERS_KEY = '__mock_users';
const TOKEN_KEY = '__auth_token';
function getUsers() {
try { return JSON.parse(localStorage.getItem(USERS_KEY) || '{}'); }
catch(e) { return {}; }
}
function saveUsers(users) {
localStorage.setItem(USERS_KEY, JSON.stringify(users));
}
function generateToken(phone) {
return btoa(phone + ':' + Date.now());
}
function getUserFromToken(token) {
try {
const decoded = atob(token);
const phone = decoded.split(':')[0];
const users = getUsers();
if (users[phone]) {
const { password, ...user } = users[phone];
if (!user.familyIds) user.familyIds = [];
if (!user.activeFamilyId) user.activeFamilyId = null;
return user;
}
} catch(e) {}
return null;
}
// 预设账号13800138000 / 123456
function ensureDefaultUser() {
const users = getUsers();
if (!users['13800138000']) {
users['13800138000'] = {
id: '1001',
phone: '13800138000',
password: '123456',
nickname: '小明',
email: null,
familyIds: [],
activeFamilyId: null,
joinDate: '2026-03',
};
saveUsers(users);
}
}
ensureDefaultUser();
// Auto-create family for preset user
(function autoCreateFamily() {
var presetFamilies = JSON.parse(localStorage.getItem('__mock_families') || '{}');
var presetHasFamily = false;
for (var fid in presetFamilies) {
if (presetFamilies[fid].members && presetFamilies[fid].members.some(function(m) { return m.userId === '1001'; })) {
presetHasFamily = true;
break;
}
}
if (!presetHasFamily) {
var users = getUsers();
var presetUser = users['13800138000'];
if (presetUser) {
var familyId = 'f_' + Date.now();
var chars = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
var code = '';
for (var i = 0; i < 6; i++) code += chars.charAt(Math.floor(Math.random() * chars.length));
presetFamilies[familyId] = {
id: familyId, name: '小明的家', inviteCode: code,
createdBy: '1001', createdAt: '2026-03',
members: [{ userId: '1001', nickname: '小明', role: 'owner', joinedAt: '2026-03' }]
};
localStorage.setItem('__mock_families', JSON.stringify(presetFamilies));
presetUser.familyIds = [familyId];
presetUser.activeFamilyId = familyId;
saveUsers(users);
}
}
})();
return {
login(phone, password) {
return new Promise((resolve) => {
setTimeout(() => {
const users = getUsers();
const user = users[phone];
if (!user) {
resolve({ success: false, error: '该手机号未注册' });
return;
}
if (user.password !== password) {
resolve({ success: false, error: '密码错误' });
return;
}
const token = generateToken(phone);
localStorage.setItem(TOKEN_KEY, token);
const { password: pwd, ...safeUser } = user;
resolve({ success: true, data: { token, user: safeUser } });
}, 400);
});
},
register(phone, password, nickname) {
return new Promise((resolve) => {
setTimeout(() => {
const users = getUsers();
if (users[phone]) {
resolve({ success: false, error: '该手机号已注册' });
return;
}
const newUser = {
id: String(Date.now()).slice(-6),
phone,
password,
nickname: nickname || '用户' + phone.slice(-4),
email: null,
familyIds: [],
activeFamilyId: null,
joinDate: new Date().toISOString().slice(0, 7).replace('-', '年') + '月',
};
users[phone] = newUser;
saveUsers(users);
const token = generateToken(phone);
localStorage.setItem(TOKEN_KEY, token);
const { password: pwd, ...safeUser } = newUser;
resolve({ success: true, data: { token, user: safeUser } });
}, 500);
});
},
logout() {
localStorage.removeItem(TOKEN_KEY);
},
getUserByToken(token) {
if (!token) return null;
return getUserFromToken(token);
},
bindEmail(userId, email) {
return new Promise((resolve) => {
setTimeout(() => {
const users = getUsers();
for (const phone in users) {
if (users[phone].id === userId) {
users[phone].email = email;
saveUsers(users);
resolve({ success: true });
return;
}
}
resolve({ success: false, error: '用户不存在' });
}, 300);
});
},
createFamily(userId, name) {
return new Promise((resolve) => {
setTimeout(() => {
if (!userId || !name) { resolve({ success: false, error: '参数不完整' }); return; }
var families = JSON.parse(localStorage.getItem('__mock_families') || '{}');
var inviteCode = '';
var chars = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
for (var i = 0; i < 6; i++) inviteCode += chars.charAt(Math.floor(Math.random() * chars.length));
var familyId = 'f_' + Date.now();
var family = { id: familyId, name: name, inviteCode: inviteCode, createdBy: userId, createdAt: new Date().toISOString().slice(0, 10), members: [{ userId: userId, nickname: '', role: 'owner', joinedAt: new Date().toISOString().slice(0, 10) }] };
var users = JSON.parse(localStorage.getItem('__mock_users') || '{}');
for (var phone in users) { if (users[phone].id === userId) { family.members[0].nickname = users[phone].nickname || '用户'; break; } }
families[familyId] = family;
localStorage.setItem('__mock_families', JSON.stringify(families));
for (var p in users) { if (users[p].id === userId) { users[p].familyIds = users[p].familyIds || []; if (users[p].familyIds.indexOf(familyId) === -1) users[p].familyIds.push(familyId); users[p].activeFamilyId = users[p].activeFamilyId || familyId; localStorage.setItem('__mock_users', JSON.stringify(users)); break; } }
resolve({ success: true, data: { family: family } });
}, 400);
});
},
joinFamily(userId, inviteCode) {
return new Promise((resolve) => {
setTimeout(() => {
if (!inviteCode || inviteCode.length !== 6) { resolve({ success: false, error: '邀请码无效' }); return; }
var families = JSON.parse(localStorage.getItem('__mock_families') || '{}');
var foundFamily = null;
for (var fid in families) { if (families[fid].inviteCode === inviteCode.toUpperCase()) { foundFamily = families[fid]; break; } }
if (!foundFamily) { resolve({ success: false, error: '邀请码无效' }); return; }
if (foundFamily.members.some(function(m) { return m.userId === userId; })) { resolve({ success: false, error: '你已在该家庭中' }); return; }
var nickname = '用户', users = JSON.parse(localStorage.getItem('__mock_users') || '{}');
for (var phone in users) { if (users[phone].id === userId) { nickname = users[phone].nickname || '用户'; users[phone].familyIds = users[phone].familyIds || []; if (users[phone].familyIds.indexOf(foundFamily.id) === -1) users[phone].familyIds.push(foundFamily.id); users[phone].activeFamilyId = foundFamily.id; localStorage.setItem('__mock_users', JSON.stringify(users)); break; } }
foundFamily.members.push({ userId: userId, nickname: nickname, role: 'member', joinedAt: new Date().toISOString().slice(0, 10) });
localStorage.setItem('__mock_families', JSON.stringify(families));
resolve({ success: true, data: { family: foundFamily } });
}, 400);
});
},
leaveFamily(userId, familyId) {
return new Promise((resolve) => {
setTimeout(function() {
var families = JSON.parse(localStorage.getItem('__mock_families') || '{}');
var family = families[familyId];
if (!family) { resolve({ success: false, error: '家庭不存在' }); return; }
var member = null;
for (var mi = 0; mi < family.members.length; mi++) { if (family.members[mi].userId === userId) { member = family.members[mi]; break; } }
if (!member) { resolve({ success: false, error: '你不在该家庭中' }); return; }
if (member.role === 'owner') { resolve({ success: false, error: '创建者不能退出家庭' }); return; }
family.members = family.members.filter(function(m) { return m.userId !== userId; });
localStorage.setItem('__mock_families', JSON.stringify(families));
var users = JSON.parse(localStorage.getItem('__mock_users') || '{}');
for (var p in users) { if (users[p].id === userId) { users[p].familyIds = (users[p].familyIds || []).filter(function(id) { return id !== familyId; }); if (users[p].activeFamilyId === familyId) users[p].activeFamilyId = users[p].familyIds.length > 0 ? users[p].familyIds[0] : null; localStorage.setItem('__mock_users', JSON.stringify(users)); break; } }
resolve({ success: true });
}, 300);
});
},
getUserFamilies(userId) {
var families = JSON.parse(localStorage.getItem('__mock_families') || '{}');
var result = [];
for (var fid in families) { if (families[fid].members && families[fid].members.some(function(m) { return m.userId === userId; })) result.push(families[fid]); }
return { success: true, data: { families: result } };
},
switchActiveFamily(userId, familyId) {
var users = JSON.parse(localStorage.getItem('__mock_users') || '{}');
for (var p in users) { if (users[p].id === userId) { users[p].activeFamilyId = familyId; localStorage.setItem('__mock_users', JSON.stringify(users)); return { success: true }; } }
return { success: false, error: '用户不存在' };
},
getFamilyById(familyId) {
var families = JSON.parse(localStorage.getItem('__mock_families') || '{}');
return families[familyId] || null;
},
// --- Bill methods ---
// Seed data migration: called once on first bill API access
_ensureBillSeedData: function() {
var key = '__mock_bills_seeded';
if (localStorage.getItem(key)) return;
var bills = JSON.parse(localStorage.getItem('__mock_bills') || '[]');
if (bills.length > 0) return;
var seedTransactions = [
{ emoji: '\uD83C\uDF5C', name: '早餐', category: '餐饮', amount: -18.00 },
{ emoji: '\uD83D\uDE95', name: '打车上班', category: '交通', amount: -25.00 },
{ emoji: '\u2615', name: '星巴克', category: '餐饮', amount: -38.00 },
{ emoji: '\uD83D\uDCBC', name: '工资到账', category: '工资', amount: 8000.00 },
{ emoji: '\uD83D\uDED2', name: '永辉超市', category: '购物', amount: -156.80 },
{ emoji: '\uD83D\uDCE6', name: '快递运费', category: '其他', amount: -12.00 },
{ emoji: '\uD83D\uDC8A', name: '药店', category: '医疗', amount: -89.00 },
{ emoji: '\uD83C\uDF72', name: '晚餐外卖', category: '餐饮', amount: -42.00 },
];
var times = ['08:15','09:30','10:45','10:00','12:20','14:00','16:30','19:00'];
seedTransactions.forEach(function(t, i) {
bills.push({
id: 'b_seed_' + i,
userId: '1001',
familyId: null,
type: t.amount > 0 ? 'income' : 'expense',
amount: Math.abs(t.amount),
category: t.category,
note: t.name,
emoji: t.emoji,
time: times[i] || '12:00',
date: '2026-05-12',
createdAt: '2026-05-12T' + (times[i] || '12:00') + ':00.000Z',
});
});
localStorage.setItem('__mock_bills', JSON.stringify(bills));
localStorage.setItem(key, 'true');
},
addBill(userId, billData) {
this._ensureBillSeedData();
var bills = JSON.parse(localStorage.getItem('__mock_bills') || '[]');
var categoryMap = {
'餐饮': '\uD83C\uDF5C', '交通': '\uD83D\uDE95', '购物': '\uD83D\uDED2',
'居住': '\uD83C\uDFE0', '医疗': '\uD83D\uDC8A', '娱乐': '\uD83C\uDFAE',
'教育': '\uD83D\uDCDA', '其他': '\uD83D\uDCE6',
'工资': '\uD83D\uDCB0', '理财': '\uD83D\uDCC8', '兼职': '\uD83C\uDF93',
'红包': '\uD83E\uDD11', '奖金': '\uD83C\uDFC6',
};
var now = new Date();
var h = String(now.getHours()).padStart(2, '0');
var m = String(now.getMinutes()).padStart(2, '0');
var y = now.getFullYear();
var mo = String(now.getMonth() + 1).padStart(2, '0');
var d = String(now.getDate()).padStart(2, '0');
var bill = {
id: 'b_' + Date.now(),
userId: userId,
familyId: billData.familyId || null,
type: billData.type || 'expense',
amount: Number(billData.amount) || 0,
category: billData.category || '其他',
note: billData.note || '',
emoji: categoryMap[billData.category] || '\uD83D\uDCE6',
time: h + ':' + m,
date: y + '-' + mo + '-' + d,
createdAt: now.toISOString(),
};
bills.unshift(bill);
localStorage.setItem('__mock_bills', JSON.stringify(bills));
return { success: true, data: { bill: bill } };
},
getBills(userId) {
this._ensureBillSeedData();
var bills = JSON.parse(localStorage.getItem('__mock_bills') || '[]');
var userBills = bills.filter(function(b) { return b.userId === userId && !b.familyId; });
return { success: true, data: { bills: userBills } };
},
getFamilyBills(familyId) {
this._ensureBillSeedData();
var bills = JSON.parse(localStorage.getItem('__mock_bills') || '[]');
var familyBills = bills.filter(function(b) { return b.familyId === familyId; });
return { success: true, data: { bills: familyBills } };
},
};
})();
/* ===== LoginPage ===== */
function LoginPage({ onLoginSuccess, onGoRegister, onClose }) {
var _useState = useState(''), phone = _useState[0], setPhone = _useState[1];
var _useState2 = useState(''), password = _useState2[0], setPassword = _useState2[1];
var _useState3 = useState(''), error = _useState3[0], setError = _useState3[1];
var _useState4 = useState(false), loading = _useState4[0], setLoading = _useState4[1];
var handleLogin = function() {
setError('');
if (!phone || phone.length !== 11) { setError('请输入正确的手机号'); return; }
if (!password || password.length < 6) { setError('密码至少6位'); return; }
setLoading(true);
window.__authApi.login(phone, password).then(function(result) {
setLoading(false);
if (result.success) {
onLoginSuccess(result.data.token, result.data.user);
} else {
setError(result.error);
}
});
};
return React.createElement('div', { style: {
position: 'absolute', top: -54, left: 0, right: 0, bottom: 0,
background: '#F2F2F7', zIndex: 100,
display: 'flex', flexDirection: 'column',
animation: 'fadeIn 0.2s ease',
}},
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '54px 16px 16px' }},
React.createElement('div', { onClick: onClose, style: { fontSize: 20, color: '#007AFF', cursor: 'pointer', fontWeight: 500 }}, '\u53D6\u6D88'),
React.createElement('div', { style: { fontSize: 17, fontWeight: 600, color: '#000' }}, '\u767B\u5F55'),
React.createElement('div', { style: { width: 40 }})
),
React.createElement('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '0 32px', gap: 20 }},
error ? React.createElement('div', { style: { fontSize: 14, color: '#FF3B30', textAlign: 'center', padding: '8px 0' }}, error) : null,
React.createElement('div', { style: { background: 'rgba(255,255,255,0.8)', backdropFilter: 'blur(30px)', borderRadius: 14, padding: '0 16px', height: 48, display: 'flex', alignItems: 'center', gap: 8, border: '1px solid rgba(255,255,255,0.9)', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }},
React.createElement('span', { style: { fontSize: 16, flexShrink: 0 }}, '\uD83D\uDCDE'),
React.createElement('input', { type: 'tel', placeholder: '\u8BF7\u8F93\u5165\u624B\u673A\u53F7', value: phone, onChange: function(e) { setPhone(e.target.value.replace(/\D/g, '').slice(0, 11)); setError(''); }, style: { border: 'none', outline: 'none', flex: 1, fontSize: 16, background: 'transparent', color: '#000', fontFamily: 'inherit' }})
),
React.createElement('div', { style: { background: 'rgba(255,255,255,0.8)', backdropFilter: 'blur(30px)', borderRadius: 14, padding: '0 16px', height: 48, display: 'flex', alignItems: 'center', gap: 8, border: '1px solid rgba(255,255,255,0.9)', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }},
React.createElement('span', { style: { fontSize: 16, flexShrink: 0 }}, '\uD83D\uDD12'),
React.createElement('input', { type: 'password', placeholder: '\u8BF7\u8F93\u5165\u5BC6\u7801', value: password, onChange: function(e) { setPassword(e.target.value); setError(''); }, style: { border: 'none', outline: 'none', flex: 1, fontSize: 16, background: 'transparent', color: '#000', fontFamily: 'inherit' }})
),
React.createElement('button', { onClick: handleLogin, disabled: loading, style: { width: '100%', height: 50, background: loading ? '#FF9F5B' : '#FF6B35', color: '#fff', border: 'none', borderRadius: 25, fontSize: 17, fontWeight: 600, cursor: loading ? 'not-allowed' : 'pointer', boxShadow: '0 4px 16px rgba(255,107,53,0.3)' }}, loading ? '\u767B\u5F55\u4E2D...' : '\u767B \u5F55'),
React.createElement('div', { style: { textAlign: 'center' }},
React.createElement('span', { onClick: onGoRegister, style: { fontSize: 14, color: '#FF6B35', fontWeight: 500, cursor: 'pointer' }}, '\u6CA1\u6709\u8D26\u53F7\uFF1F\u7ACB\u5373\u6CE8\u518C')
)
)
);
}
/* ===== RegisterPage ===== */
function RegisterPage({ onRegisterSuccess, onGoLogin, onClose }) {
var _useState = useState(''), phone = _useState[0], setPhone = _useState[1];
var _useState2 = useState(''), password = _useState2[0], setPassword = _useState2[1];
var _useState3 = useState(''), nickname = _useState3[0], setNickname = _useState3[1];
var _useState4 = useState(''), error = _useState4[0], setError = _useState4[1];
var _useState5 = useState(false), loading = _useState5[0], setLoading = _useState5[1];
var handleRegister = function() {
setError('');
if (!nickname || nickname.trim() === '') { setError('请输入昵称'); return; }
if (!phone || phone.length !== 11 || !phone.startsWith('1')) { setError('请输入正确的手机号'); return; }
if (!password || password.length < 6) { setError('密码至少6位'); return; }
setLoading(true);
window.__authApi.register(phone, password, nickname.trim()).then(function(result) {
setLoading(false);
if (result.success) {
onRegisterSuccess(result.data.token, result.data.user);
} else {
setError(result.error);
}
});
};
return React.createElement('div', { style: {
position: 'absolute', top: -54, left: 0, right: 0, bottom: 0,
background: '#F2F2F7', zIndex: 100,
display: 'flex', flexDirection: 'column',
animation: 'fadeIn 0.2s ease',
}},
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '54px 16px 16px' }},
React.createElement('div', { onClick: onClose, style: { fontSize: 20, color: '#007AFF', cursor: 'pointer', fontWeight: 500 }}, '\u53D6\u6D88'),
React.createElement('div', { style: { fontSize: 17, fontWeight: 600, color: '#000' }}, '\u6CE8\u518C'),
React.createElement('div', { style: { width: 40 }})
),
React.createElement('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '0 32px', gap: 16 }},
error ? React.createElement('div', { style: { fontSize: 14, color: '#FF3B30', textAlign: 'center', padding: '8px 0' }}, error) : null,
React.createElement('div', { style: { textAlign: 'center', marginBottom: 12 }},
React.createElement('div', { style: { fontSize: 24, fontWeight: 700, color: '#1C1C1E' }}, '\u521B\u5EFA\u4F60\u7684\u8D26\u53F7'),
React.createElement('div', { style: { fontSize: 14, color: '#8E8E93', marginTop: 6 }}, '\u52A0\u5165\u5BB6\u5EAD\u8BB0\u8D26\uFF0C\u8F7B\u677E\u7BA1\u7406\u8D22\u52A1')
),
React.createElement('div', { style: { background: 'rgba(255,255,255,0.8)', backdropFilter: 'blur(30px)', borderRadius: 14, padding: '0 16px', height: 48, display: 'flex', alignItems: 'center', gap: 8, border: '1px solid rgba(255,255,255,0.9)', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }},
React.createElement('span', { style: { fontSize: 16, flexShrink: 0 }}, '\uD83D\uDC64'),
React.createElement('input', { type: 'text', placeholder: '\u8BF7\u8F93\u5165\u6635\u79F0', value: nickname, onChange: function(e) { setNickname(e.target.value); setError(''); }, style: { border: 'none', outline: 'none', flex: 1, fontSize: 16, background: 'transparent', color: '#000', fontFamily: 'inherit' }})
),
React.createElement('div', { style: { background: 'rgba(255,255,255,0.8)', backdropFilter: 'blur(30px)', borderRadius: 14, padding: '0 16px', height: 48, display: 'flex', alignItems: 'center', gap: 8, border: '1px solid rgba(255,255,255,0.9)', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }},
React.createElement('span', { style: { fontSize: 16, flexShrink: 0 }}, '\uD83D\uDCDE'),
React.createElement('input', { type: 'tel', placeholder: '\u8BF7\u8F93\u5165\u624B\u673A\u53F7', value: phone, onChange: function(e) { setPhone(e.target.value.replace(/\D/g, '').slice(0, 11)); setError(''); }, style: { border: 'none', outline: 'none', flex: 1, fontSize: 16, background: 'transparent', color: '#000', fontFamily: 'inherit' }})
),
React.createElement('div', { style: { background: 'rgba(255,255,255,0.8)', backdropFilter: 'blur(30px)', borderRadius: 14, padding: '0 16px', height: 48, display: 'flex', alignItems: 'center', gap: 8, border: '1px solid rgba(255,255,255,0.9)', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }},
React.createElement('span', { style: { fontSize: 16, flexShrink: 0 }}, '\uD83D\uDD12'),
React.createElement('input', { type: 'password', placeholder: '\u8BF7\u8BBE\u7F6E\u5BC6\u7801\uFF08\u81F3\u5C116\u4F4D\uFF09', value: password, onChange: function(e) { setPassword(e.target.value); setError(''); }, style: { border: 'none', outline: 'none', flex: 1, fontSize: 16, background: 'transparent', color: '#000', fontFamily: 'inherit' }})
),
React.createElement('button', { onClick: handleRegister, disabled: loading, style: { width: '100%', height: 50, background: loading ? '#FF9F5B' : '#FF6B35', color: '#fff', border: 'none', borderRadius: 25, fontSize: 17, fontWeight: 600, cursor: loading ? 'not-allowed' : 'pointer', boxShadow: '0 4px 16px rgba(255,107,53,0.3)', marginTop: 4 }}, loading ? '\u6CE8\u518C\u4E2D...' : '\u6CE8 \u518C'),
React.createElement('div', { style: { textAlign: 'center' }},
React.createElement('span', { onClick: onGoLogin, style: { fontSize: 14, color: '#FF6B35', fontWeight: 500, cursor: 'pointer' }}, '\u5DF2\u6709\u8D26\u53F7\uFF1F\u7ACB\u5373\u767B\u5F55')
)
)
);
}
/* ===== UserProfilePage ===== */
function UserProfilePage({ user, onUpdate, onBindEmail, onClose }) {
var _useState = useState(user.nickname || '');
var editNickname = _useState[0];
var setEditNickname = _useState[1];
var maskedPhone = user.phone ? user.phone.slice(0, 3) + '****' + user.phone.slice(-4) : '';
var avatarChar = user.nickname ? user.nickname.charAt(0) : '?';
var emailDisplay = user.email ? user.email : '未绑定';
var emailColor = user.email ? '#8E8E93' : '#FF6B35';
return React.createElement('div', { style: {
position: 'absolute', top: -54, left: 0, right: 0, bottom: 0,
background: '#F2F2F7', zIndex: 100,
display: 'flex', flexDirection: 'column',
animation: 'fadeIn 0.2s ease',
}},
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '54px 16px 16px' }},
React.createElement('span', { onClick: onClose, style: { fontSize: 16, color: '#007AFF', cursor: 'pointer', fontWeight: 500 }}, '← 返回'),
React.createElement('span', { style: { fontSize: 17, fontWeight: 600, color: '#000' }}, '个人信息'),
React.createElement('span', { style: { width: 60 }})
),
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '10px 32px 24px', gap: 10 }},
React.createElement('div', { style: {
width: 56, height: 56, borderRadius: '50%',
background: 'linear-gradient(135deg, #FF6B35, #FF9F5B)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 22, fontWeight: 700, color: '#fff',
boxShadow: '0 4px 16px rgba(255,107,53,0.25)',
}}, avatarChar),
React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: '#1C1C1E' }}, user.nickname || '用户')
),
React.createElement('div', { style: {
background: 'rgba(255,255,255,0.65)', backdropFilter: 'blur(30px)',
borderRadius: 16, margin: '0 16px', overflow: 'hidden',
border: '1px solid rgba(255,255,255,0.8)',
boxShadow: '0 8px 32px rgba(255,107,53,0.06), 0 0 0 1px rgba(255,255,255,0.9) inset',
}},
React.createElement('div', { style: { display: 'flex', alignItems: 'center', padding: '14px 16px', borderBottom: '1px solid rgba(60,60,67,0.06)' }},
React.createElement('span', { style: { fontSize: 14, color: '#8E8E93', width: 60 }}, '昵称'),
React.createElement('span', { style: { flex: 1, fontSize: 16, color: '#1C1C1E', textAlign: 'right' }}, user.nickname || ''),
React.createElement('span', { style: { fontSize: 14, color: '#C7C7CC', marginLeft: 8 }}, '')
),
React.createElement('div', { style: { display: 'flex', alignItems: 'center', padding: '14px 16px', borderBottom: '1px solid rgba(60,60,67,0.06)' }},
React.createElement('span', { style: { fontSize: 14, color: '#8E8E93', width: 60 }}, '手机号'),
React.createElement('span', { style: { flex: 1, fontSize: 16, color: '#1C1C1E', textAlign: 'right' }}, maskedPhone)
),
React.createElement('div', { onClick: onBindEmail, style: { display: 'flex', alignItems: 'center', padding: '14px 16px', cursor: 'pointer' }},
React.createElement('span', { style: { fontSize: 14, color: '#8E8E93', width: 60 }}, '邮箱'),
React.createElement('span', { style: { flex: 1, fontSize: 16, color: emailColor, textAlign: 'right' }}, emailDisplay),
React.createElement('span', { style: { fontSize: 14, color: '#C7C7CC', marginLeft: 8 }}, '')
)
),
React.createElement('button', { onClick: onClose, style: {
width: 'calc(100% - 64px)', height: 50,
background: '#FF6B35', color: '#fff', border: 'none',
borderRadius: 25, fontSize: 17, fontWeight: 600, cursor: 'pointer',
boxShadow: '0 4px 16px rgba(255,107,53,0.3)',
margin: '24px 32px 0',
}}, '保 存'),
React.createElement('div', { style: { fontSize: 12, color: '#8E8E93', textAlign: 'center', marginTop: 12 }},
'绑定邮箱后可用于找回密码'
)
);
}
/* ===== BindEmailPage ===== */
function BindEmailPage({ currentEmail, user, onBind, onClose }) {
var _useState = useState(currentEmail || '');
var email = _useState[0], setEmail = _useState[1];
var _useState2 = useState(''), error = _useState2[0], setError = _useState2[1];
var _useState3 = useState(false), loading = _useState3[0], setLoading = _useState3[1];
var handleBind = function() {
setError('');
var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email || !emailRegex.test(email)) {
setError('请输入正确的邮箱地址');
return;
}
setLoading(true);
window.__authApi.bindEmail(user.id, email).then(function(result) {
setLoading(false);
if (result.success) {
onBind(email);
} else {
setError(result.error || '绑定失败,请重试');
}
});
};
return React.createElement('div', { style: {
position: 'absolute', top: -54, left: 0, right: 0, bottom: 0,
background: '#F2F2F7', zIndex: 100,
display: 'flex', flexDirection: 'column',
animation: 'fadeIn 0.2s ease',
}},
/* Top Bar */
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '54px 16px 16px' }},
React.createElement('span', { onClick: onClose, style: { fontSize: 16, color: '#007AFF', cursor: 'pointer', fontWeight: 500 }}, '\u2190 \u8FD4\u56DE'),
React.createElement('span', { style: { fontSize: 17, fontWeight: 600, color: '#000' }}, '\u7ED1\u5B9A\u90AE\u7BB1'),
React.createElement('span', { style: { width: 60 }})
),
/* Content */
React.createElement('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '0 32px', gap: 20 }},
/* Error */
error ? React.createElement('div', { style: { fontSize: 14, color: '#FF3B30', textAlign: 'center', padding: '8px 0' }}, error) : null,
/* Title */
React.createElement('div', { style: { textAlign: 'center', fontSize: 14, color: '#8E8E93', fontWeight: 500, marginBottom: -8 }}, '\u7ED1\u5B9A\u90AE\u7BB1'),
/* Email Input */
React.createElement('div', { style: { background: 'rgba(255,255,255,0.8)', backdropFilter: 'blur(30px)', borderRadius: 14, padding: '0 16px', height: 48, display: 'flex', alignItems: 'center', gap: 8, border: '1px solid rgba(255,255,255,0.9)', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }},
React.createElement('span', { style: { fontSize: 16, flexShrink: 0 }}, '\uD83D\uDCE7'),
React.createElement('input', { type: 'email', placeholder: '\u8BF7\u8F93\u5165\u90AE\u7BB1\u5730\u5740', value: email, onChange: function(e) { setEmail(e.target.value); setError(''); }, style: { border: 'none', outline: 'none', flex: 1, fontSize: 16, background: 'transparent', color: '#000', fontFamily: 'inherit' }})
),
/* Confirm Button */
React.createElement('button', { onClick: handleBind, disabled: loading, style: { width: '100%', height: 50, background: loading ? '#FF9F5B' : '#FF6B35', color: '#fff', border: 'none', borderRadius: 25, fontSize: 17, fontWeight: 600, cursor: loading ? 'not-allowed' : 'pointer', boxShadow: '0 4px 16px rgba(255,107,53,0.3)' }}, loading ? '\u7ED1\u5B9A\u4E2D...' : '\u786E \u8BA4 \u7ED1 \u5B9A'),
/* Hint */
React.createElement('div', { style: { textAlign: 'center', fontSize: 12, color: '#8E8E93', marginTop: -8 }}, '\u7ED1\u5B9A\u540E\u53EF\u7528\u4E8E\u627E\u56DE\u5BC6\u7801')
)
);
}
/* ===== CreateFamilyPage ===== */
function CreateFamilyPage({ user, onCreated, onClose }) {
var _useState = useState('');
var familyName = _useState[0], setFamilyName = _useState[1];
var _useState2 = useState(''), error = _useState2[0], setError = _useState2[1];
var _useState3 = useState(false), loading = _useState3[0], setLoading = _useState3[1];
var _useState4 = useState(null), createdFamily = _useState4[0], setCreatedFamily = _useState4[1];
var handleCreate = function() {
setError('');
if (!familyName || !familyName.trim()) {
setError('请输入家庭名称');
return;
}
setLoading(true);
window.__authApi.createFamily(user.id, familyName.trim()).then(function(result) {
setLoading(false);
if (result.success) {
setCreatedFamily(result.data.family);
} else {
setError(result.error || '创建失败,请重试');
}
});
};
// Show invite code after creation
if (createdFamily) {
return React.createElement('div', { style: {
position: 'absolute', top: -54, left: 0, right: 0, bottom: 0,
background: '#F2F2F7', zIndex: 100,
display: 'flex', flexDirection: 'column',
animation: 'fadeIn 0.2s ease',
}},
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '54px 16px 16px' }},
React.createElement('span', { style: { width: 40 }}),
React.createElement('span', { style: { fontSize: 17, fontWeight: 600, color: '#000' }}, '创建家庭'),
React.createElement('span', { style: { width: 40 }})
),
React.createElement('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', padding: '0 32px', gap: 24 }},
React.createElement('div', { style: { fontSize: 48 }}, '🎉'),
React.createElement('div', { style: { fontSize: 22, fontWeight: 700, color: '#1C1C1E' }}, '家庭创建成功!'),
React.createElement('div', { style: { fontSize: 18, fontWeight: 600, color: '#FF6B35' }}, createdFamily.name),
React.createElement('div', { style: { textAlign: 'center', marginTop: 8 }},
React.createElement('div', { style: { fontSize: 13, color: '#8E8E93', marginBottom: 8 }}, '邀请码'),
React.createElement('div', { style: {
fontSize: 28, fontWeight: 700, color: '#1C1C1E', letterSpacing: '6px',
background: '#E8E8ED', borderRadius: 12, padding: '12px 24px',
border: '2px dashed #C7C7CC',
fontFamily: 'monospace',
textAlign: 'center',
}}, createdFamily.inviteCode),
React.createElement('div', { style: { fontSize: 12, color: '#8E8E93', marginTop: 10, lineHeight: 1.5 }},
'将此邀请码分享给家人,他们可以在设置页加入你的家庭'
)
),
React.createElement('button', { onClick: function() { onCreated(createdFamily); }, style: {
width: '100%', height: 50,
background: '#FF6B35', color: '#fff', border: 'none',
borderRadius: 25, fontSize: 17, fontWeight: 600, cursor: 'pointer',
boxShadow: '0 4px 16px rgba(255,107,53,0.3)',
marginTop: 8,
}}, '完 成')
)
);
}
// Input form
return React.createElement('div', { style: {
position: 'absolute', top: -54, left: 0, right: 0, bottom: 0,
background: '#F2F2F7', zIndex: 100,
display: 'flex', flexDirection: 'column',
animation: 'fadeIn 0.2s ease',
}},
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '54px 16px 16px' }},
React.createElement('span', { onClick: onClose, style: { fontSize: 16, color: '#007AFF', cursor: 'pointer', fontWeight: 500 }}, '← 返回'),
React.createElement('span', { style: { fontSize: 17, fontWeight: 600, color: '#000' }}, '创建家庭'),
React.createElement('span', { style: { width: 40 }})
),
React.createElement('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '0 32px', gap: 20 }},
error ? React.createElement('div', { style: { fontSize: 14, color: '#FF3B30', textAlign: 'center', padding: '8px 0' }}, error) : null,
React.createElement('div', { style: { textAlign: 'center', marginBottom: 4 }},
React.createElement('div', { style: { fontSize: 24, fontWeight: 700, color: '#1C1C1E' }}, '创建一个家庭'),
React.createElement('div', { style: { fontSize: 14, color: '#8E8E93', marginTop: 6 }}, '创建后邀请家人加入,共同记账')
),
React.createElement('div', { style: { background: 'rgba(255,255,255,0.8)', backdropFilter: 'blur(30px)', borderRadius: 14, padding: '0 16px', height: 48, display: 'flex', alignItems: 'center', gap: 8, border: '1px solid rgba(255,255,255,0.9)', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }},
React.createElement('span', { style: { fontSize: 16, flexShrink: 0 }}, '🏠'),
React.createElement('input', { type: 'text', placeholder: '给家庭起个名字吧', value: familyName, onChange: function(e) { setFamilyName(e.target.value); setError(''); }, style: { border: 'none', outline: 'none', flex: 1, fontSize: 16, background: 'transparent', color: '#000', fontFamily: 'inherit' }})
),
React.createElement('button', { onClick: handleCreate, disabled: loading, style: { width: '100%', height: 50, background: loading ? '#FF9F5B' : '#FF6B35', color: '#fff', border: 'none', borderRadius: 25, fontSize: 17, fontWeight: 600, cursor: loading ? 'not-allowed' : 'pointer', boxShadow: '0 4px 16px rgba(255,107,53,0.3)' }}, loading ? '创建中...' : '创建家庭')
)
);
}
/* ===== JoinFamilyPage ===== */
function JoinFamilyPage({ user, onJoined, onClose }) {
var _useState = useState('');
var code = _useState[0], setCode = _useState[1];
var _useState2 = useState(''), error = _useState2[0], setError = _useState2[1];
var _useState3 = useState(false), loading = _useState3[0], setLoading = _useState3[1];
var handleJoin = function() {
setError('');
if (!code || code.length !== 6) {
setError('请输入6位邀请码');
return;
}
setLoading(true);
window.__authApi.joinFamily(user.id, code.toUpperCase()).then(function(result) {
setLoading(false);
if (result.success) {
onJoined(result.data.family);
} else {
setError(result.error || '加入失败,请重试');
}
});
};
return React.createElement('div', { style: {
position: 'absolute', top: -54, left: 0, right: 0, bottom: 0,
background: '#F2F2F7', zIndex: 100,
display: 'flex', flexDirection: 'column',
animation: 'fadeIn 0.2s ease',
}},
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '54px 16px 16px' }},
React.createElement('span', { onClick: onClose, style: { fontSize: 16, color: '#007AFF', cursor: 'pointer', fontWeight: 500 }}, '← 返回'),
React.createElement('span', { style: { fontSize: 17, fontWeight: 600, color: '#000' }}, '加入家庭'),
React.createElement('span', { style: { width: 40 }})
),
React.createElement('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '0 32px', gap: 20 }},
error ? React.createElement('div', { style: { fontSize: 14, color: '#FF3B30', textAlign: 'center', padding: '8px 0' }}, error) : null,
React.createElement('div', { style: { textAlign: 'center', marginBottom: 4 }},
React.createElement('div', { style: { fontSize: 24, fontWeight: 700, color: '#1C1C1E' }}, '加入家庭'),
React.createElement('div', { style: { fontSize: 14, color: '#8E8E93', marginTop: 6 }}, '输入家人分享给你的6位邀请码')
),
React.createElement('div', { style: { background: 'rgba(255,255,255,0.8)', backdropFilter: 'blur(30px)', borderRadius: 14, padding: '0 16px', height: 48, display: 'flex', alignItems: 'center', gap: 8, border: '1px solid rgba(255,255,255,0.9)', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }},
React.createElement('span', { style: { fontSize: 16, flexShrink: 0 }}, '🔑'),
React.createElement('input', { type: 'text', placeholder: '请输入邀请码', value: code, onChange: function(e) { setCode(e.target.value.replace(/[^a-zA-Z0-9]/g, '').slice(0, 6)); setError(''); }, style: { border: 'none', outline: 'none', flex: 1, fontSize: 18, background: 'transparent', color: '#000', fontFamily: 'monospace', letterSpacing: '4px', textTransform: 'uppercase' }})
),
React.createElement('button', { onClick: handleJoin, disabled: loading || code.length !== 6, style: { width: '100%', height: 50, background: loading || code.length !== 6 ? '#FF9F5B' : '#FF6B35', color: '#fff', border: 'none', borderRadius: 25, fontSize: 17, fontWeight: 600, cursor: loading || code.length !== 6 ? 'not-allowed' : 'pointer', boxShadow: '0 4px 16px rgba(255,107,53,0.3)' }}, loading ? '加入中...' : '加入家庭')
)
);
}
/* ===== FamilyManagementPage ===== */
function FamilyManagementPage({ user, onFamilyChange, onClose }) {
var _useState = useState([]);
var families = _useState[0], setFamilies = _useState[1];
var _useState2 = useState(true), loading = _useState2[0], setLoading = _useState2[1];
var _useState3 = useState('main'), page = _useState3[0], setPage = _useState3[1];
var _useState4 = useState(''), msg = _useState4[0], setMsg = _useState4[1];
var _useState5 = useState(''), msgType = _useState5[0], setMsgType = _useState5[1];
React.useEffect(function() {
var result = window.__authApi.getUserFamilies(user.id);
if (result.success) {
setFamilies(result.data.families);
}
setLoading(false);
}, []);
var handleSwitchFamily = function(familyId) {
var result = window.__authApi.switchActiveFamily(user.id, familyId);
if (result.success) {
var updatedUser = Object.assign({}, user, { activeFamilyId: familyId });
onFamilyChange(updatedUser);
setMsg('已切换到当前家庭');
setMsgType('success');
setTimeout(function() { setMsg(''); }, 2000);
}
};
var handleLeaveFamily = function(familyId, e) {
e.stopPropagation();
var family = families.find(function(f) { return f.id === familyId; });
if (family && family.createdBy === user.id) {
setMsg('创建者不能退出家庭');
setMsgType('error');
setTimeout(function() { setMsg(''); }, 3000);
return;
}
if (!confirm('确定退出该家庭吗?')) return;
window.__authApi.leaveFamily(user.id, familyId).then(function(result) {
if (result.success) {
// Reload families
var r = window.__authApi.getUserFamilies(user.id);
if (r.success) {
setFamilies(r.data.families);
// If user had active family and left it, update
if (user.activeFamilyId === familyId) {
var nextFamily = r.data.families.length > 0 ? r.data.families[0] : null;
var newUserId = nextFamily ? nextFamily.id : null;
window.__authApi.switchActiveFamily(user.id, newUserId);
var updatedUser = Object.assign({}, user, { activeFamilyId: newUserId });
onFamilyChange(updatedUser);
} else {
onFamilyChange(user);
}
}
setMsg('已退出家庭');
setMsgType('success');
setTimeout(function() { setMsg(''); }, 2000);
} else {
setMsg(result.error || '退出失败');
setMsgType('error');
setTimeout(function() { setMsg(''); }, 3000);
}
});
};
var handleCreated = function(family) {
var r = window.__authApi.getUserFamilies(user.id);
if (r.success) {
setFamilies(r.data.families);
var updatedUser = Object.assign({}, user, { activeFamilyId: family.id, familyIds: (user.familyIds || []).concat([family.id]) });
onFamilyChange(updatedUser);
}
setPage('main');
};
var handleJoined = function(family) {
var r = window.__authApi.getUserFamilies(user.id);
if (r.success) {
setFamilies(r.data.families);
}
setPage('main');
};
// Sub-pages
if (page === 'create') {
return React.createElement(CreateFamilyPage, { user: user, onCreated: handleCreated, onClose: function() { setPage('main'); } });
}
if (page === 'join') {
return React.createElement(JoinFamilyPage, { user: user, onJoined: handleJoined, onClose: function() { setPage('main'); } });
}
// Main page
var activeFamily = families.find(function(f) { return f.id === user.activeFamilyId; });
return React.createElement('div', { style: {
position: 'absolute', top: -54, left: 0, right: 0, bottom: 0,
background: '#F2F2F7', zIndex: 100,
display: 'flex', flexDirection: 'column',
animation: 'fadeIn 0.2s ease',
}},
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '54px 16px 16px' }},
React.createElement('span', { onClick: onClose, style: { fontSize: 16, color: '#007AFF', cursor: 'pointer', fontWeight: 500 }}, '← 返回'),
React.createElement('span', { style: { fontSize: 17, fontWeight: 600, color: '#000' }}, '我的家庭'),
React.createElement('span', { style: { width: 40 }})
),
msg ? React.createElement('div', { style: { fontSize: 13, color: msgType === 'success' ? '#34C759' : '#FF3B30', textAlign: 'center', padding: '6px 16px', background: msgType === 'success' ? 'rgba(52,199,89,0.1)' : 'rgba(255,59,48,0.1)', margin: '0 16px', borderRadius: 8 }}, msg) : null,
React.createElement('div', { style: { flex: 1, overflowY: 'auto', padding: '0 16px 24px' }},
// Active family card
activeFamily ? React.createElement('div', { style: {
background: 'rgba(255,255,255,0.65)', backdropFilter: 'blur(30px)',
borderRadius: 16, padding: '20px', marginBottom: 16, marginTop: 8,
border: '1px solid rgba(255,255,255,0.8)',
boxShadow: '0 8px 32px rgba(255,107,53,0.06)',
}},
React.createElement('div', { style: { fontSize: 12, fontWeight: 600, color: '#8E8E93', marginBottom: 12, textTransform: 'uppercase', letterSpacing: '0.5px' }}, '当前家庭'),
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12 }},
React.createElement('div', { style: { width: 44, height: 44, borderRadius: 12, background: 'linear-gradient(135deg, #FF6B35, #FF9F5B)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20 }}, '🏠'),
React.createElement('div', { style: { flex: 1 }},
React.createElement('div', { style: { fontSize: 17, fontWeight: 700, color: '#1C1C1E' }}, activeFamily.name),
React.createElement('div', { style: { fontSize: 12, color: '#8E8E93', marginTop: 2 }},
(activeFamily.members ? activeFamily.members.length : 0) + ' 位成员 · ' + (activeFamily.members && activeFamily.members.some(function(m) { return m.userId === user.id && m.role === 'owner'; }) ? '创建者' : '成员')
)
),
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: '#FF6B35', background: 'rgba(255,107,53,0.1)', padding: '4px 10px', borderRadius: 10 }}, '当前')
)
) : React.createElement('div', { style: { textAlign: 'center', padding: '32px 16px', fontSize: 14, color: '#8E8E93' }}, '还没有加入任何家庭'),
// My Families section
families.length > 0 ? React.createElement('div', { style: { marginBottom: 16 }},
React.createElement('div', { style: { fontSize: 12, fontWeight: 600, color: '#8E8E93', marginBottom: 8, paddingLeft: 4, textTransform: 'uppercase', letterSpacing: '0.5px' }}, '我的所有家庭'),
React.createElement('div', { style: {
background: 'rgba(255,255,255,0.65)', backdropFilter: 'blur(30px)',
borderRadius: 14, overflow: 'hidden',
border: '1px solid rgba(255,255,255,0.8)',
boxShadow: '0 8px 32px rgba(255,107,53,0.06)',
}},
families.map(function(family, i) {
var isActive = family.id === user.activeFamilyId;
var isOwner = family.members && family.members.some(function(m) { return m.userId === user.id && m.role === 'owner'; });
return React.createElement('div', { key: family.id, onClick: function() { if (!isActive) handleSwitchFamily(family.id); }, style: {
display: 'flex', alignItems: 'center', gap: 12,
padding: '14px 16px', cursor: 'pointer',
borderBottom: i < families.length - 1 ? '1px solid rgba(60,60,67,0.06)' : 'none',
background: isActive ? 'rgba(255,107,53,0.04)' : 'transparent',
}},
React.createElement('div', { style: { width: 36, height: 36, borderRadius: 10, background: isActive ? '#FF6B35' : 'rgba(142,142,147,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 16, color: isActive ? '#fff' : '#8E8E93', flexShrink: 0 }}, '🏠'),
React.createElement('div', { style: { flex: 1 }},
React.createElement('div', { style: { fontSize: 15, fontWeight: 600, color: '#1C1C1E' }}, family.name),
React.createElement('div', { style: { fontSize: 11, color: '#8E8E93', marginTop: 1 }},
(family.members ? family.members.length : 0) + ' 位成员 · ' + (isOwner ? '创建者' : '成员')
)
),
isActive ? React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: '#FF6B35', background: 'rgba(255,107,53,0.1)', padding: '4px 8px', borderRadius: 8 }}, '当前') : null,
!isOwner && !isActive ? React.createElement('span', { onClick: function(e) { handleLeaveFamily(family.id, e); }, style: { fontSize: 12, color: '#FF3B30', padding: '4px 8px', cursor: 'pointer' }}, '退出') : null
);
})
)
) : null,
// Action buttons
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 12, marginTop: 8 }},
React.createElement('button', { onClick: function() { setPage('create'); }, style: {
width: '100%', height: 48,
background: '#FF6B35', color: '#fff', border: 'none',
borderRadius: 14, fontSize: 15, fontWeight: 600, cursor: 'pointer',
boxShadow: '0 4px 12px rgba(255,107,53,0.2)',
}}, ' 创建家庭'),
React.createElement('button', { onClick: function() { setPage('join'); }, style: {
width: '100%', height: 48,
background: 'rgba(255,255,255,0.65)', color: '#FF6B35',
border: '1px solid rgba(255,107,53,0.2)',
borderRadius: 14, fontSize: 15, fontWeight: 600, cursor: 'pointer',
backdropFilter: 'blur(30px)',
}}, '🔑 加入家庭')
)
)
);
}
/* ===== App ===== */
function App() {
const [activeTab, setActiveTab] = useState('personal');
const [transitioning, setTransitioning] = useState(false);
const [displayTab, setDisplayTab] = useState('personal');
const [fadeState, setFadeState] = useState('visible');
const [screen, setScreen] = useState('home');
const [navTab, setNavTab] = useState('home');
const [previousTab, setPreviousTab] = useState('home');
const [showActionSheet, setShowActionSheet] = useState(false);
const [ocrData, setOcrData] = useState(null);
const [user, setUser] = useState(function() {
try {
var token = localStorage.getItem('__auth_token');
if (token && window.__authApi) {
var result = window.__authApi.getUserByToken(token);
if (result) return result;
}
} catch(e) {}
return null;
});
const switchTab = useCallback((tab) => {
if (tab === activeTab || transitioning) return;
setTransitioning(true);
setFadeState('exiting');
setTimeout(() => {
setActiveTab(tab);
setDisplayTab(tab);
setFadeState('entering');
requestAnimationFrame(() => {
setFadeState('visible');
setTransitioning(false);
});
}, 180);
}, [activeTab, transitioning]);
useEffect(() => {
const handler = (e) => setNavTab(e.detail);
window.addEventListener('navTo', handler);
return () => window.removeEventListener('navTo', handler);
}, []);
useEffect(() => {
window.dispatchEvent(new CustomEvent('navUpdate', { detail: navTab }));
}, [navTab]);
const handleCloseAddBill = () => {
setScreen('home');
setNavTab(previousTab);
window.__ocrPendingData = null;
window.dispatchEvent(new CustomEvent('refreshBills'));
};
const handleLoginSuccess = (token, userData) => {
localStorage.setItem('__auth_token', token);
setUser(userData);
setScreen('home');
setNavTab('profile');
};
const handleLogout = () => {
if (confirm('确定要退出登录吗?')) {
window.__authApi.logout();
setUser(null);
}
};
const handleShowLogin = () => {
setScreen('login');
};
const handleGoRegister = () => {
setScreen('register');
};
const handleGoLogin = () => {
setScreen('login');
};
const handleShowUserInfo = () => {
setScreen('userProfile');
};
const handleBindEmail = (email) => {
if (user) {
var updatedUser = Object.assign({}, user, { email: email });
setUser(updatedUser);
window.__authApi.bindEmail(user.id, email);
}
};
const handleShowFamilyManagement = () => {
setScreen('familyManagement');
};
const handleFamilyChange = (updatedUser) => {
setUser(updatedUser);
if (updatedUser) {
window.dispatchEvent(new CustomEvent('familyModeUpdate'));
}
};
const handleFamilyCreated = (family) => {
var token = localStorage.getItem('__auth_token');
if (token && window.__authApi) {
var updatedUser = window.__authApi.getUserByToken(token);
if (updatedUser) {
setUser(updatedUser);
window.dispatchEvent(new CustomEvent('familyModeUpdate'));
}
}
setScreen('home');
};
const handleShowActionSheet = () => {
setShowActionSheet(true);
};
useEffect(() => {
const handler = () => handleShowActionSheet();
window.addEventListener('showActionSheet', handler);
return () => window.removeEventListener('showActionSheet', handler);
}, [handleShowActionSheet]);
const handleOpenOCR = () => {
setShowActionSheet(false);
setScreen('ocrScan');
};
const handleOCRBack = () => {
setScreen('home');
setOcrData(null);
};
const handleOCRDataReady = (data) => {
setOcrData(data);
setScreen('ocrResult');
};
const handleOCRConfirm = () => {
const data = ocrData;
setOcrData(null);
setScreen('addBill');
window.__ocrPendingData = data;
};
if (screen === 'ocrScan') {
return (
<OCRScanPage
onClose={handleOCRBack}
onCaptured={handleOCRDataReady}
/>
);
}
if (screen === 'ocrResult') {
return (
<OCRResultPreview
data={ocrData}
onConfirm={handleOCRConfirm}
onRetake={handleOCRBack}
/>
);
}
if (screen === 'addBill') {
return <AddBillScreen onClose={handleCloseAddBill} ocrData={window.__ocrPendingData || null} />;
}
if (screen === 'login') {
return (
<LoginPage
onLoginSuccess={handleLoginSuccess}
onGoRegister={handleGoRegister}
onClose={function() { setScreen('home'); }}
/>
);
}
if (screen === 'register') {
return (
<RegisterPage
onRegisterSuccess={handleLoginSuccess}
onGoLogin={handleGoLogin}
onClose={function() { setScreen('home'); }}
/>
);
}
if (screen === 'userProfile') {
return (
<UserProfilePage
user={user}
onUpdate={setUser}
onBindEmail={function() { setScreen('bindEmail'); }}
onClose={function() { setScreen('home'); }}
/>
);
}
if (screen === 'bindEmail') {
return (
<BindEmailPage
currentEmail={user ? user.email : null}
user={user}
onBind={function(email) {
handleBindEmail(email);
setScreen('userProfile');
}}
onClose={function() { setScreen('userProfile'); }}
/>
);
}
if (screen === 'familyManagement') {
return (
<FamilyManagementPage
user={user}
onFamilyChange={handleFamilyChange}
onClose={function() { setScreen('home'); }}
/>
);
}
const contentOpacity = fadeState === 'visible' ? 1 : 0;
const contentTransform = fadeState === 'visible' ? 'translateY(0)' : 'translateY(8px)';
const renderContent = () => {
switch (navTab) {
case 'stats':
return <StatisticsContent user={user} activeTab={activeTab} />;
case 'profile':
return (
<ProfileContent
user={user}
onLogin={handleShowLogin}
onLogout={handleLogout}
onUserInfo={handleShowUserInfo}
onFamilyManagement={handleShowFamilyManagement}
/>
);
default:
return (
<>
<OrangeHeader user={user} />
<TabSwitcher activeTab={activeTab} onSwitch={switchTab} animating={transitioning} />
<div style={{
transition: 'opacity 0.25s ease, transform 0.25s ease',
opacity: contentOpacity,
transform: contentTransform,
}}>
{displayTab === 'personal' ? <PersonalContent user={user} /> : <FamilyContent user={user} />}
</div>
</>
);
}
};
return (
<div style={{
minHeight: '100%', display: 'flex', flexDirection: 'column',
background: 'linear-gradient(180deg, #F5F7FA 0%, #EDF2F9 100%)',
overflow: 'auto',
}}>
<ActionSheet
show={showActionSheet}
onManual={() => { setShowActionSheet(false); setScreen('addBill'); }}
onScan={handleOpenOCR}
onCancel={() => setShowActionSheet(false)}
/>
<div style={{ flex: 1 }}>
{renderContent()}
</div>
</div>
);
}
/* ===== Main ===== */
function Main() {
return (
<div className="phone-wrapper">
<IosFrame
width={370}
height={780}
time="10:28"
battery={78}
darkMode={false}
bottomOverlay={<BottomTabBar />}
fabOverlay={
<div style={{
bottom: 100,
right: 16,
position: 'absolute',
}}>
<FAB onClick={() => window.dispatchEvent(new CustomEvent('showActionSheet'))} />
</div>
}
>
<App />
</IosFrame>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Main />);
</script>
</body>
</html>