Files
autoAiWorkSys/app/views/InvitePage.vue
张成 e17d5610f5 1
2025-12-22 16:26:59 +08:00

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