Files
pdf-translate-web/templates/profile.html
coder 69e4ca4d64 feat: 前端页面使用网站基础配置
- 使用 Flask context_processor 自动注入 site_config
- 所有页面标题使用 site_name 配置
- 所有页面导航栏品牌使用 site_name 配置
- 所有页面底部使用 site_footer 配置
- 文件上传时使用 max_file_size 配置验证文件大小
- 显示最大文件限制提示
2026-04-16 18:44:57 +08:00

635 lines
30 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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>个人中心 - {{ site_config.site_name }}</title>
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/style.css" rel="stylesheet">
<style>
.balance-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.stat-item { border-left: 3px solid #dee2e6; padding-left: 12px; }
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/">📄 {{ site_config.site_name }}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item"><a class="nav-link" href="/">首页</a></li>
<li class="nav-item"><a class="nav-link" href="/pricing">会员套餐</a></li>
<li class="nav-item"><a class="nav-link" href="/history">翻译历史</a></li>
<li class="nav-item"><a class="nav-link active" href="/profile">个人中心</a></li>
</ul>
<div class="navbar-nav">
<span class="nav-link text-light">👋 {{ user.username }}</span>
<a class="nav-link" href="/logout">退出</a>
</div>
</div>
</div>
</nav>
<main class="container my-4">
<!-- 会员状态栏 -->
{% if user.user_type == 'vip_enterprise' %}
<div class="alert alert-dark py-2 mb-4 text-center">
<strong>👑 企业会员</strong> | 无限翻译 | 到期:{{ user.membership_expire.strftime('%Y-%m-%d') if user.membership_expire else '永久' }}
</div>
{% elif user.user_type == 'vip_pro' %}
<div class="alert alert-warning py-2 mb-4 text-center">
<strong>⭐ 专业会员</strong> | 每日200次·500页 | 到期:{{ user.membership_expire.strftime('%Y-%m-%d') if user.membership_expire else '永久' }}
</div>
{% elif user.user_type == 'vip_basic' %}
<div class="alert alert-success py-2 mb-4 text-center">
<strong>💚 基础会员</strong> | 每日50次·100页 | 到期:{{ user.membership_expire.strftime('%Y-%m-%d') if user.membership_expire else '永久' }}
</div>
{% else %}
<div class="alert alert-info py-2 mb-4 text-center">
<strong>👤 免费用户</strong> | 每日10次·50页 | <a href="/pricing" class="alert-link">升级会员</a>
</div>
{% endif %}
<div class="row">
<!-- 左侧:账户余额 -->
<div class="col-md-4 mb-4">
<div class="card balance-card text-white h-100">
<div class="card-body text-center">
<h5 class="card-title mb-3">💰 账户余额</h5>
<div class="balance-amount my-4">
<span class="fs-1 fw-bold">¥{{ "%.2f"|format(user.balance) }}</span>
</div>
<div class="d-grid gap-2">
<button class="btn btn-light" onclick="showRechargeModal()">充值</button>
<button class="btn btn-outline-light" onclick="showRefundModal()">申请退款</button>
</div>
</div>
</div>
</div>
<!-- 右侧:使用统计 -->
<div class="col-md-8 mb-4">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">📊 使用统计</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-6 stat-item mb-3">
<small class="text-muted">今日翻译</small>
<div class="fs-4 fw-bold text-primary">{{ user.daily_count }}</div>
</div>
<div class="col-6 stat-item mb-3">
<small class="text-muted">累计翻译</small>
<div class="fs-4 fw-bold">{{ user.total_count }}</div>
</div>
<div class="col-6 stat-item mb-3">
<small class="text-muted">剩余次数</small>
<div class="fs-4 fw-bold text-success">{{ daily_remaining }}</div>
</div>
<div class="col-6 stat-item mb-3">
<small class="text-muted">最大页数</small>
<div class="fs-4 fw-bold">{{ max_pages }}页</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 账户流水 -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">📜 账户流水</h5>
<select class="form-select form-select-sm" id="transactionFilter" style="width: auto;">
<option value="all">全部</option>
<option value="recharge">充值</option>
<option value="refund">退款</option>
<option value="purchase">购买会员</option>
<option value="consume">消费</option>
</select>
</div>
<div class="card-body">
<div id="transactionsList">
<table class="table table-striped">
<thead>
<tr>
<th>时间</th>
<th>类型</th>
<th>金额</th>
<th>余额变化</th>
<th>描述</th>
</tr>
</thead>
<tbody id="transactionBody">
<!-- 动态加载 -->
</tbody>
</table>
</div>
</div>
</div>
<!-- 会员购买记录 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">🎫 会员购买记录</h5>
</div>
<div class="card-body">
<div id="membershipPurchases">
<table class="table">
<thead>
<tr>
<th>套餐</th>
<th>金额</th>
<th>有效期</th>
<th>状态</th>
<th>时间</th>
</tr>
</thead>
<tbody id="purchaseBody">
<!-- 动态加载 -->
</tbody>
</table>
</div>
</div>
</div>
<!-- 账户设置 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">⚙️ 账户设置</h5>
</div>
<div class="card-body">
<div class="row">
<!-- 手机号绑定 -->
<div class="col-md-6 mb-3">
<div class="border rounded p-3">
<h6 class="mb-3">📱 手机号</h6>
<div id="phoneStatus">
{% if user.phone %}
<p class="text-success">已绑定:{{ user.phone }}
{% if user.phone_verified %}<span class="badge bg-success">已验证</span>{% endif %}
</p>
<button class="btn btn-outline-secondary btn-sm" onclick="showPhoneModal()">更换手机号</button>
{% else %}
<p class="text-muted">未绑定手机号</p>
<button class="btn btn-primary btn-sm" onclick="showPhoneModal()">绑定手机号</button>
{% endif %}
</div>
</div>
</div>
<!-- 邮件通知设置 -->
<div class="col-md-6 mb-3">
<div class="border rounded p-3">
<h6 class="mb-3">📧 通知邮箱</h6>
<div id="emailStatus">
{% if user.email %}
<p class="mb-2">当前通知邮箱:<strong>{{ user.email }}</strong></p>
<button class="btn btn-outline-secondary btn-sm mb-3" onclick="showEmailModal()">更换通知邮箱</button>
{% else %}
<p class="text-muted">未设置通知邮箱</p>
<button class="btn btn-primary btn-sm" onclick="showEmailModal()">绑定邮箱</button>
{% endif %}
<hr class="my-2">
<!-- 翻译完成邮件通知(所有用户都有) -->
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="notifyComplete"
{% if user.notify_on_complete %}checked{% endif %}
onchange="updateNotifySettings()">
<label class="form-check-label">翻译完成邮件通知</label>
</div>
<!-- 邮件带附件VIP功能 -->
{% if has_email_attachment %}
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="notifyAttachment"
{% if user.notify_with_attachment %}checked{% endif %}
onchange="updateNotifySettings()">
<label class="form-check-label">邮件带附件发送 <span class="badge bg-success">VIP</span></label>
</div>
{% else %}
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" disabled>
<label class="form-check-label text-muted">邮件带附件发送 <span class="badge bg-secondary">需VIP</span></label>
<a href="/pricing" class="small ms-2">升级会员</a>
</div>
{% endif %}
<!-- 会员到期提醒 -->
{% if user.user_type.startswith('vip') %}
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="notifyExpire"
{% if user.notify_on_expire %}checked{% endif %}
onchange="updateNotifySettings()">
<label class="form-check-label">会员到期提醒</label>
</div>
{% endif %}
<small class="text-muted">通知将发送至上述邮箱</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 邀请好友 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">🎁 邀请好友</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="border rounded p-3 bg-light">
<h6>您的专属邀请码</h6>
<div class="input-group mb-3">
<input type="text" class="form-control form-control-lg text-center fw-bold"
id="inviteCode" value="{{ user.invite_code or '生成中...' }}" readonly>
<button class="btn btn-outline-primary" onclick="copyInviteCode()">复制</button>
</div>
<p class="text-muted mb-0">
分享邀请码给好友,好友注册后您可获得 <strong class="text-success">¥5</strong> 奖励!
</p>
</div>
</div>
<div class="col-md-6">
<div class="border rounded p-3">
<h6>邀请统计</h6>
<div class="d-flex justify-content-around text-center">
<div>
<div class="fs-4 fw-bold text-primary">{{ user.invite_count }}</div>
<small class="text-muted">已邀请</small>
</div>
<div>
<div class="fs-4 fw-bold text-success">¥{{ "%.2f"|format(user.invite_rewards) }}</div>
<small class="text-muted">累计奖励</small>
</div>
</div>
</div>
</div>
</div>
<!-- 邀请记录 -->
<div class="mt-3">
<h6>邀请记录</h6>
<table class="table table-sm">
<thead>
<tr>
<th>被邀请人</th>
<th>注册时间</th>
<th>奖励</th>
<th>状态</th>
</tr>
</thead>
<tbody id="inviteRecords">
<!-- 动态加载 -->
</tbody>
</table>
</div>
</div>
</div>
</main>
<!-- 充值模态框 -->
<div class="modal fade" id="rechargeModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">充值</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="rechargeForm">
<div class="mb-3">
<label class="form-label">充值金额(元)</label>
<input type="number" class="form-control" id="rechargeAmount" min="10" max="10000" step="10" value="100" required>
</div>
<div class="mb-3">
<label class="form-label">支付方式</label>
<select class="form-select" id="paymentMethod">
<option value="balance">余额支付</option>
<option value="alipay">支付宝</option>
<option value="wechat">微信支付</option>
</select>
</div>
<div class="alert alert-info">
<small>提示:当前为演示模式,充值将直接到账</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="submitRecharge()">确认充值</button>
</div>
</div>
</div>
</div>
<!-- 退款模态框 -->
<div class="modal fade" id="refundModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">申请退款</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="refundForm">
<div class="mb-3">
<label class="form-label">当前余额</label>
<input type="text" class="form-control" value="¥{{ "%.2f"|format(user.balance) }}" readonly>
</div>
<div class="mb-3">
<label class="form-label">退款金额(元)</label>
<input type="number" class="form-control" id="refundAmount" min="1" max="{{ user.balance }}" step="1" value="{{ user.balance }}" required>
</div>
<div class="mb-3">
<label class="form-label">退款原因</label>
<textarea class="form-control" id="refundReason" rows="3" placeholder="请填写退款原因" required></textarea>
</div>
<div class="alert alert-warning">
<small>⚠️ 退款申请需要管理员审核,审核通过后余额将扣除</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-warning" onclick="submitRefund()">提交申请</button>
</div>
</div>
</div>
</div>
<!-- 更换邮箱模态框 -->
<div class="modal fade" id="emailModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">更换通知邮箱</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="emailForm">
<div class="mb-3">
<label class="form-label">新邮箱地址</label>
<input type="email" class="form-control" id="newEmail" placeholder="example@email.com" required>
</div>
<div class="alert alert-info">
<small>💡 更换后,通知邮件将发送至新邮箱</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="submitEmail()">确认更换</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 加载账户流水
function loadTransactions(type = 'all') {
fetch('/api/profile/transactions?type=' + type)
.then(r => r.json())
.then(data => {
const tbody = document.getElementById('transactionBody');
tbody.innerHTML = data.transactions.map(t => {
const typeMap = {
'recharge': '充值',
'refund': '退款',
'purchase': '购买会员',
'consume': '消费',
'refund_purchase': '会员退款'
};
const typeClass = {
'recharge': 'text-success',
'refund': 'text-warning',
'purchase': 'text-primary',
'consume': 'text-danger',
'refund_purchase': 'text-info'
};
return `<tr>
<td>${t.created_at}</td>
<td><span class="${typeClass[t.transaction_type]}">${typeMap[t.transaction_type]}</span></td>
<td class="${t.amount > 0 ? 'text-success' : 'text-danger'}">¥${t.amount.toFixed(2)}</td>
<td>¥${t.balance_before.toFixed(2)} → ¥${t.balance_after.toFixed(2)}</td>
<td>${t.description || '-'}</td>
</tr>`;
}).join('');
});
}
// 加载会员购买记录
function loadPurchases() {
fetch('/api/profile/purchases')
.then(r => r.json())
.then(data => {
const tbody = document.getElementById('purchaseBody');
tbody.innerHTML = data.purchases.map(p => {
const statusMap = { 'completed': '已完成', 'pending': '待支付', 'refunded': '已退款' };
const statusClass = { 'completed': 'text-success', 'pending': 'text-warning', 'refunded': 'text-info' };
return `<tr>
<td>${p.plan_name}</td>
<td>¥${p.price.toFixed(2)}</td>
<td>${p.period_days}天(至${p.expire_after || '永久'})</td>
<td><span class="${statusClass[p.status]}">${statusMap[p.status]}</span></td>
<td>${p.created_at}</td>
</tr>`;
}).join('');
});
}
// 充值
function showRechargeModal() {
new bootstrap.Modal(document.getElementById('rechargeModal')).show();
}
function submitRecharge() {
const amount = parseFloat(document.getElementById('rechargeAmount').value);
const method = document.getElementById('paymentMethod').value;
fetch('/api/profile/recharge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount, payment_method: method })
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('充值成功!余额:¥' + data.balance.toFixed(2));
location.reload();
} else {
alert('充值失败:' + data.error);
}
});
}
// 退款
function showRefundModal() {
if ({{ user.balance }} <= 0) {
alert('余额不足,无法退款');
return;
}
new bootstrap.Modal(document.getElementById('refundModal')).show();
}
function submitRefund() {
const amount = parseFloat(document.getElementById('refundAmount').value);
const reason = document.getElementById('refundReason').value;
fetch('/api/profile/refund', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount, reason })
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('退款申请已提交,等待管理员审核');
location.reload();
} else {
alert('申请失败:' + data.error);
}
});
}
// 初始化
loadTransactions();
loadPurchases();
loadInviteRecords();
document.getElementById('transactionFilter').addEventListener('change', function() {
loadTransactions(this.value);
});
// 加载邀请记录
function loadInviteRecords() {
fetch('/api/profile/invitations')
.then(r => r.json())
.then(data => {
const tbody = document.getElementById('inviteRecords');
if (data.invitations && data.invitations.length > 0) {
tbody.innerHTML = data.invitations.map(inv => {
const statusMap = { 'pending': '待注册', 'registered': '已注册', 'rewarded': '已奖励' };
const statusClass = { 'pending': 'text-muted', 'registered': 'text-info', 'rewarded': 'text-success' };
return `<tr>
<td>${inv.invitee_email || '用户' + inv.invitee_id}</td>
<td>${inv.created_at}</td>
<td>¥${inv.reward_amount.toFixed(2)}</td>
<td><span class="${statusClass[inv.status]}">${statusMap[inv.status]}</span></td>
</tr>`;
}).join('');
} else {
tbody.innerHTML = '<tr><td colspan="4" class="text-muted text-center">暂无邀请记录</td></tr>';
}
});
}
// 复制邀请码
function copyInviteCode() {
const code = document.getElementById('inviteCode').value;
navigator.clipboard.writeText(code).then(() => {
alert('邀请码已复制:' + code);
});
}
// 更新通知设置
function updateNotifySettings() {
const notifyComplete = document.getElementById('notifyComplete').checked;
const notifyExpire = document.getElementById('notifyExpire')?.checked || false;
const notifyAttachment = document.getElementById('notifyAttachment')?.checked || false;
fetch('/api/profile/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
notify_on_complete: notifyComplete,
notify_on_expire: notifyExpire,
notify_with_attachment: notifyAttachment
})
})
.then(r => r.json())
.then(data => {
if (data.success) {
console.log('设置已更新');
} else if (data.feature === 'email_attachment') {
alert('邮件附件功能需要VIP会员');
location.href = '/pricing';
} else {
alert('更新失败:' + data.error);
}
});
}
// 绑定手机号
function showPhoneModal() {
// 简单处理,直接弹输入框
const phone = prompt('请输入手机号:');
if (phone && phone.length >= 10) {
fetch('/api/profile/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: phone })
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('手机号已绑定');
location.reload();
} else {
alert('绑定失败:' + data.error);
}
});
} else if (phone) {
alert('手机号格式不正确');
}
}
// 更换邮箱
function showEmailModal() {
new bootstrap.Modal(document.getElementById('emailModal')).show();
}
function submitEmail() {
const newEmail = document.getElementById('newEmail').value;
if (!newEmail || !newEmail.includes('@')) {
alert('请输入有效的邮箱地址');
return;
}
fetch('/api/profile/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: newEmail })
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('通知邮箱已更新为:' + newEmail);
location.reload();
} else {
alert('更新失败:' + data.error);
}
});
}
</script>
<!-- 页脚 -->
<footer class="bg-light py-4 mt-5">
<div class="container text-center">
{{ site_config.site_footer | safe }}
</div>
</footer>
</body>
</html>