1
This commit is contained in:
733
app/views/InvitePage.vue
Normal file
733
app/views/InvitePage.vue
Normal 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>
|
||||
Reference in New Issue
Block a user