更新版

This commit is contained in:
tianfeng 2025-08-14 23:35:56 +08:00
parent 819ad5a45b
commit a04b27b0c3
13 changed files with 2677 additions and 799 deletions

322
src/api/teamBill.js Normal file
View File

@ -0,0 +1,322 @@
import { get, post } from '../utils/http.js'
/**
* 获取团队成员列表
* @param {string} teamId - 团队ID
* @returns {Promise}
*/
export const getTeamMembers = async (teamId) => {
try {
const response = await get(`/team/members?id=${teamId}`)
return response
} catch (error) {
console.error('Failed to fetch team members:', error)
throw error
}
}
/**
* 删除团队成员
* @param {string} memberId - 成员ID
* @returns {Promise}
*/
export const deleteMember = async (memberId) => {
try {
const response = await post('/team/member/remove', {
memberId: memberId
})
return response
} catch (error) {
console.error('Failed to delete member:', error)
throw error
}
}
/**
* 获取团队发布的政策
* @param {string} teamId - 团队ID
* @returns {Promise}
*/
export const getTeamReleasePolicy = async (teamId) => {
try {
const response = await get(`/team/release/policy?id=${teamId}`)
return response
} catch (error) {
console.error('Failed to fetch team release policy:', error)
throw error
}
}
/**
* 获取用户等待申请的团队信息
* @returns {Promise}
*/
export const getUserSendJoinWaitApplyTeamProfile = async () => {
try {
const response = await get('/team/wait-apply/profile')
return response
} catch (error) {
console.error('Failed to get user wait apply team profile:', error)
throw error
}
}
/**
* 搜索团队信息
* @param {string} account - 团队账户
* @returns {Promise}
*/
export const searchTeamByAccount = async (account) => {
try {
const response = await get(`/team/search-account?account=${account}`)
return response
} catch (error) {
console.error('Failed to search team by account:', error)
throw error
}
}
/**
* 提交团队申请
* @param {string} teamId - 团队ID
* @returns {Promise}
*/
export const sendTeamApplyJoin = async (teamId) => {
try {
const response = await post('/team/user/apply', {
teamId,
reason: 'JOIN'
})
return response
} catch (error) {
console.error('Failed to send team apply join:', error)
throw error
}
}
/**
* 取消加入申请
* @param {string} messageId - 申请消息ID
* @returns {Promise}
*/
export const cancelJoinApplyById = async (messageId) => {
try {
const response = await post('/team/user/join-apply-cancel', {
id: messageId
})
return response
} catch (error) {
console.error('Failed to cancel join apply:', error)
throw error
}
}
/**
* 获取邀请消息列表
* @returns {Promise}
*/
export const getInviteMessageList = async () => {
try {
const response = await get('/anchor-agent/invite/message')
return response
} catch (error) {
console.error('Failed to get invite message list:', error)
throw error
}
}
/**
* 处理邀请消息
* @param {object} data - 处理数据 {id, status, contact}
* @returns {Promise}
*/
export const processInviteMessage = async (data) => {
try {
const response = await post('/anchor-agent/invite/process', data)
return response
} catch (error) {
console.error('Failed to process invite message:', error)
throw error
}
}
/**
* 获取团队成员工作报告
* @param {string} userId - 用户ID
* @returns {Promise}
*/
export const getTeamMemberWork = async (userId) => {
try {
const response = await get(`/team/member-work?userId=${userId}`)
return response
} catch (error) {
console.error('Failed to fetch team member work:', error)
throw error
}
}
/**
* 格式化显示状态
* @param {string} status - 状态值
* @returns {object} 包含显示文本和样式类的对象
*/
export const formatBillStatus = (status) => {
const statusMap = {
'UNPAID': {
text: 'In Progress',
class: 'in-progress',
color: '#3B82F6'
},
'HANG_UP': {
text: 'Pending',
class: 'pending',
color: '#EF4444'
},
'PAID': {
text: 'Completed',
class: 'completed',
color: '#10B981'
},
'SETTLED': {
text: 'Settled',
class: 'settled',
color: '#059669'
}
}
return statusMap[status] || {
text: status,
class: 'unknown',
color: '#666'
}
}
/**
* 格式化时间显示
* @param {number|string} minutes - 分钟数
* @returns {string} 格式化的时间字符串
*/
export const formatOnlineTime = (minutes) => {
if (!minutes || minutes === '0') return '0h'
const numMinutes = parseInt(minutes)
const hours = Math.floor(numMinutes / 60)
const mins = numMinutes % 60
if (hours > 0) {
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`
}
return `${mins}m`
}
/**
* 格式化礼物价值显示
* @param {string|number} value - 礼物价值
* @returns {string} 格式化的价值字符串
*/
export const formatGiftValue = (value) => {
if (!value || value === '0') return '$0'
// 如果已经是格式化的字符串,直接返回
if (typeof value === 'string' && value.includes('K')) {
return `$${value}`
}
const numValue = parseFloat(value)
if (isNaN(numValue)) return '$0'
if (numValue >= 1000000) {
return `$${(numValue / 1000000).toFixed(1)}M`
} else if (numValue >= 1000) {
return `$${(numValue / 1000).toFixed(1)}K`
}
return `$${numValue.toFixed(0)}`
}
/**
* 格式化钻石价值显示
* @param {string|number} value - 钻石价值
* @returns {string} 格式化的钻石价值字符串
*/
export const formatDiamondValue = (value) => {
if (!value || value === '0') return '0'
const numValue = parseInt(value)
if (isNaN(numValue)) return '0'
if (numValue >= 1000000) {
return `${(numValue / 1000000).toFixed(1)}M`
} else if (numValue >= 1000) {
return `${(numValue / 1000).toFixed(1)}K`
}
return numValue.toLocaleString()
}
/**
* 格式化日期显示
* @param {number} dateNumber - 日期数字格式 (: 20250808)
* @returns {string} 格式化的日期字符串
*/
export const formatDateDisplay = (dateNumber) => {
if (!dateNumber) return ''
const dateStr = dateNumber.toString()
if (dateStr.length !== 8) return dateStr
const year = dateStr.substring(0, 4)
const month = dateStr.substring(4, 6)
const day = dateStr.substring(6, 8)
return `${month}/${day}/${year}`
}
/**
* 格式化账户显示
* @param {object} memberProfile - 成员资料对象
* @returns {string} 格式化的账户字符串
*/
export const formatAccount = (memberProfile) => {
if (!memberProfile) return ''
// 如果有特殊ID优先显示特殊ID
if (memberProfile.ownSpecialId && memberProfile.ownSpecialId.account) {
return memberProfile.ownSpecialId.account
}
// 否则显示普通账户
return memberProfile.account || ''
}
/**
* 检查是否可以领取道具奖励
* @param {object} item - 账单项目
* @returns {boolean} 是否可以领取奖励
*/
export const canReceivePropsReward = (item) => {
if (item.status !== 'SETTLED') {
return false
}
if (!item.target || !item.target.settlementResult) {
return false
}
const propsRewards = item.target.settlementResult.propsRewards
if (!propsRewards || propsRewards.length <= 0) {
return false
}
return true
}
/**
* 领取政策道具奖励
* @param {string} targetId - 目标ID
* @returns {Promise}
*/
export const receivePolicyPropsReward = async (targetId) => {
try {
const response = await post('/team/policy-props-reward', {
id: targetId
})
return response
} catch (error) {
console.error('Failed to receive policy props reward:', error)
throw error
}
}

View File

@ -1,11 +1,11 @@
import { get } from '../utils/http.js'
import {get, post} from '../utils/http.js'
/**
* 获取钱包流水详情
* @returns {Promise} 返回流水详情数据
*/
export function getWalletDetails() {
return get('/wallet/salary-diamond/balance/water/details')
return get('/wallet/bank/balance/water/details')
}
/**
@ -24,6 +24,25 @@ export function userBankCheckTransfer() {
return get('/wallet/bank/check/transfer')
}
/**
* 验证兑换金币限制
* @returns {Promise} 返回用户角色和权限信息
*/
export function userSalaryCheckExchange() {
return get('/wallet/salary-diamond/check/exchange-gold')
}
// 获取用户银行卡余额-兑换金币
export function userBankExchangeGold(data) {
return post('/wallet/bank/exchange/gold', data)
}
// 用户银行卡余额-转账
export function userBankTransfer(data) {
return post('/wallet/bank/transfer', data)
}
/**
* 搜索用户资料
* @param {string} type - 用户类型 (AGENT/BD/ANCHOR)
@ -46,7 +65,7 @@ export function formatDateTime(timestamp) {
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${day}/${month}/${year} ${hours}:${minutes}`
}

View File

@ -0,0 +1,532 @@
<template>
<div class="team-bill-more">
<div class="bill-header">
<h2>{{ billData?.billTitle }}</h2>
<span :class="['bill-status', getStatusClass(billData?.status)]">
{{ getStatusText(billData?.status) }}
</span>
</div>
<!-- 账单汇总信息 -->
<div class="bill-summary-card">
<h3>Summary</h3>
<div class="summary-grid">
<div class="summary-item">
<span class="summary-label">Gift Value:</span>
<span class="summary-value">{{ formatGiftValue(billData?.target?.acceptGiftValue) }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Online Time:</span>
<span class="summary-value">{{ formatOnlineTime(billData?.target?.onlineTime) }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Diamond:</span>
<span class="summary-value diamond-value">{{ formatDiamondValue(billData?.target?.acceptDiamondValue) }}</span>
</div>
<div class="summary-item">
<span class="summary-label">Effective Days:</span>
<span class="summary-value">{{ billData?.target?.effectiveDay || 0 }}</span>
</div>
</div>
<!-- 结算结果如果已结算 -->
<div v-if="billData?.status === 'SETTLED' && billData?.target?.settlementResult" class="settlement-info">
<div class="settlement-row">
<span class="settlement-label">Target Level:</span>
<span class="settlement-value">{{ billData.target.settlementResult.level || '' }}</span>
</div>
<div class="settlement-row">
<span class="settlement-label">Member Salary:</span>
<span class="settlement-value salary-value">${{ billData.target.settlementResult.memberSalary || 0 }}</span>
</div>
<!-- 道具奖励 -->
<div v-if="canReceivePropsReward(billData)" class="props-reward-section">
<div class="props-reward-header">
<span class="props-label">
{{ billData.target.settlementResult.propsReceived ? 'Props Received:' : 'Props Reward:' }}
</span>
<button
v-if="!billData.target.settlementResult.propsReceived"
class="receive-props-btn"
@click="receivePropsReward"
:disabled="receivingReward"
>
{{ receivingReward ? 'Receiving...' : 'Receive Props' }}
</button>
</div>
<div class="props-list">
<div
v-for="(prop, index) in billData.target.settlementResult.propsRewards"
:key="index"
class="prop-item"
>
<div class="prop-info">
<span class="prop-name">{{ prop.name }}</span>
<span class="prop-type">{{ prop.type }}</span>
</div>
<div class="prop-value">${{ prop.amount || 0 }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- 每日详情 -->
<div class="daily-details-card">
<h3>Daily Details</h3>
<div v-if="billData?.target?.dailyTargets?.length > 0" class="daily-list">
<div
v-for="(daily, index) in billData.target.dailyTargets"
:key="index"
class="daily-item"
>
<div class="daily-header">
<span class="daily-date">{{ formatDate(daily.dateNumber) }}</span>
<span class="daily-time">{{ formatCreateTime(daily.createTime) }}</span>
</div>
<div class="daily-stats">
<div class="daily-stat">
<span class="stat-label">Gift:</span>
<span class="stat-value">{{ formatGiftValue(daily.acceptGiftValue) }}</span>
</div>
<div class="daily-stat">
<span class="stat-label">Time:</span>
<span class="stat-value">{{ formatOnlineTime(daily.onlineTime) }}</span>
</div>
<div class="daily-stat">
<span class="stat-label">Diamond:</span>
<span class="stat-value diamond-value">{{ formatDiamondValue(daily.acceptDiamondValue) }}</span>
</div>
</div>
</div>
</div>
<div v-else class="no-daily-data">
<div class="no-data-icon">📅</div>
<p>No daily data available</p>
</div>
</div>
<!-- 关闭按钮 -->
<div class="close-button-container">
<button class="close-btn" @click="$emit('close')">Close</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import {
formatGiftValue,
formatOnlineTime,
formatDiamondValue,
formatDateDisplay,
canReceivePropsReward,
receivePolicyPropsReward
} from '../../api/teamBill.js'
const props = defineProps({
billData: {
type: Object,
required: true
}
})
const emit = defineEmits(['close', 'propsReceived'])
const receivingReward = ref(false)
//
const getStatusText = (status) => {
const statusMap = {
'UNPAID': 'In Progress',
'HANG_UP': 'Pending',
'PAID': 'Completed',
'SETTLED': 'Settled'
}
return statusMap[status] || status
}
//
const getStatusClass = (status) => {
const statusClassMap = {
'UNPAID': 'in-progress',
'HANG_UP': 'pending',
'PAID': 'completed',
'SETTLED': 'settled'
}
return statusClassMap[status] || 'unknown'
}
//
const formatDate = (dateNumber) => {
return formatDateDisplay(dateNumber)
}
//
const formatCreateTime = (timestamp) => {
if (!timestamp) return ''
const date = new Date(timestamp)
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit'
})
}
//
const receivePropsReward = async () => {
if (!props.billData?.target?.id || receivingReward.value) {
return
}
try {
receivingReward.value = true
await receivePolicyPropsReward(props.billData.target.id)
//
if (props.billData.target.settlementResult) {
props.billData.target.settlementResult.propsReceived = true
}
emit('propsReceived')
} catch (error) {
console.error('Failed to receive props reward:', error)
alert('Failed to receive props reward. Please try again.')
} finally {
receivingReward.value = false
}
}
</script>
<style scoped>
.team-bill-more {
background-color: #f1f2f3;
padding: 20px;
min-height: 100%;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
/* 账单头部 */
.bill-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.bill-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #333;
}
.bill-status {
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
}
.bill-status.in-progress {
background-color: #DBEAFE;
color: #1D4ED8;
}
.bill-status.pending {
background-color: #FEF3C7;
color: #D97706;
}
.bill-status.completed {
background-color: #D1FAE5;
color: #059669;
}
.bill-status.settled {
background-color: #E0E7FF;
color: #5B21B6;
}
/* 卡片样式 */
.bill-summary-card,
.daily-details-card {
background-color: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.bill-summary-card h3,
.daily-details-card h3 {
margin: 0 0 16px 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
/* 汇总网格 */
.summary-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
.summary-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.summary-label {
font-size: 12px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.summary-value {
font-size: 16px;
font-weight: 600;
color: #333;
}
.diamond-value {
color: #1E40AF !important;
}
/* 结算信息 */
.settlement-info {
border-top: 1px solid #e5e7eb;
padding-top: 16px;
}
.settlement-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.settlement-label {
font-size: 14px;
color: #666;
}
.settlement-value {
font-size: 14px;
font-weight: 600;
color: #333;
}
.salary-value {
color: #059669 !important;
}
/* 道具奖励区域 */
.props-reward-section {
margin-top: 16px;
padding: 16px;
background-color: #f9fafb;
border-radius: 8px;
}
.props-reward-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.props-label {
font-size: 14px;
font-weight: 600;
color: #333;
}
.receive-props-btn {
background-color: #3B82F6;
color: white;
border: none;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s;
}
.receive-props-btn:hover:not(:disabled) {
background-color: #2563EB;
}
.receive-props-btn:disabled {
background-color: #9CA3AF;
cursor: not-allowed;
}
.props-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.prop-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
background-color: white;
border-radius: 6px;
}
.prop-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.prop-name {
font-size: 14px;
font-weight: 500;
color: #333;
}
.prop-type {
font-size: 12px;
color: #666;
}
.prop-value {
font-size: 14px;
font-weight: 600;
color: #059669;
}
/* 每日列表 */
.daily-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.daily-item {
background-color: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
}
.daily-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.daily-date {
font-size: 14px;
font-weight: 600;
color: #333;
}
.daily-time {
font-size: 12px;
color: #666;
}
.daily-stats {
display: flex;
justify-content: space-between;
gap: 16px;
}
.daily-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.stat-label {
font-size: 12px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 14px;
font-weight: 600;
color: #333;
}
/* 无数据状态 */
.no-daily-data {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
color: #666;
}
.no-data-icon {
font-size: 48px;
margin-bottom: 12px;
}
.no-daily-data p {
margin: 0;
font-size: 14px;
}
/* 关闭按钮 */
.close-button-container {
display: flex;
justify-content: center;
padding: 20px 0;
}
.close-btn {
background-color: #6B7280;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.close-btn:hover {
background-color: #4B5563;
}
/* 响应式设计 */
@media (max-width: 480px) {
.summary-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.daily-stats {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.daily-stat {
flex-direction: row;
justify-content: space-between;
}
.props-reward-header {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
}
</style>

View File

@ -29,11 +29,7 @@ const router = createRouter({
name: 'host-setting',
component: () => import('../views/HostSettingView.vue'),
},
{
path: '/history-salary',
name: 'history-salary',
component: () => import('../views/HistorySalaryView.vue'),
},
{
path: '/transfer',
name: 'transfer',

View File

@ -3,7 +3,7 @@ const API_BASE_URL = 'https://api.likeichat.com'
// 公共请求头配置
const COMMON_HEADERS = {
'authorization': 'Bearer 1850459451220E4F1FABF65F36A43E76.djIlM0ExOTU0MDU5NzkzOTYzMzkzMDI1JTNBTElLRUklM0ExNzU3NjQ4ODMwMjM3JTNBMTc1NTA1NjgzMDIzNw==',
'authorization': 'Bearer F77126B08E13987C3AD88B6015C5B777.djIlM0ExOTU0MDU5NzkzOTYzMzkzMDI1JTNBTElLRUklM0ExNzU3NzYzODU5NzE1JTNBMTc1NTE3MTg1OTcxNQ==',
'req-app-intel': 'version=1.0.0;build=1;model=SM-G9550;sysVersion=9;channel=Google',
'req-client': 'Android',
'req-imei': '8fa54d728ab449e04f9292329ed44bda',
@ -32,6 +32,7 @@ export async function request(url, options = {}) {
const config = {
method,
headers: {
'Content-Type': 'application/json',
...COMMON_HEADERS,
...headers
},

View File

@ -139,7 +139,11 @@
<div class="team-member-section">
<div class="team-member-card" @click="goToTeamMember">
<div class="card-content">
<span class="member-title">Team Member ({{ teamMemberCount }})</span>
<span class="member-title">
Team Member
<span v-if="loadingMemberCount">(Loading...)</span>
<span v-else>({{ teamMemberCount }})</span>
</span>
<span class="arrow">></span>
</div>
</div>
@ -159,9 +163,10 @@
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import { get } from '../utils/http.js'
const router = useRouter()
@ -172,7 +177,8 @@ const currentIdentity = ref('agency')
const taskTab = ref('agency')
//
const teamMemberCount = ref(87)
const teamMemberCount = ref(0)
const loadingMemberCount = ref(false)
//
const salaryInfo = reactive({
@ -259,6 +265,33 @@ const transfer = () => {
const goToPersonalSalary = () => {
router.push('/platform-policy')
}
//
const fetchTeamMemberCount = async () => {
try {
loadingMemberCount.value = true
// 使teamId使ID
const teamId = '1927993836921233409'
const response = await get(`/team/members/count?id=${teamId}`)
if (response.status && typeof response.body === 'number') {
teamMemberCount.value = response.body
} else {
teamMemberCount.value = 0
}
} catch (error) {
console.error('Failed to fetch team member count:', error)
teamMemberCount.value = 0
} finally {
loadingMemberCount.value = false
}
}
//
onMounted(() => {
fetchTeamMemberCount()
})
</script>
<style scoped>

View File

@ -1,10 +1,10 @@
<template>
<div class="apply-page gradient-background-circles">
<!-- 顶部导航 -->
<MobileHeader
title="Apply to join the team"
:show-help="true"
@help="showHelp"
<MobileHeader
title="Apply to join the team"
:show-help="true"
@help="showHelp"
/>
<!-- 主要内容 -->
@ -20,9 +20,9 @@
<div class="form-group">
<label class="form-label">Apply ID:</label>
<div class="input-group">
<input
v-model="agentId"
type="text"
<input
v-model="agentId"
type="text"
placeholder="Please enter the agent ID"
class="form-input"
/>
@ -45,50 +45,283 @@
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import { get, post } from '../utils/http.js'
const router = useRouter()
const agentId = ref('')
const showHelpModal = ref(false)
//
//
const searchAccount = ref('')
const searching = ref(false)
const applying = ref(false)
const cancelling = ref(false)
const showHelpModal = ref(false)
const showInviteModal = ref(false)
const showContactModal = ref(false)
const teamProfile = reactive({})
const teamOwnUserProfile = reactive({})
const applyMessageId = ref('')
const inviteMessage = ref([])
const inviteMessageLoading = ref(false)
const processingItem = reactive({})
const contactForm = reactive({
id: '',
status: '',
contact: ''
})
//
const hasUnreadInvites = computed(() => {
return inviteMessage.value.some(msg => msg.status === 0)
})
//
const loadUserSendJoinWaitApplyTeamProfile = async () => {
try {
const response = await get('/team/wait-apply/profile')
if (response.status && response.body) {
const result = response.body
applyMessageId.value = result.messageId || ''
Object.assign(teamProfile, result.teamProfile || {})
Object.assign(teamOwnUserProfile, result.ownUserProfile || {})
}
} catch (error) {
console.error('Failed to load wait apply profile:', error)
if (error.errorCode === 6001) {
//
router.replace('/host-center')
}
}
}
//
const searchTeam = async () => {
if (!searchAccount.value.trim()) {
alert('Please enter agent ID')
return
}
try {
searching.value = true
const response = await get(`/team/search-account?account=${searchAccount.value.trim()}`)
if (response.status && response.body) {
const result = response.body
Object.assign(teamProfile, result.teamProfile || {})
Object.assign(teamOwnUserProfile, result.ownUserProfile || {})
}
} catch (error) {
console.error('Failed to search team:', error)
if (error.errorCode === 6010) {
alert('Team not found!')
} else if (error.errorMsg) {
alert(error.errorMsg)
}
} finally {
searching.value = false
}
}
//
const submitApplyJoin = async () => {
if (!teamProfile.id) {
alert('Invalid team information')
return
}
try {
applying.value = true
const response = await post('/team/user/apply', {
teamId: teamProfile.id,
reason: 'JOIN'
})
if (response.status && response.body) {
applyMessageId.value = response.body
alert('Application submitted successfully!')
}
} catch (error) {
console.error('Failed to apply:', error)
if (error.errorMsg) {
alert(error.errorMsg)
}
} finally {
applying.value = false
}
}
//
const cancelApplication = async () => {
if (!applyMessageId.value) {
return
}
try {
cancelling.value = true
await post('/team/user/join-apply-cancel', {
id: applyMessageId.value
})
//
Object.assign(teamProfile, {})
Object.assign(teamOwnUserProfile, {})
applyMessageId.value = ''
alert('Application cancelled successfully')
} catch (error) {
console.error('Failed to cancel application:', error)
} finally {
cancelling.value = false
}
}
//
const loadInviteMessage = async () => {
try {
inviteMessageLoading.value = true
const response = await get('/anchor-agent/invite/message')
if (response.status && response.body) {
inviteMessage.value = response.body || []
}
} catch (error) {
console.error('Failed to load invite messages:', error)
inviteMessage.value = []
} finally {
inviteMessageLoading.value = false
}
}
//
const processInvite = async (type, item) => {
Object.assign(processingItem, item)
contactForm.id = item.id
if (type === 'PASS') {
contactForm.status = 1
showContactModal.value = true
return
}
contactForm.status = 2
await submitInviteProcess()
}
//
const submitInviteProcess = async () => {
try {
const response = await post('/anchor-agent/invite/process', contactForm)
if (response.status) {
processingItem.status = contactForm.status
closeContactModal()
if (contactForm.status === 1) {
//
router.replace('/host-center')
}
}
} catch (error) {
console.error('Failed to process invite:', error)
if (error.errorMsg) {
alert(error.errorMsg)
}
}
}
//
const getAvatarColor = (name) => {
if (!name) return '#8B5CF6'
const colors = ['#8B5CF6', '#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6']
const index = name.charCodeAt(0) % colors.length
return colors[index]
}
const formatAccount = (userProfile) => {
if (!userProfile) return ''
if (userProfile.ownSpecialId && userProfile.ownSpecialId.account) {
return userProfile.ownSpecialId.account
}
return userProfile.account || ''
}
const truncateName = (name) => {
if (!name) return ''
return name.length > 12 ? name.substring(0, 12) + '...' : name
}
//
const showHelp = () => {
showHelpModal.value = true
}
//
const submitApplication = () => {
if (!agentId.value.trim()) {
alert('Please enter agent ID')
const goBackToSearch = () => {
Object.assign(teamProfile, {})
Object.assign(teamOwnUserProfile, {})
applyMessageId.value = ''
}
const showInviteMessages = () => {
showInviteModal.value = true
loadInviteMessage()
}
const closeInviteModal = () => {
showInviteModal.value = false
}
const closeContactModal = () => {
showContactModal.value = false
contactForm.contact = ''
}
const submitContactInfo = () => {
if (!contactForm.contact.trim()) {
alert('Please enter WhatsApp number')
return
}
//
alert(`Application submitted with Agent ID: ${agentId.value}`)
//
// router.push('/host-center')
submitInviteProcess()
}
//
onMounted(() => {
loadUserSendJoinWaitApplyTeamProfile()
loadInviteMessage()
})
</script>
<style scoped>
.apply-page {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
min-height: 100vh;
background-color: #f1f2f3;
}
.content {
padding: 16px;
padding: 16px 16px 100px 16px;
position: relative;
z-index: 2;
}
/* 搜索区域 */
.search-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.reminder {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
@ -142,13 +375,13 @@ const submitApplication = () => {
.form-input:focus {
outline: none;
border-color: #007AFF;
border-color: #8B5CF6;
background-color: white;
}
.apply-btn {
padding: 12px 24px;
background-color: #8B5CF6;
background-color: #10B981;
color: white;
border: none;
border-radius: 8px;
@ -156,18 +389,202 @@ const submitApplication = () => {
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
white-space: nowrap;
}
.apply-btn:hover:not(:disabled) {
background-color: #7C3AED;
background-color: #059669;
}
.apply-btn:disabled {
background-color: #ccc;
background-color: #9CA3AF;
cursor: not-allowed;
}
/* 弹窗样式 */
.apply-tips {
background-color: #F0F9FF;
padding: 12px;
border-radius: 8px;
border-left: 4px solid #3B82F6;
}
.apply-tips p {
margin: 0;
font-size: 14px;
color: #1E40AF;
}
/* 团队信息区域 */
.team-profile-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.team-profile-card {
background-color: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-align: center;
}
.team-owner-info {
margin-bottom: 24px;
}
.avatar-wrapper {
margin: 0 auto 12px auto;
width: 80px;
height: 80px;
}
.owner-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
}
.name-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 32px;
font-weight: 600;
}
.owner-details {
text-align: center;
}
.owner-name {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.owner-id {
font-size: 14px;
color: #666;
}
.action-buttons {
display: flex;
gap: 12px;
justify-content: center;
}
.btn-cancel,
.btn-confirm,
.btn-cancel-apply {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-cancel {
background-color: #F3F4F6;
color: #374151;
}
.btn-cancel:hover {
background-color: #E5E7EB;
}
.btn-confirm {
background-color: #10B981;
color: white;
}
.btn-confirm:hover:not(:disabled) {
background-color: #059669;
}
.btn-cancel-apply {
background-color: #EF4444;
color: white;
}
.btn-cancel-apply:hover:not(:disabled) {
background-color: #DC2626;
}
.btn-cancel:disabled,
.btn-confirm:disabled,
.btn-cancel-apply:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 底部消息栏 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: white;
border-top: 1px solid #E5E7EB;
padding: 12px 16px;
z-index: 10;
}
.message-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 12px;
background-color: #8B5CF6;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
position: relative;
transition: background-color 0.2s;
}
.message-btn:hover {
background-color: #7C3AED;
}
.message-btn.has-unread {
background-color: #EF4444;
}
.message-btn.has-unread:hover {
background-color: #DC2626;
}
.unread-badge {
position: absolute;
top: -4px;
right: -4px;
background-color: #FBBF24;
color: #92400E;
width: 20px;
height: 20px;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
}
/* 模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
@ -182,43 +599,332 @@ const submitApplication = () => {
padding: 16px;
}
.modal {
.help-modal,
.invite-modal,
.contact-modal {
background: white;
padding: 20px;
border-radius: 12px;
width: 100%;
max-width: 300px;
text-align: center;
max-width: 400px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal h3 {
.help-modal {
padding: 20px;
text-align: center;
max-width: 300px;
}
.help-modal h3 {
margin: 0 0 16px 0;
font-size: 18px;
}
.modal p {
.help-modal p {
margin: 0 0 20px 0;
font-size: 14px;
color: #666;
}
.modal button {
.help-modal button {
padding: 8px 20px;
background-color: #007AFF;
background-color: #8B5CF6;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
/* 手机适配 */
.input-group {
flex-direction: column;
align-items: stretch;
/* 邀请和联系模态框 */
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #E5E7EB;
}
.apply-btn {
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
padding: 16px;
overflow-y: auto;
flex: 1;
}
/* 邀请列表 */
.invite-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.invite-item {
background-color: #F6FAFA;
padding: 16px;
border-radius: 8px;
}
.invite-profile {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.invite-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.invite-details {
flex: 1;
}
.invite-name {
font-size: 14px;
font-weight: 600;
color: #333;
}
.invite-id,
.invite-country {
font-size: 12px;
color: #666;
margin-top: 2px;
}
.invite-status {
margin-bottom: 8px;
}
.status-pending {
background-color: #FEF3C7;
color: #D97706;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.status-passed {
background-color: #D1FAE5;
color: #059669;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.status-refused {
background-color: #FEE2E2;
color: #DC2626;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.invite-message-text {
font-size: 14px;
color: #666;
margin-bottom: 12px;
word-break: break-word;
}
.invite-actions {
display: flex;
gap: 8px;
}
.btn-refuse,
.btn-accept {
flex: 1;
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-refuse {
background-color: #FEE2E2;
color: #DC2626;
}
.btn-refuse:hover {
background-color: #FECACA;
}
.btn-accept {
background-color: #D1FAE5;
color: #059669;
}
.btn-accept:hover {
background-color: #A7F3D0;
}
/* 联系方式表单 */
.contact-profile {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.contact-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.contact-details {
flex: 1;
}
.contact-name {
font-size: 14px;
font-weight: 600;
color: #333;
}
.contact-id {
font-size: 12px;
color: #666;
margin-top: 2px;
}
.contact-form .form-group {
margin-bottom: 16px;
}
.contact-form label {
display: block;
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 6px;
}
.contact-input {
width: 100%;
margin-top: 8px;
padding: 8px 12px;
border: 1px solid #D1D5DB;
border-radius: 6px;
font-size: 14px;
}
.contact-input:focus {
outline: none;
border-color: #8B5CF6;
}
.form-actions {
display: flex;
gap: 12px;
}
.btn-submit {
flex: 1;
padding: 8px 16px;
background-color: #8B5CF6;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
.btn-submit:hover:not(:disabled) {
background-color: #7C3AED;
}
.btn-submit:disabled {
background-color: #9CA3AF;
cursor: not-allowed;
}
/* 加载和空状态 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
color: #666;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #8B5CF6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.empty-invites {
text-align: center;
padding: 40px 20px;
color: #666;
}
.empty-invites p {
margin: 0;
font-size: 16px;
}
/* 响应式设计 */
@media (max-width: 480px) {
.input-group {
flex-direction: column;
align-items: stretch;
}
.apply-btn {
width: 100%;
margin-top: 8px;
}
.action-buttons {
flex-direction: column;
}
.bottom-bar {
padding-bottom: calc(12px + env(safe-area-inset-bottom));
}
}
</style>

View File

@ -20,11 +20,11 @@
<!-- 兑换部分 -->
<div class="exchange-section">
<h3>Exchange Gold Coins</h3>
<!-- 金币选择 -->
<div class="coins-grid">
<div
v-for="coin in coinOptions"
<div
v-for="coin in coinOptions"
:key="coin.id"
@click="selectCoin(coin)"
:class="['coin-item', { selected: selectedCoin === coin.id }]"
@ -36,8 +36,8 @@
</div>
<!-- 兑换按钮 -->
<button
class="exchange-btn"
<button
class="exchange-btn"
@click="exchangeCoins"
:disabled="!selectedCoin"
>
@ -52,31 +52,34 @@
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import { getBankBalance } from '../api/wallet.js'
import { getBankBalance, userSalaryCheckExchange, userBankExchangeGold } from '../api/wallet.js'
const router = useRouter()
const availableSalary = ref('0')
const selectedCoin = ref(null)
const loading = ref(false)
const isLoadingRole = ref(false)
const userRole = ref('')
const selectedUserType = ref('')
//
const coinOptions = reactive([
{ id: 1, amount: 10000, price: 1 },
{ id: 2, amount: 10000, price: 1 },
{ id: 3, amount: 10000, price: 1 },
{ id: 4, amount: 10000, price: 1 },
{ id: 5, amount: 10000, price: 1 },
{ id: 6, amount: 10000, price: 1 }
{ id: 2, amount: 50000, price: 5 },
{ id: 3, amount: 100000, price: 10 },
{ id: 4, amount: 500000, price: 50 },
{ id: 5, amount: 1000000, price: 100 },
{ id: 6, amount: 2000000, price: 200 }
])
//
const fetchBankBalance = async () => {
loading.value = true
try {
const response = await getBankBalance()
if (response.status && typeof response.body === 'number') {
// 2
availableSalary.value = response.body.toFixed(2)
@ -93,6 +96,106 @@ const fetchBankBalance = async () => {
}
}
// exchange
// exchange
const checkUserRole = async () => {
isLoadingRole.value = true
try {
const response = await userSalaryCheckExchange()
console.log('sssss', response)
if (response.status && response.body) {
// role
userRole.value = response.body.role || ''
console.log('用户角色:', userRole.value)
//
if (userRole.value === 'TEAM_OWN') {
selectedUserType.value = 'ANCHOR' // TEAM_OWN ANCHOR
} else {
selectedUserType.value = 'AGENT' // AGENT
}
} else {
//
userRole.value = ''
selectedUserType.value = 'USER'
router.back() // 退
}
} catch (error) {
//
if (error.errorCode === 1083) {
// NOT_OPEN_ERROR
alert('Exchange function is not available')
} else if (error.message && error.message.includes('HTTP Error: 406')) {
// 406 Not Acceptable
alert('The server cannot produce a response matching the list of acceptable values defined in the request headers')
} else {
//
alert('Failed to load user information')
}
router.back() // 退
//
userRole.value = ''
selectedUserType.value = 'USER'
} finally {
isLoadingRole.value = false
}
}
const exchangeCoins = async () => {
if (!selectedCoin.value) {
alert('Please select a coin amount first')
return
}
const selectedCoinData = coinOptions.find(coin => coin.id === selectedCoin.value)
//
const coinPrice = selectedCoinData.price
const availableBalance = parseFloat(availableSalary.value)
if (isNaN(availableBalance) || availableBalance < coinPrice) {
alert(`Insufficient balance. Your current balance is $${availableBalance.toFixed(2)}, but you need $${coinPrice} to exchange.`)
return
}
//
const confirmExchange = confirm(`Do you want to exchange ${selectedCoinData.amount} coins for $${selectedCoinData.price}?`)
if (confirmExchange) {
try {
//
const response = await userBankExchangeGold({
coinId: selectedCoinData.id,
amount: selectedCoinData.price,
price: selectedCoinData.price
})
console.log('res', response)
if (response.status === true || response.errorCode === 0) {
alert(`Successfully exchanged ${selectedCoinData.amount} coins for $${selectedCoinData.price}`)
//
await fetchBankBalance()
//
selectedCoin.value = null
} else {
//
const errorMessage = response.message || 'Exchange failed, please try again later.'
alert(errorMessage)
}
} catch (error) {
console.error('Exchange error:', error)
alert('An error occurred during the exchange process. Please try again later.')
}
}
//
}
//
const showDetails = () => {
router.push('/information-details')
@ -107,28 +210,10 @@ const selectCoin = (coin) => {
}
}
const exchangeCoins = () => {
if (!selectedCoin.value) {
alert('Please select a coin amount first')
return
}
const selectedCoinData = coinOptions.find(coin => coin.id === selectedCoin.value)
//
const confirmExchange = confirm(`Do you want to exchange ${selectedCoinData.amount} coins for $${selectedCoinData.price}?`)
if (confirmExchange) {
alert(`Successfully exchanged ${selectedCoinData.amount} coins for $${selectedCoinData.price}`)
//
selectedCoin.value = null
}
}
//
onMounted(() => {
fetchBankBalance()
checkUserRole()
})
</script>

View File

@ -126,14 +126,16 @@
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import { getBankBalance } from '../api/wallet.js'
const router = useRouter()
//
const currentIdentity = ref('host')
const loading = ref(false)
//
const identities = [
@ -142,6 +144,29 @@ const identities = [
{ label: 'Coin Seller', value: 'coin-seller' }
]
//
const fetchBankBalance = async () => {
loading.value = true
try {
const response = await getBankBalance()
if (response.status && typeof response.body === 'number') {
// 2
salaryInfo.currentSalary = response.body.toFixed(2)
} else {
salaryInfo.currentSalary = 0.00
}
} catch (error) {
console.error('Failed to fetch bank balance:', error)
salaryInfo.currentSalary = 0.00
//
alert('Failed to load balance. Please try again.')
} finally {
loading.value = false
}
}
//
const userInfo = reactive({
name: 'User1',
@ -150,8 +175,8 @@ const userInfo = reactive({
//
const salaryInfo = reactive({
hostSalary: 12345,
currentSalary: 111
hostSalary: 120,
currentSalary: 0.00
})
//
@ -207,6 +232,10 @@ const transfer = () => {
router.push('/transfer')
}
//
onMounted(() => {
fetchBankBalance()
})
</script>
<style scoped>

View File

@ -3,89 +3,158 @@
<MobileHeader title="Platform Policy" />
<div class="content">
<div class="policy-list">
<div
v-for="policy in policies"
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-spinner"></div>
<p>Loading policy...</p>
</div>
<!-- 政策列表 -->
<div v-else-if="policies.length > 0" class="policy-list">
<div
v-for="policy in policies"
:key="policy.level"
class="policy-item"
>
<div class="policy-header">
<h3>{{ policy.level }}</h3>
<h3>Lv.{{ policy.level }}</h3>
</div>
<div class="policy-details">
<div class="policy-grid">
<div class="policy-cell">
<span class="cell-label">Time (hours)</span>
<span class="cell-value">{{ policy.timeHours }}</span>
<span class="cell-value">{{ policy.onlineTime || 0 }}</span>
</div>
<div class="policy-cell">
<span class="cell-label">Target</span>
<span class="cell-value">{{ policy.target }}</span>
<span class="cell-value">{{ formatTarget(policy.target) }}</span>
</div>
<div class="policy-cell">
<span class="cell-label">Host Salary</span>
<span class="cell-value">${{ policy.hostSalary }}</span>
<span class="cell-label">Member Salary</span>
<span class="cell-value salary">${{ policy.memberSalary || 0 }}</span>
</div>
</div>
<div class="policy-grid bottom-row">
<!-- 只有角色是 OWN 时才显示额外的薪资信息 -->
<div v-if="role === 'OWN'" class="policy-grid bottom-row">
<div class="policy-cell">
<span class="cell-label">Agent Salary</span>
<span class="cell-value">${{ policy.agentSalary }}</span>
<span class="cell-label">Owner Salary</span>
<span class="cell-value salary">${{ policy.ownSalary || 0 }}</span>
</div>
<div class="policy-cell">
<span class="cell-label">Total Salary</span>
<span class="cell-value">${{ policy.totalSalary }}</span>
<span class="cell-value salary">${{ policy.totalSalary || 0 }}</span>
</div>
<div class="policy-cell empty"></div>
</div>
<!-- 道具奖励区域 -->
<div v-if="policy.propsRewards && policy.propsRewards.length > 0" class="rewards-section">
<div class="rewards-divider"></div>
<div class="rewards-header">
<span class="rewards-title">Rewards:</span>
</div>
<div class="rewards-list">
<div
v-for="(reward, index) in policy.propsRewards"
:key="index"
class="reward-item"
>
<div class="reward-info">
<span class="reward-name">{{ reward.name }}</span>
<span class="reward-type">{{ formatRewardType(reward.type) }}</span>
</div>
<div class="reward-amount">${{ reward.amount || 0 }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else class="empty-state">
<div class="empty-icon">📋</div>
<p>No policy data available</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import { get } from '../utils/http.js'
const router = useRouter()
const loading = ref(false)
const policies = ref([])
const role = ref('')
//
const policies = ref([
{
level: 'Lv.1',
timeHours: 20,
target: 32500,
hostSalary: 3,
agentSalary: 1,
totalSalary: 4
},
{
level: 'Lv.2',
timeHours: 20,
target: 65000,
hostSalary: 6,
agentSalary: 2,
totalSalary: 8
},
{
level: 'Lv.3',
timeHours: 15,
target: 108000,
hostSalary: 10,
agentSalary: 3,
totalSalary: 13
//
const fetchTeamPolicy = async () => {
try {
loading.value = true
// teamId role使
const teamId = router.currentRoute.value.query.teamId || '1927993836921233409'
const userRole = router.currentRoute.value.query.role || 'OWN'
role.value = userRole
const response = await get(`/team/release/policy?id=${teamId}`)
if (response.status && response.body && response.body.policy) {
policies.value = response.body.policy || []
} else {
policies.value = []
}
} catch (error) {
console.error('Failed to fetch team policy:', error)
policies.value = []
} finally {
loading.value = false
}
])
}
//
const formatTarget = (target) => {
if (!target) return '0'
const numTarget = parseFloat(target)
if (isNaN(numTarget)) return target
if (numTarget >= 1000000) {
return `${(numTarget / 1000000).toFixed(1)}M`
} else if (numTarget >= 1000) {
return `${(numTarget / 1000).toFixed(1)}K`
}
return numTarget.toString()
}
//
const formatRewardType = (type) => {
const typeMap = {
'AVATAR_FRAME': 'Avatar Frame',
'RIDE': 'Ride Effect',
'THEME': 'Theme',
'BADGE': 'Badge',
'GIFT': 'Gift'
}
return typeMap[type] || type
}
//
onMounted(() => {
fetchTeamPolicy()
})
</script>
<style scoped>
.platform-policy {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
min-height: 100vh;
background-color: #f1f2f3;
}
.content {
@ -94,6 +163,31 @@ const policies = ref([
z-index: 2;
}
/* 加载状态 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 60px 20px;
color: #666;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #8B5CF6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 政策列表 */
.policy-list {
display: flex;
flex-direction: column;
@ -109,7 +203,8 @@ const policies = ref([
.policy-header {
padding: 16px 16px 8px 16px;
border-bottom: 1px solid #f0f0f0;
background-color: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.policy-header h3 {
@ -139,6 +234,9 @@ const policies = ref([
flex-direction: column;
align-items: center;
text-align: center;
padding: 12px 8px;
background-color: #f1f2f3;
border-radius: 8px;
}
.policy-cell.empty {
@ -153,23 +251,131 @@ const policies = ref([
}
.cell-value {
font-size: 16px;
font-size: 14px;
font-weight: 600;
color: #333;
}
.cell-value.salary {
color: #FF6600;
font-weight: 700;
}
/* 奖励区域 */
.rewards-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #e9ecef;
}
.rewards-divider {
height: 2px;
background: linear-gradient(90deg, #c2d9f0 0%, #03dbb8 100%);
margin-bottom: 12px;
border-radius: 1px;
}
.rewards-header {
margin-bottom: 12px;
}
.rewards-title {
font-size: 14px;
font-weight: 600;
color: #333;
}
.rewards-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.reward-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: linear-gradient(135deg, #c2d9f0 0%, #03dbb8 100%);
border-radius: 8px;
color: white;
}
.reward-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.reward-name {
font-size: 14px;
font-weight: 600;
}
.reward-type {
font-size: 12px;
opacity: 0.9;
}
.reward-amount {
font-size: 14px;
font-weight: 700;
color: white;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-state p {
margin: 0;
font-size: 16px;
}
/* 响应式调整 */
@media (max-width: 480px) {
.policy-grid {
gap: 12px;
}
.cell-label {
font-size: 11px;
}
.cell-value {
font-size: 14px;
font-size: 13px;
}
.policy-cell {
padding: 8px 4px;
}
.reward-item {
padding: 10px;
}
.reward-name {
font-size: 13px;
}
.reward-type {
font-size: 11px;
}
.reward-amount {
font-size: 13px;
}
}
</style>

View File

@ -3,50 +3,67 @@
<MobileHeader title="Team Bill" />
<div class="content">
<!-- 历史团队收入 -->
<div class="history-income-card">
<div class="income-header">
<span class="income-label">History Team Income:</span>
<button class="help-btn" @click="showHelpInfo">?</button>
<!-- 成员资料信息 -->
<div v-if="memberProfile" class="member-profile-card">
<div class="member-info">
<div class="avatar-wrapper">
<img :src="memberProfile.userAvatar" class="member-avatar" alt="Avatar" />
<span class="country-flag">{{ memberProfile.countryCode }}</span>
</div>
<div class="member-details">
<div class="member-name">{{ memberProfile.userNickname }}</div>
<div class="member-id">ID: {{ memberProfile.account }}</div>
<div class="member-status">{{ memberProfile.accountStatusName }}</div>
</div>
</div>
<div class="income-amount">${{ historyIncome }}</div>
</div>
<!-- 工作报告标题 -->
<div class="work-report-title">
<span>Work report:</span>
<button class="help-btn" @click="showHelpInfo">?</button>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-spinner"></div>
<p>Loading...</p>
</div>
<!-- 账单列表 -->
<div class="bill-list">
<div v-else-if="billList.length > 0" class="bill-list">
<div
v-for="bill in bills"
v-for="bill in billList"
:key="bill.id"
class="bill-item"
@click="showBillDetail(bill)"
>
<div class="bill-header">
<span class="bill-period">{{ bill.period }}</span>
<span :class="['bill-status', bill.status.toLowerCase()]">
{{ bill.statusText }}
<span class="bill-period">{{ bill.billTitle }}</span>
<span :class="['bill-status', getStatusClass(bill.status)]">
{{ getStatusText(bill.status) }}
</span>
</div>
<div class="bill-details">
<div class="salary-row">
<div class="salary-item">
<span class="salary-label">Host Salary:</span>
<span class="salary-value">{{ bill.hostSalary }}</span>
</div>
<div class="salary-item">
<span class="salary-label">Agent Salary:</span>
<span class="salary-value">{{ bill.agentSalary }}</span>
</div>
<div class="bill-row">
<span class="bill-label">Gift Value:</span>
<span class="bill-value">{{ formatGiftValue(bill.target?.acceptGiftValue) }}</span>
</div>
<div class="total-row">
<span class="total-label">Total:</span>
<span class="total-value">{{ bill.total }}</span>
<div class="bill-row">
<span class="bill-label">Online Time:</span>
<span class="bill-value">{{ formatOnlineTime(bill.target?.onlineTime) }}</span>
</div>
<div class="bill-row">
<span class="bill-label">Diamond:</span>
<span class="bill-value diamond-value">{{ formatDiamondValue(bill.target?.acceptDiamondValue) }}</span>
</div>
<div class="bill-row">
<span class="bill-label">Effective Days:</span>
<span class="bill-value">{{ bill.target?.effectiveDay || 0 }}</span>
</div>
</div>
@ -56,20 +73,26 @@
</div>
</div>
</div>
<!-- 空数据状态 -->
<div v-else class="empty-state">
<div class="empty-icon">📋</div>
<p>No data available</p>
</div>
</div>
<!-- 帮助模态框 -->
<div v-if="showHelpModal" class="modal-overlay" @click="closeHelpModal">
<div class="help-modal-content" @click.stop>
<div class="help-modal-header">
<h3>Help Information</h3>
<h3>Work Report Help</h3>
<button class="close-btn" @click="closeHelpModal">×</button>
</div>
<div class="help-modal-body">
<div class="help-section">
<p class="help-description">
Shown below is the work report of the members "More" can view more work detailed.
Below is the work report of the team members. Click "More" to view detailed daily work records.
</p>
</div>
@ -78,23 +101,18 @@
<div class="status-list">
<div class="status-item">
<span class="status-number">(1)</span>
<span class="status-name">Completed:</span>
<span class="status-desc">The system has completed the settlement report.</span>
<span class="status-name">UNPAID:</span>
<span class="status-desc">Work is currently in progress.</span>
</div>
<div class="status-item">
<span class="status-number">(2)</span>
<span class="status-name">Pending:</span>
<span class="status-desc">There is currently a dispute, and the system cannot perform a settlement.</span>
<span class="status-name">HANG_UP:</span>
<span class="status-desc">Work has been suspended or is pending review.</span>
</div>
<div class="status-item">
<span class="status-number">(3)</span>
<span class="status-name">Out of account:</span>
<span class="status-desc">waiting for system verification</span>
</div>
<div class="status-item">
<span class="status-number">(4)</span>
<span class="status-name">In progress:</span>
<span class="status-desc">work now in progress.</span>
<span class="status-name">PAID:</span>
<span class="status-desc">Work has been completed and settled.</span>
</div>
</div>
</div>
@ -102,177 +120,100 @@
</div>
</div>
<!-- 成员收入弹窗 -->
<div v-if="showMemberModal" class="modal-overlay" @click="closeMemberModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>Member Income</h3>
<button class="close-btn" @click="closeMemberModal">×</button>
</div>
<div class="modal-body">
<div class="modal-period">
<span>{{ selectedBill?.period }}</span>
<span :class="['modal-status', selectedBill?.status.toLowerCase()]">
{{ selectedBill?.statusText }}
</span>
</div>
<div class="search-section">
<div class="search-box">
<span class="search-icon">🔍</span>
<input
type="text"
placeholder="Please enter the host ID"
v-model="searchQuery"
class="search-input"
/>
</div>
</div>
<div class="member-list">
<div
v-for="member in filteredMembers"
:key="member.id"
class="member-item"
>
<div class="member-info">
<div class="member-avatar">
<span>{{ member.name.charAt(0) }}</span>
</div>
<div class="member-details">
<div class="member-name">{{ member.name }}</div>
<div class="member-id">ID: {{ member.id }}</div>
<div class="member-target">Target: {{ member.target }}</div>
</div>
</div>
<div class="member-stats">
<div class="member-stat">
<span class="stat-icon">💰</span>
<span>{{ member.id }}</span>
</div>
<div class="member-time">Time (Days): {{ member.days }}</div>
</div>
</div>
</div>
</div>
<!-- 账单详情模态框 (使用 TeamBillMore 组件) -->
<div v-if="showDetailModal" class="modal-overlay">
<div class="detail-modal-wrapper">
<TeamBillMore
:billData="selectedBill"
@close="closeDetailModal"
@propsReceived="handlePropsReceived"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import TeamBillMore from '../components/team/TeamBillMore.vue'
import { getTeamMemberWork, formatGiftValue, formatOnlineTime, formatDateDisplay } from '../api/teamBill.js'
const router = useRouter()
const historyIncome = ref('12345')
const showMemberModal = ref(false)
const loading = ref(false)
const showHelpModal = ref(false)
const showDetailModal = ref(false)
const selectedBill = ref(null)
const searchQuery = ref('')
//
const bills = ref([
{
id: 1,
period: '2025.08',
status: 'pending',
statusText: '进行中 (Pending)',
hostSalary: '',
agentSalary: '',
total: ''
},
{
id: 2,
period: '2025.07',
status: 'pending',
statusText: '待处理 (Pending)',
hostSalary: '$100',
agentSalary: '$10',
total: '110'
},
{
id: 3,
period: '2025.06',
status: 'completed',
statusText: '已完成 (Completed)',
hostSalary: '$200',
agentSalary: '$30',
total: '$230'
},
{
id: 4,
period: '2025.05',
status: 'out',
statusText: '挂起 (Out of account)',
hostSalary: '$500',
agentSalary: '$100',
total: '$600'
//
const memberProfile = ref(null)
const billList = ref([])
//
const fetchTeamMemberWork = async (userId) => {
try {
loading.value = true
const response = await getTeamMemberWork(userId)
if (response.status && response.body) {
memberProfile.value = response.body.memberProfile
billList.value = response.body.targets || []
}
} catch (error) {
console.error('Failed to fetch team member work:', error)
} finally {
loading.value = false
}
])
}
//
const members = ref([
{
id: '1234567890',
name: 'User name 1',
target: 'A',
days: 14
},
{
id: '1234567890',
name: 'User name 1',
target: 'A',
days: 14
},
{
id: '1234567890',
name: 'User name 1',
target: 'A',
days: 14
},
{
id: '1234567890',
name: 'User name 1',
target: 'A',
days: 14
},
{
id: '1234567890',
name: 'User name 1',
target: 'A',
days: 14
},
{
id: '1234567890',
name: 'User name 1',
target: 'A',
days: 14
//
const getStatusText = (status) => {
const statusMap = {
'UNPAID': 'In Progress',
'HANG_UP': 'Pending',
'PAID': 'Completed'
}
])
return statusMap[status] || status
}
//
const filteredMembers = computed(() => {
if (!searchQuery.value) return members.value
return members.value.filter(member =>
member.id.includes(searchQuery.value) ||
member.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
//
const getStatusClass = (status) => {
const statusClassMap = {
'UNPAID': 'in-progress',
'HANG_UP': 'pending',
'PAID': 'completed'
}
return statusClassMap[status] || 'unknown'
}
//
const formatDiamondValue = (value) => {
if (!value || value === '0') return '0'
const numValue = parseInt(value)
if (numValue >= 1000000) {
return `${(numValue / 1000000).toFixed(1)}M`
} else if (numValue >= 1000) {
return `${(numValue / 1000).toFixed(1)}K`
}
return numValue.toString()
}
//
const formatDate = (dateNumber) => {
return formatDateDisplay(dateNumber)
}
//
const showBillDetail = (bill) => {
selectedBill.value = bill
showMemberModal.value = true
showDetailModal.value = true
}
//
const closeMemberModal = () => {
showMemberModal.value = false
//
const closeDetailModal = () => {
showDetailModal.value = false
selectedBill.value = null
searchQuery.value = ''
}
//
@ -284,11 +225,26 @@ const showHelpInfo = () => {
const closeHelpModal = () => {
showHelpModal.value = false
}
//
const handlePropsReceived = () => {
//
console.log('Props received successfully')
}
//
onMounted(() => {
// ID
const userId = router.currentRoute.value.query.userId || '1911661502909562881'
fetchTeamMemberWork(userId)
})
</script>
<style scoped>
.team-bill {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
min-height: 100vh;
background-color: #f1f2f3;
}
.content {
@ -297,8 +253,8 @@ const closeHelpModal = () => {
z-index: 2;
}
/* 历史收入卡片 */
.history-income-card {
/* 成员资料卡片 */
.member-profile-card {
background-color: white;
padding: 16px;
border-radius: 12px;
@ -306,16 +262,65 @@ const closeHelpModal = () => {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.income-header {
.member-info {
display: flex;
align-items: center;
gap: 12px;
}
.avatar-wrapper {
position: relative;
flex-shrink: 0;
}
.member-avatar {
width: 48px;
height: 48px;
border-radius: 8px;
object-fit: cover;
}
.country-flag {
position: absolute;
bottom: -4px;
left: 2px;
background-color: #333;
color: white;
font-size: 10px;
padding: 1px 3px;
border-radius: 2px;
}
.member-details {
flex: 1;
}
.member-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.member-id {
font-size: 12px;
color: #666;
margin-bottom: 2px;
}
.member-status {
font-size: 12px;
color: #10B981;
}
/* 工作报告标题 */
.work-report-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.income-label {
font-size: 14px;
font-size: 16px;
color: #666;
margin-bottom: 12px;
}
.help-btn {
@ -334,23 +339,30 @@ const closeHelpModal = () => {
background-color: #8B5CF6;
}
.help-btn:active {
transform: scale(0.95);
}
.income-amount {
font-size: 24px;
font-weight: 600;
color: #333;
}
/* 工作报告标题 */
.work-report-title {
font-size: 16px;
/* 加载状态 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
color: #666;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #8B5CF6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 账单列表 */
.bill-list {
display: flex;
@ -391,6 +403,11 @@ const closeHelpModal = () => {
font-weight: 500;
}
.bill-status.in-progress {
background-color: #DBEAFE;
color: #1D4ED8;
}
.bill-status.pending {
background-color: #FEF3C7;
color: #D97706;
@ -401,54 +418,34 @@ const closeHelpModal = () => {
color: #059669;
}
.bill-status.out {
background-color: #FEE2E2;
color: #DC2626;
}
.bill-details {
margin-bottom: 12px;
}
.salary-row {
.bill-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.salary-item {
display: flex;
flex-direction: column;
align-items: center;
.bill-row:last-child {
margin-bottom: 0;
}
.salary-label {
font-size: 12px;
.bill-label {
font-size: 14px;
color: #666;
margin-bottom: 4px;
}
.salary-value {
.bill-value {
font-size: 14px;
font-weight: 600;
color: #333;
}
.total-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.total-label {
font-size: 14px;
color: #666;
}
.total-value {
font-size: 14px;
font-weight: 600;
color: #333;
.diamond-value {
color: #1E40AF !important;
}
.more-btn {
@ -458,13 +455,28 @@ const closeHelpModal = () => {
gap: 4px;
color: #C084FC;
font-size: 14px;
cursor: pointer;
}
.arrow {
font-weight: bold;
}
/* 模态框 */
/* 空数据状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
color: #666;
}
.empty-icon {
font-size: 48px;
margin-bottom: 12px;
}
/* 模态框公共样式 */
.modal-overlay {
position: fixed;
top: 0;
@ -479,7 +491,8 @@ const closeHelpModal = () => {
padding: 16px;
}
.modal-content {
.help-modal-content,
.detail-modal-content {
background-color: white;
border-radius: 12px;
width: 100%;
@ -524,149 +537,7 @@ const closeHelpModal = () => {
overflow-y: auto;
}
.modal-period {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.modal-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.modal-status.pending {
background-color: #FEF3C7;
color: #D97706;
}
.search-section {
margin-bottom: 16px;
}
.search-box {
display: flex;
align-items: center;
background-color: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 8px 12px;
}
.search-icon {
margin-right: 8px;
color: #9ca3af;
}
.search-input {
flex: 1;
border: none;
background: none;
outline: none;
font-size: 14px;
color: #333;
}
.search-input::placeholder {
color: #9ca3af;
}
.member-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.member-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background-color: #f9fafb;
border-radius: 8px;
}
.member-info {
display: flex;
align-items: center;
flex: 1;
}
.member-avatar {
width: 32px;
height: 32px;
border-radius: 16px;
background-color: #8B5CF6;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 14px;
font-weight: 600;
margin-right: 12px;
}
.member-details {
flex: 1;
}
.member-name {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 2px;
}
.member-id {
font-size: 12px;
color: #666;
margin-bottom: 2px;
}
.member-target {
font-size: 12px;
color: #666;
}
.member-stats {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.member-stat {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #333;
}
.stat-icon {
font-size: 10px;
}
.member-time {
font-size: 11px;
color: #666;
}
/* 帮助模态框 */
.help-modal-content {
background-color: white;
border-radius: 12px;
width: 100%;
max-width: 400px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 帮助模态框样式 */
.help-modal-header {
display: flex;
justify-content: space-between;
@ -739,7 +610,7 @@ const closeHelpModal = () => {
font-weight: 600;
color: #333;
flex-shrink: 0;
min-width: 100px;
min-width: 80px;
}
.status-desc {
@ -747,4 +618,27 @@ const closeHelpModal = () => {
line-height: 1.4;
flex: 1;
}
</style>
/* 详情模态框新样式 */
.detail-modal-wrapper {
width: 100%;
height: 100%;
max-width: 100vw;
max-height: 100vh;
overflow: auto;
background-color: white;
border-radius: 0;
}
/* 在大屏幕上显示为模态框 */
@media (min-width: 768px) {
.detail-modal-wrapper {
width: 90%;
height: 90%;
max-width: 600px;
max-height: 80vh;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
}
}
</style>

View File

@ -1,212 +1,178 @@
<template>
<div class="team-member gradient-background-circles">
<MobileHeader title="Team Member (87)" />
<MobileHeader :title="`Team Member (${members.length})`" />
<div class="content">
<!-- 搜索框 -->
<div class="search-section">
<div class="search-box">
<span class="search-icon">🔍</span>
<input
type="text"
placeholder="Search by ID or name"
v-model="searchQuery"
class="search-input"
/>
</div>
</div>
<!-- 筛选标签 -->
<div class="filter-tabs">
<button
v-for="tab in filterTabs"
:key="tab.value"
@click="activeFilter = tab.value"
:class="['filter-tab', { active: activeFilter === tab.value }]"
>
{{ tab.label }}
</button>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-spinner"></div>
<p>Loading members...</p>
</div>
<!-- 成员列表 -->
<div class="member-list">
<div
v-for="member in filteredMembers"
<div v-else-if="members.length > 0" class="member-list">
<div
v-for="member in members"
:key="member.id"
class="member-item"
@click="viewMemberDetail(member)"
>
<div class="member-info">
<div class="member-avatar">
<img v-if="member.avatar" :src="member.avatar" :alt="member.name" />
<span v-else>{{ member.name.charAt(0) }}</span>
<img
v-if="member.userProfile.userAvatar"
:src="member.userProfile.userAvatar"
:alt="member.userProfile.userNickname"
@error="handleImageError"
/>
<span v-else>{{ member.userProfile.userNickname.charAt(0) }}</span>
</div>
<div class="member-details">
<h4>{{ member.name }}</h4>
<p>ID: {{ member.id }}</p>
<div class="member-tags">
<span v-if="member.isHost" class="tag host-tag">👑 Host</span>
<span v-if="member.isAgent" class="tag agent-tag">👤 Agent</span>
<span v-if="member.isActive" class="tag active-tag">🟢 Active</span>
<span v-else class="tag inactive-tag">🔴 Inactive</span>
</div>
</div>
</div>
<div class="member-stats">
<div class="stat-item">
<span class="stat-label">Target:</span>
<span class="stat-value">{{ member.target }}</span>
</div>
<div class="stat-item">
<span class="stat-label">Days:</span>
<span class="stat-value">{{ member.activeDays }}</span>
</div>
<div class="stat-item">
<span class="stat-label">Salary:</span>
<span class="stat-value">${{ member.salary }}</span>
<h4>{{ member.userProfile.userNickname }}</h4>
<p>ID: {{ formatAccount(member.userProfile) }}</p>
</div>
</div>
<button
class="delete-btn"
@click="handleDeleteMember(member)"
:disabled="deletingMemberId === member.id"
>
<span v-if="deletingMemberId === member.id"></span>
<span v-else>🗑</span>
</button>
</div>
</div>
<!-- 空状态 -->
<div v-if="filteredMembers.length === 0" class="empty-state">
<div v-else class="empty-state">
<div class="empty-icon">👥</div>
<p>No members found</p>
<p>No team members found</p>
</div>
</div>
<!-- 删除确认模态框 -->
<div v-if="showDeleteModal" class="modal-overlay" @click="closeDeleteModal">
<div class="delete-modal-content" @click.stop>
<div class="modal-header">
<h3>Confirm Delete</h3>
</div>
<div class="modal-body">
<p>Are you sure you want to remove <strong>{{ memberToDelete?.userProfile?.userNickname }}</strong> from the team?</p>
<p class="warning-text">This action cannot be undone.</p>
</div>
<div class="modal-actions">
<button class="cancel-btn" @click="closeDeleteModal">Cancel</button>
<button class="confirm-btn" @click="confirmDelete">Delete</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import { getTeamMembers, deleteMember as deleteMemberApi } from '../api/teamBill.js'
const router = useRouter()
const searchQuery = ref('')
const activeFilter = ref('all')
const loading = ref(false)
const members = ref([])
const deletingMemberId = ref(null)
const showDeleteModal = ref(false)
const memberToDelete = ref(null)
//
const filterTabs = [
{ label: 'All', value: 'all' },
{ label: 'Host', value: 'host' },
{ label: 'Agent', value: 'agent' },
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' }
]
//
const fetchTeamMembers = async () => {
try {
loading.value = true
// teamId
const teamId = router.currentRoute.value.query.teamId || '1927993836921233409'
//
const members = ref([
{
id: '1234567890',
name: 'Alice Johnson',
avatar: '',
isHost: true,
isAgent: false,
isActive: true,
target: 'A',
activeDays: 25,
salary: 450
},
{
id: '0987654321',
name: 'Bob Smith',
avatar: '',
isHost: false,
isAgent: true,
isActive: true,
target: 'B',
activeDays: 18,
salary: 320
},
{
id: '1122334455',
name: 'Carol Davis',
avatar: '',
isHost: true,
isAgent: false,
isActive: false,
target: 'A',
activeDays: 8,
salary: 120
},
{
id: '5566778899',
name: 'David Wilson',
avatar: '',
isHost: false,
isAgent: true,
isActive: true,
target: 'C',
activeDays: 30,
salary: 580
},
{
id: '9988776655',
name: 'Eva Brown',
avatar: '',
isHost: false,
isAgent: true,
isActive: false,
target: 'B',
activeDays: 5,
salary: 80
},
{
id: '4433221100',
name: 'Frank Miller',
avatar: '',
isHost: true,
isAgent: false,
isActive: true,
target: 'A',
activeDays: 22,
salary: 390
const response = await getTeamMembers(teamId)
if (response.status && response.body) {
members.value = response.body || []
} else {
members.value = []
}
} catch (error) {
console.error('Failed to fetch team members:', error)
members.value = []
} finally {
loading.value = false
}
])
//
const filteredMembers = computed(() => {
let filtered = members.value
//
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
filtered = filtered.filter(member =>
member.name.toLowerCase().includes(query) ||
member.id.includes(query)
)
}
//
switch (activeFilter.value) {
case 'host':
filtered = filtered.filter(member => member.isHost)
break
case 'agent':
filtered = filtered.filter(member => member.isAgent)
break
case 'active':
filtered = filtered.filter(member => member.isActive)
break
case 'inactive':
filtered = filtered.filter(member => !member.isActive)
break
}
return filtered
})
//
const viewMemberDetail = (member) => {
alert(`View details for ${member.name}`)
}
//
const formatAccount = (userProfile) => {
if (!userProfile) return ''
// IDID
if (userProfile.ownSpecialId && userProfile.ownSpecialId.account) {
return userProfile.ownSpecialId.account
}
//
return userProfile.account || ''
}
//
const handleImageError = (event) => {
event.target.style.display = 'none'
event.target.nextElementSibling.style.display = 'flex'
}
//
const handleDeleteMember = (member) => {
memberToDelete.value = member
showDeleteModal.value = true
}
//
const closeDeleteModal = () => {
showDeleteModal.value = false
memberToDelete.value = null
}
//
const confirmDelete = async () => {
if (!memberToDelete.value) return
try {
deletingMemberId.value = memberToDelete.value.id
// API
await deleteMemberApi(memberToDelete.value.id)
//
const index = members.value.findIndex(m => m.id === memberToDelete.value.id)
if (index > -1) {
members.value.splice(index, 1)
}
closeDeleteModal()
} catch (error) {
console.error('Failed to delete member:', error)
alert('Failed to delete member. Please try again.')
} finally {
deletingMemberId.value = null
}
}
//
onMounted(() => {
fetchTeamMembers()
})
</script>
<style scoped>
.team-member {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
min-height: 100vh;
background-color: #f1f2f3;
}
.content {
@ -215,70 +181,28 @@ const viewMemberDetail = (member) => {
z-index: 2;
}
/* 搜索部分 */
.search-section {
margin-bottom: 16px;
}
.search-box {
/* 加载状态 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
background-color: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 8px 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.search-icon {
margin-right: 8px;
color: #9ca3af;
}
.search-input {
flex: 1;
border: none;
background: none;
outline: none;
font-size: 14px;
color: #333;
}
.search-input::placeholder {
color: #9ca3af;
}
/* 筛选标签 */
.filter-tabs {
display: flex;
gap: 8px;
margin-bottom: 16px;
overflow-x: auto;
padding-bottom: 4px;
}
.filter-tab {
padding: 6px 12px;
border: 1px solid #e5e7eb;
border-radius: 16px;
background-color: white;
padding: 60px 20px;
color: #666;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.filter-tab.active {
background-color: #8B5CF6;
color: white;
border-color: #8B5CF6;
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #8B5CF6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
.filter-tab:not(.active):hover {
border-color: #8B5CF6;
color: #8B5CF6;
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 成员列表 */
@ -293,18 +217,20 @@ const viewMemberDetail = (member) => {
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.2s;
}
.member-item:active {
transform: scale(0.98);
.member-item:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.member-info {
display: flex;
align-items: center;
margin-bottom: 12px;
flex: 1;
}
.member-avatar {
@ -320,6 +246,7 @@ const viewMemberDetail = (member) => {
font-weight: 600;
margin-right: 12px;
overflow: hidden;
position: relative;
}
.member-avatar img {
@ -328,6 +255,14 @@ const viewMemberDetail = (member) => {
object-fit: cover;
}
.member-avatar span {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-transform: uppercase;
}
.member-details {
flex: 1;
}
@ -337,70 +272,47 @@ const viewMemberDetail = (member) => {
font-size: 16px;
font-weight: 600;
color: #333;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.member-details p {
margin: 0 0 8px 0;
margin: 0;
font-size: 14px;
color: #666;
}
.member-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.tag {
padding: 2px 6px;
border-radius: 8px;
font-size: 10px;
font-weight: 600;
}
.host-tag {
background-color: #E0E7FF;
color: #5B21B6;
}
.agent-tag {
background-color: #DBEAFE;
color: #1E40AF;
}
.active-tag {
background-color: #D1FAE5;
color: #059669;
}
.inactive-tag {
/* 删除按钮 */
.delete-btn {
background-color: #FEE2E2;
border: 1px solid #FECACA;
color: #DC2626;
}
.member-stats {
padding: 8px;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
transition: all 0.2s;
display: flex;
justify-content: space-between;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
.delete-btn:hover:not(:disabled) {
background-color: #FECACA;
border-color: #FCA5A5;
}
.stat-label {
font-size: 11px;
color: #666;
margin-bottom: 2px;
.delete-btn:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.stat-value {
font-size: 14px;
font-weight: 600;
color: #333;
.delete-btn:active:not(:disabled) {
transform: scale(0.95);
}
/* 空状态 */
@ -423,4 +335,110 @@ const viewMemberDetail = (member) => {
margin: 0;
font-size: 16px;
}
/* 删除确认模态框 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 16px;
}
.delete-modal-content {
background-color: white;
border-radius: 12px;
width: 100%;
max-width: 400px;
overflow: hidden;
}
.modal-header {
padding: 20px 20px 0 20px;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
text-align: center;
}
.modal-body {
padding: 16px 20px;
text-align: center;
}
.modal-body p {
margin: 0 0 8px 0;
font-size: 14px;
color: #666;
line-height: 1.5;
}
.warning-text {
color: #DC2626 !important;
font-weight: 500;
}
.modal-actions {
display: flex;
gap: 12px;
padding: 0 20px 20px 20px;
}
.cancel-btn,
.confirm-btn {
flex: 1;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.cancel-btn {
background-color: #F3F4F6;
color: #374151;
}
.cancel-btn:hover {
background-color: #E5E7EB;
}
.confirm-btn {
background-color: #DC2626;
color: white;
}
.confirm-btn:hover {
background-color: #B91C1C;
}
.cancel-btn:active,
.confirm-btn:active {
transform: scale(0.98);
}
/* 响应式设计 */
@media (max-width: 480px) {
.member-details h4 {
max-width: 150px;
}
.delete-btn {
width: 36px;
height: 36px;
font-size: 14px;
}
}
</style>

View File

@ -81,7 +81,7 @@
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import { getBankBalance } from '../api/wallet.js'
import {getBankBalance, userBankTransfer} from '../api/wallet.js'
import { getSelectedPayee, clearSelectedPayee } from '../utils/payeeStore.js'
const router = useRouter()
@ -176,22 +176,59 @@ const selectCoin = (coin) => {
}
}
const transfer = () => {
const transfer = async () => {
if (!selectedCoin.value) {
alert('Please select an amount first')
return
}
if (!selectedPayee.id) {
alert('Please select a payee first')
return
}
const selectedCoinData = coinOptions.find(coin => coin.id === selectedCoin.value)
alert(`Transfer ${selectedCoinData.amount} (${selectedCoinData.price} USD) to ${selectedPayee.name}`)
//
selectedCoin.value = null
//
// const password = prompt('Please enter your payment password:')
// if (!password) {
// alert('Payment password is required')
// return
// }
//
const transferData = {
amount: selectedCoinData.price,
acceptUserId: selectedPayee.id,
acceptMethod: 'BANK_TRANSFER',
// password: password
}
try {
//
const response = await userBankTransfer(transferData)
//
alert(`Successfully transferred $${selectedCoinData.price} to ${selectedPayee.name}`)
//
selectedCoin.value = null
//
await fetchBankBalance()
} catch (error) {
//
console.error('Transfer failed:', error)
if (error.errorCode === 5000) {
alert('Insufficient balance')
} else if (error.errorCode === 5010 || error.errorCode === 5009) {
alert('Payment password error')
} else {
alert('Transfer failed, please try again later')
}
}
}
//