- 用户模型添加:手机号、邀请码、邀请统计、邮件通知设置 - 邀请好友系统:专属邀请码、邀请奖励(¥5/人) - 邮件通知:翻译完成通知(含附件)、欢迎邮件、到期提醒 - 新增模型:UserInvitation, InviteRewardConfig, EmailNotification, EmailTemplateConfig - 个人中心添加:手机号绑定、通知设置、邀请好友模块 - email_service.py:邮件发送服务(支持附件) 新用户注册奖励:¥2 邀请人奖励:¥5/人
532 lines
25 KiB
HTML
532 lines
25 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>个人中心 - PDF翻译助手</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="/">📄 PDF翻译助手</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 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>
|
|
<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>
|
|
<small class="text-muted">通知发送至:{{ user.email }}</small>
|
|
</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>
|
|
|
|
<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;
|
|
|
|
fetch('/api/profile/settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
notify_on_complete: notifyComplete,
|
|
notify_on_expire: notifyExpire
|
|
})
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// 不刷新页面,只显示提示
|
|
console.log('设置已更新');
|
|
}
|
|
});
|
|
}
|
|
|
|
// 绑定手机号
|
|
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('手机号格式不正确');
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |