This commit is contained in:
张成
2025-12-22 16:26:59 +08:00
parent aa2d03ee30
commit e17d5610f5
54 changed files with 11735 additions and 3 deletions

733
app/views/InvitePage.vue Normal file
View File

@@ -0,0 +1,733 @@
<template>
<div class="page-invite">
<h2 class="page-title">推广邀请</h2>
<!-- 邀请信息卡片 -->
<Card class="invite-card">
<template #title>我的邀请链接</template>
<template #content>
<div class="invite-code-section">
<div class="link-display">
<span class="link-label">邀请链接</span>
<span class="link-value">{{ inviteInfo.invite_link || '加载中...' }}</span>
<Button
label="复制链接"
size="small"
@click="handleCopyLink"
v-if="inviteInfo.invite_link"
/>
</div>
<div class="code-display" v-if="inviteInfo.invite_code">
<span class="code-label">邀请码</span>
<span class="code-value">{{ inviteInfo.invite_code }}</span>
<Button label="复制" size="small" @click="handleCopyCode" />
</div>
</div>
<div class="invite-tip">
<p>💡 分享邀请链接给好友好友通过链接注册后您将获得 <strong>3天试用期</strong> 奖励</p>
</div>
</template>
</Card>
<!-- 邀请记录列表 -->
<div class="records-section">
<div class="section-header">
<h3>邀请记录
<Badge :value="(statistics && statistics.totalInvites) || 0" severity="success" class="stat-value" />
</h3>
<Button
label="刷新"
@click="loadRecords"
:loading="recordsLoading"
:disabled="recordsLoading"
/>
</div>
<ProgressSpinner v-if="recordsLoading" />
<div v-else-if="recordsList && recordsList.length > 0" class="records-list">
<Card
v-for="record in recordsList"
:key="record.id"
class="record-item"
>
<template #content>
<div class="record-info">
<div class="record-phone">{{ record.invitee_phone || '未知' }}</div>
<div class="record-time">{{ formatTime(record.register_time) }}</div>
</div>
<div class="record-status">
<Tag
:value="record.reward_status === 1 ? '已奖励' : '待奖励'"
:severity="record.reward_status === 1 ? 'success' : 'warning'"
/>
<span v-if="record.reward_status === 1" class="reward-info">
+{{ record.reward_value || 3 }}
</span>
</div>
</template>
</Card>
</div>
<div v-else class="empty-tip">暂无邀请记录</div>
<!-- 分页 -->
<Paginator
v-if="recordsTotal > 0"
:rows="pageSize"
:totalRecords="recordsTotal"
:first="(currentPage - 1) * pageSize"
@page="onPageChange"
/>
</div>
<!-- 邀请说明 -->
<div class="info-section">
<h3>邀请说明</h3>
<ul class="info-list">
<li>分享您的邀请链接给好友</li>
<li>好友通过您的邀请链接注册后您将自动获得 <strong>3天试用期</strong> 奖励</li>
<li>每成功邀请一位用户注册您将获得3天试用期</li>
<li>试用期将自动累加到您的账户剩余天数中</li>
</ul>
</div>
<!-- 成功提示 -->
<Message
v-if="showSuccess"
severity="success"
:closable="true"
@close="showSuccess = false"
class="success-message"
>
{{ successMessage }}
</Message>
</div>
</template>
<script>
import inviteAPI from '../api/invite.js';
import { mapState } from 'vuex';
import { Card, Button, Badge, Tag, Paginator, Message, ProgressSpinner } from '../components/PrimeVue';
import logMixin from '../mixins/logMixin.js';
export default {
name: 'InvitePage',
mixins: [logMixin],
components: {
Card,
Button,
Badge,
Tag,
Paginator,
Message,
ProgressSpinner
},
data() {
return {
inviteInfo: {},
statistics: {},
showSuccess: false,
successMessage: '',
recordsList: [],
recordsLoading: false,
recordsTotal: 0,
currentPage: 1,
pageSize: 20
};
},
computed: {
...mapState('auth', ['snCode', 'isLoggedIn']),
totalPages() {
return Math.ceil(this.recordsTotal / this.pageSize);
}
},
watch: {
// 监听登录状态变化
isLoggedIn(newVal) {
if (newVal && this.snCode) {
this.loadInviteInfo();
this.loadStatistics();
this.loadRecords();
}
},
// 监听SN码变化
snCode(newVal) {
if (newVal && this.isLoggedIn) {
this.loadInviteInfo();
this.loadStatistics();
this.loadRecords();
}
}
},
mounted() {
// 确保在登录状态且SN码存在时才加载数据
this.$nextTick(() => {
if (this.isLoggedIn && this.snCode) {
this.loadInviteInfo();
this.loadStatistics();
this.loadRecords();
}
});
},
activated() {
// 当页面被激活时(切换到这个页面时)重新加载邀请信息
// 确保在登录状态且SN码存在时才加载
this.$nextTick(() => {
if (this.isLoggedIn && this.snCode) {
this.loadInviteInfo();
this.loadStatistics();
this.loadRecords();
}
});
},
methods: {
/**
* 加载邀请信息
*/
async loadInviteInfo() {
try {
if (!this.snCode) {
console.warn('SN码不存在无法加载邀请信息');
return;
}
const result = await inviteAPI.getInviteInfo(this.snCode);
// 支持新的响应格式:{ code: 0 } 或旧的格式:{ success: true }
if (result && (result.code === 0 || result.success)) {
this.inviteInfo = result.data || {};
// 如果没有邀请链接,自动生成
if (!this.inviteInfo.invite_link) {
await this.handleGenerateCode();
}
}
} catch (error) {
console.error('加载邀请信息失败:', error);
if (this.addLog) {
this.addLog('error', '加载邀请信息失败: ' + (error.message || '未知错误'));
}
}
},
/**
* 加载统计数据
*/
async loadStatistics() {
try {
if (!this.snCode) {
return;
}
const result = await inviteAPI.getStatistics(this.snCode);
// 支持新的响应格式:{ code: 0 } 或旧的格式:{ success: true }
if (result && (result.code === 0 || result.success)) {
this.statistics = result.data;
}
} catch (error) {
console.error('加载统计数据失败:', error);
}
},
/**
* 生成新邀请码
*/
async handleGenerateCode() {
try {
if (!this.snCode) {
console.warn('SN码不存在无法生成邀请码');
return;
}
const result = await inviteAPI.generateInviteCode(this.snCode);
// 支持新的响应格式:{ code: 0 } 或旧的格式:{ success: true }
if (result && (result.code === 0 || result.success)) {
this.inviteInfo = result.data || {};
if (!this.showSuccess) {
this.showSuccessMessage('邀请链接已生成!');
}
}
} catch (error) {
console.error('生成邀请码失败:', error);
if (this.addLog) {
this.addLog('error', '生成邀请码失败: ' + (error.message || '未知错误'));
}
}
},
/**
* 复制邀请码
*/
async handleCopyCode() {
if (!this.inviteInfo.invite_code) return;
try {
if (window.electronAPI && window.electronAPI.clipboard) {
await window.electronAPI.clipboard.writeText(this.inviteInfo.invite_code);
this.showSuccessMessage('邀请码已复制到剪贴板');
} else {
// 使用浏览器API
await navigator.clipboard.writeText(this.inviteInfo.invite_code);
this.showSuccessMessage('邀请码已复制到剪贴板');
}
} catch (error) {
console.error('复制失败:', error);
if (this.addLog) {
this.addLog('error', '复制失败: ' + (error.message || '未知错误'));
}
}
},
/**
* 复制邀请链接
*/
async handleCopyLink() {
if (!this.inviteInfo.invite_link) return;
try {
if (window.electronAPI && window.electronAPI.clipboard) {
await window.electronAPI.clipboard.writeText(this.inviteInfo.invite_link);
this.showSuccessMessage('邀请链接已复制到剪贴板');
} else {
// 使用浏览器API
await navigator.clipboard.writeText(this.inviteInfo.invite_link);
this.showSuccessMessage('邀请链接已复制到剪贴板');
}
} catch (error) {
console.error('复制失败:', error);
if (this.addLog) {
this.addLog('error', '复制失败: ' + (error.message || '未知错误'));
}
}
},
/**
* 显示成功消息
*/
showSuccessMessage(message) {
this.successMessage = message;
this.showSuccess = true;
setTimeout(() => {
this.showSuccess = false;
}, 3000);
},
/**
* 加载邀请记录列表
*/
async loadRecords() {
try {
if (!this.snCode) {
return;
}
this.recordsLoading = true;
const result = await inviteAPI.getRecords(this.snCode, {
page: this.currentPage,
pageSize: this.pageSize
});
if (result && (result.code === 0 || result.success)) {
this.recordsList = result.data?.list || [];
this.recordsTotal = result.data?.total || 0;
}
} catch (error) {
console.error('加载邀请记录列表失败:', error);
if (this.addLog) {
this.addLog('error', '加载邀请记录列表失败: ' + (error.message || '未知错误'));
}
} finally {
this.recordsLoading = false;
}
},
/**
* 切换页码
*/
changePage(page) {
if (page >= 1 && page <= this.totalPages) {
this.currentPage = page;
this.loadRecords();
}
},
/**
* Paginator 分页事件
*/
onPageChange(event) {
this.currentPage = Math.floor(event.first / this.pageSize) + 1;
this.loadRecords();
},
/**
* 格式化时间
*/
formatTime(time) {
if (!time) return '-';
const date = new Date(time);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
}
};
</script>
<style lang="less" scoped>
.page-invite {
padding: 20px;
height: 100%;
overflow-y: auto;
}
.page-title {
margin: 0 0 20px 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.invite-card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #eee;
h3 {
margin: 0;
font-size: 18px;
color: #333;
}
}
.card-body {
padding: 20px;
}
.invite-code-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.invite-tip {
margin-top: 20px;
padding: 15px;
background: #e8f5e9;
border-radius: 4px;
border-left: 4px solid #4CAF50;
p {
margin: 0;
color: #2e7d32;
font-size: 14px;
line-height: 1.6;
strong {
color: #1b5e20;
font-weight: 600;
}
}
}
.code-display,
.link-display {
display: flex;
align-items: center;
gap: 10px;
padding: 15px;
background: #f5f5f5;
border-radius: 4px;
}
.code-label,
.link-label {
font-weight: 600;
color: #333;
min-width: 80px;
}
.code-value,
.link-value {
flex: 1;
font-family: 'Courier New', monospace;
color: #4CAF50;
font-size: 16px;
word-break: break-all;
}
.btn-copy {
padding: 6px 12px;
background: #4CAF50;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
&:hover {
background: #45a049;
}
}
.stats-section {
display: flex;
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
flex: 1;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
text-align: center;
max-width: 300px;
}
.records-section {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
padding: 20px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
h3 {
margin: 0;
font-size: 18px;
color: #333;
display: flex;
align-items: center;
gap: 10px;
}
.stat-value {
display: inline-block;
padding: 2px 10px;
background: #4CAF50;
color: #fff;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
min-width: 24px;
text-align: center;
line-height: 1.5;
}
}
.btn-refresh {
padding: 6px 12px;
background: #4CAF50;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
&:hover:not(:disabled) {
background: #45a049;
}
&:disabled {
background: #ccc;
cursor: not-allowed;
}
}
.loading-tip,
.empty-tip {
text-align: center;
padding: 40px 20px;
color: #999;
font-size: 14px;
}
.records-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.record-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #f9f9f9;
border-radius: 4px;
border-left: 3px solid #4CAF50;
transition: background 0.3s;
&:hover {
background: #f0f0f0;
}
}
.record-info {
flex: 1;
.record-phone {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 5px;
}
.record-time {
font-size: 12px;
color: #999;
}
}
.record-status {
display: flex;
align-items: center;
gap: 10px;
}
.status-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
&.status-success {
background: #e8f5e9;
color: #2e7d32;
}
&.status-pending {
background: #fff3e0;
color: #e65100;
}
}
.reward-info {
font-size: 14px;
color: #4CAF50;
font-weight: 600;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.page-btn {
padding: 6px 16px;
background: #4CAF50;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
&:hover:not(:disabled) {
background: #45a049;
}
&:disabled {
background: #ccc;
cursor: not-allowed;
}
}
.page-info {
font-size: 14px;
color: #666;
}
.stat-label {
font-size: 14px;
color: #666;
}
.info-section {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
h3 {
margin: 0 0 15px 0;
font-size: 18px;
color: #333;
}
}
.info-list {
margin: 0;
padding-left: 20px;
li {
margin-bottom: 10px;
color: #666;
line-height: 1.6;
}
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
&.btn-primary {
background: #4CAF50;
color: #fff;
&:hover {
background: #45a049;
}
}
}
.success-message {
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: #fff;
padding: 12px 20px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
</style>