734 lines
16 KiB
Vue
734 lines
16 KiB
Vue
<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>
|