959 lines
23 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="coin-seller gradient-background-circles">
<!-- 使用 GeneralHeader 替换 MobileHeader -->
<GeneralHeader
:isHomePage="true"
:showLanguageList="true"
:title="t('recharge_agency')"
color="black"
style="box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); z-index: 999"
/>
<!-- 权限校验加载状态 -->
<div v-if="checkingAccess" class="loading-container">
<LoadingSpinner />
<p>{{ t('checking_permissions') }}...</p>
</div>
<!-- 主要内容 -->
<div v-else-if="dealerAccess" class="content">
<!-- 身份 -->
<div style="display: flex; justify-content: space-between">
<div
style="
margin: 0 1%;
margin-bottom: 12px;
font-weight: 600;
display: flex;
align-items: center;
font-size: 1.3em;
"
>
{{ t('my_account') }}
<img
src="../../assets/icon/identity/coinSeller.png"
alt=""
height="20px"
style="display: block; margin-left: 10px"
/>
</div>
<img
@click="goToRecords"
src="../../assets/icon/Azizi/records.png"
height="20px"
alt=""
style="display: block; object-fit: cover"
/>
</div>
<!-- 用户信息卡片 -->
<div class="user-card">
<!-- 个人信息 -->
<div class="user-info">
<div class="avatar">
<img
:src="userInfo.userAvatar || ''"
alt=""
style="
display: block;
object-fit: cover;
width: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
"
@error="handleAvatarImageError"
/>
</div>
<div class="user-details">
<h3>{{ userInfo.userNickname || userInfo.name }}</h3>
<p>
<span style="display: flex; align-items: center">
<span>ID</span>
<span style="margin: 0 2px">:</span>
<span>{{ userInfo.id }}</span>
</span>
</p>
</div>
</div>
<!-- 我的金币 -->
<div>
<div style="font-weight: 600">{{ t('my_coins') }}</div>
<div style="margin-top: 12px; width: 100%; display: flex; align-items: center">
<img
src="../../assets/icon/Azizi/coin.png"
alt=""
height="20px"
style="display: block"
/>
<div style="color: #ffb627; font-weight: 590; margin-left: 5px">
{{ coinsAmount }}
</div>
</div>
</div>
</div>
<!-- 设置最小接收金额 -->
<div
style="
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
gap: 10px;
"
>
<div style="display: flex; justify-content: space-between; align-items: center; gap: 4px">
<div style="font-weight: 600">{{ t('min_transfer_amount') }}</div>
<img
src="../../assets/icon/Azizi/help.png"
height="20px"
alt=""
style="display: block; object-fit: cover"
@click="showHelpTip = true"
/>
</div>
<div style="display: flex; justify-content: space-around; align-items: center; gap: 10px">
<div
v-for="amountItem in AmountList"
style="
flex: 1;
min-width: 0;
align-self: stretch;
border-radius: 12px;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
padding: 10px;
display: flex;
justify-content: center;
align-items: center;
"
:style="amountStyle(amountItem)"
@click="selectAmount(amountItem)"
>
<div style="font-weight: 700">{{ amountItem.type }}</div>
</div>
</div>
</div>
<!-- 用户搜索部分 -->
<div class="user-section">
<div style="margin-bottom: 12px; font-weight: 600; color: #333">
{{ t('user') }}
</div>
<!-- 有用户 -->
<div v-if="selectedUser.id" class="selected-user">
<div class="user-info">
<div class="user-avatar">
<img
:src="selectedUser.userAvatar || ''"
alt=""
style="
display: block;
object-fit: cover;
width: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
"
@error="handleAvatarImageError"
/>
</div>
<div class="user-details">
<div class="user-name">
{{ selectedUser.name }}
</div>
<div class="user-id">
<span style="display: flex; align-items: center">
<span>ID</span>
<span style="margin: 0 2px">:</span>
<span>{{ selectedUser.account }}</span>
</span>
</div>
</div>
</div>
<button class="change-btn" @click="searchUser">{{ t('change') }}</button>
</div>
<!-- 无用户 -->
<div v-else style="display: flex; justify-content: center; align-items: center">
<div class="search-placeholder" @click="searchUser">
<span>{{ t('search') }}</span>
</div>
</div>
<!-- 充值部分 -->
<div v-if="selectedUser.id" style="margin-top: 12px">
<div style="margin-bottom: 12px; font-weight: 600; color: #333">
{{ t('recharge') }}
</div>
<div></div>
<!-- 输入金额 -->
<div
style="
background-color: white;
padding: 10px 16px;
border-radius: 12px;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
"
>
<input
v-model="rechargeAmount"
:placeholder="t('enter_recharge_amount')"
class="amount-input"
@input="preventDecimalInput"
@keydown="onKeyDown"
dir="ltr"
inputmode="decimal"
/>
</div>
<div style="margin: 10px 0 16px; display: flex; flex-direction: column; gap: 4px">
<div
v-if="rechargeCoinsNeeded == 0"
style="color: rgba(0, 0, 0, 0.4); font-weight: 600; font-size: 0.9em"
>
{{ t('recharge_auto_convert_tip') }}
</div>
<div v-else style="width: 100%; display: flex; align-items: center">
<img
src="../../assets/icon/Azizi/coin.png"
alt=""
height="15px"
style="display: block"
/>
<div style="color: #ffb627; font-weight: 590; margin-left: 5px">
{{ rechargeCoinsNeeded }}
</div>
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-weight: 600; font-size: 0.9em">
{{ t('recharge_ratio_text', { ratio: rechargeRatio }) }}
</div>
</div>
<!-- 按钮 -->
<div style="display: flex; justify-content: center">
<button
class="recharge-btn"
:disabled="!canRecharge || isRecharging"
@click="rechargeNow"
>
{{ isRecharging ? t('loading') : t('recharge_now') }}
</button>
</div>
</div>
</div>
</div>
<!-- 遮罩层 -->
<maskLayer :maskLayerShow="maskLayerShow" @click="closedPopup">
<div
style="
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
"
>
<!-- 帮助提示弹窗 -->
<div
v-if="showHelpTip"
style="
width: 80%;
border-radius: 12px;
background: #fff;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
"
@click.stop
>
<div style="position: relative; width: 100%">
<div style="text-align: center; color: rgba(0, 0, 0, 0.8); font-weight: 800">
{{ t('tips') }}
</div>
<!-- 关闭按钮 -->
<div style="position: absolute; top: 0; right: 0" @click="closedPopup">X</div>
</div>
<div style="color: rgba(0, 0, 0, 0.5); font-weight: 600; text-align: center">
{{ t('min_transfer_amount_tip') }}
</div>
</div>
<!-- 设置转账确认弹窗 -->
<div
v-if="showTransfersTip"
style="
width: 80%;
border-radius: 12px;
background: #fff;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
"
@click.stop
>
<div style="color: rgba(0, 0, 0, 0.8); font-weight: 800">{{ t('tips') }}</div>
<div style="color: rgba(0, 0, 0, 0.5); font-weight: 600; text-align: center">
{{ t('confirm_transfer_limit') }}
</div>
<div
style="width: 100%; display: flex; justify-content: space-around; align-items: center"
>
<div
style="
padding: 12px 20px;
border-radius: 12px;
border: 1px solid #e6e6e6;
color: rgba(0, 0, 0, 0.4);
font-weight: 800;
"
@click="Cancel"
>
{{ t('cancel') }}
</div>
<div
style="
padding: 12px 20px;
border-radius: 12px;
background: linear-gradient(
152deg,
rgba(198, 112, 255, 0.6) 7.01%,
rgba(119, 38, 255, 0.6) 92.99%
);
color: rgba(255, 255, 255, 0.8);
font-weight: 800;
"
@click="Confirm"
>
{{ t('confirm') }}
</div>
</div>
</div>
</div>
</maskLayer>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { getUserId } from '@/utils/userStore.js'
import { setDocumentDirection } from '@/locales/i18n'
import { getSelectedUser } from '@/utils/coinSellerStore.js'
import { showError, showSuccess } from '@/utils/toast.js'
import { usePageInitializationWithConfig } from '@/utils/pageConfig.js'
import { handleAvatarImageError } from '@/utils/image/imageHandler.js'
import { useDebounce, useThrottle } from '@/utils/useDebounce'
import {
checkFreightDealer,
getFreightBalance,
freightRecharge,
apiGetFreightThreshold, //获取金币代理最小收入金额
apiSetThreshold, // 金币代理设置最小收入金额
} from '@/api/wallet.js'
import GeneralHeader from '@/components/GeneralHeader.vue'
import maskLayer from '@/components/MaskLayer.vue'
const { t, locale } = useI18n()
const router = useRouter()
// 监听语言变化并设置文档方向
locale.value && setDocumentDirection(locale.value)
// 权限校验状态
const dealerAccess = ref(false)
const checkingAccess = ref(true)
// 金币数量
const coinsAmount = ref('0')
const loadingBalance = ref(false)
const freightThreshold = ref(null)
const AmountList = computed(() => {
return [
{
id: 1,
type: '>$0',
value: 0,
selectedStatus: freightThreshold.value?.deliveryThreshold == 0,
},
{
id: 2,
type: '>=$10',
value: 10,
selectedStatus: freightThreshold.value?.deliveryThreshold == 10,
},
{
id: 3,
type: '>=$50',
value: 50,
selectedStatus: freightThreshold.value?.deliveryThreshold == 50,
},
]
})
const selectedAmount = ref({})
const isAbleSelect = ref(false) // 是否可以设置
const rechargeRatio = ref(0) // 充值比例1美元兑换多少金币
// 选项样式
const amountStyle = (amountItem) => {
if (amountItem.selectedStatus) {
return 'border: 1px solid #7726ff;background: #fff;'
} else if (isAbleSelect.value) {
return 'border: 1px solid transparent;background: transparent;'
} else {
return 'border: 1px solid transparent;background: rgba(0, 0, 0, 0.2);'
}
}
// 弹窗提示
const showHelpTip = ref(false) //帮助提示弹窗
const showTransfersTip = ref(false) //设置转账确认弹窗
const maskLayerShow = computed(() => {
return showHelpTip.value || showTransfersTip.value
})
// 选中的用户
const selectedUser = ref({
id: '',
name: '',
})
// 关闭弹窗
const closedPopup = () => {
showHelpTip.value = false
showTransfersTip.value = false
}
// 选择限制转账金额
const selectAmount = (amountItem) => {
// 不可选择或者已选择
if (!isAbleSelect.value || amountItem.selectedStatus) return
console.log('选择限制转账金额', amountItem)
selectedAmount.value = amountItem //设置选中的金额
showTransfersTip.value = true //展示确认弹窗
}
// 取消选择
const Cancel = () => {
console.log('取消选择')
selectedAmount.value = {} //清空选中的金额
closedPopup()
}
// 确认选择
const Confirm = () => {
console.log('确认选择')
// 发送接口
setThreshold()
closedPopup()
}
// 键盘按下事件,阻止某些按键
const onKeyDown = (event) => {
// 阻止小数点、e、+、- 等特殊字符
if (['.', 'e', '+', '-'].includes(event.key)) {
event.preventDefault()
}
}
// 校验销售商权限
const checkDealerAccess = async () => {
try {
checkingAccess.value = true
const response = await checkFreightDealer()
if (response && response.status !== undefined) {
dealerAccess.value = response.body === true
if (!dealerAccess.value) {
// 没有权限,跳转回上一页或首页
showError(t('not_authorized'))
router.go(-1)
return
}
}
} catch (error) {
console.error('Permission verification failed:', error)
dealerAccess.value = false
router.go(-1)
} finally {
checkingAccess.value = false
}
}
// 获取金币余额
const fetchFreightBalance = async () => {
try {
loadingBalance.value = true
const response = await getFreightBalance()
if (response && response.status && response.body !== undefined) {
coinsAmount.value = response.body.toString()
}
} catch (error) {
console.error('获取金币余额失败:', error)
} finally {
loadingBalance.value = false
}
}
// 获取金币代理最小收入金额
const getFreightThreshold = async () => {
const resFreightThreshold = await apiGetFreightThreshold(userInfo.id)
console.log('任务列表:', resFreightThreshold)
if (resFreightThreshold.status && resFreightThreshold.body) {
freightThreshold.value = resFreightThreshold.body
isAbleSelect.value = !resFreightThreshold.body?.settinged //是否可以设置
rechargeRatio.value = resFreightThreshold.body?.rechargeRatio || 0 //充值比例
}
}
// 金币代理设置最小收入金额
const setThreshold = async () => {
try {
let data = {
deliveryThreshold: selectedAmount.value.value,
}
await apiSetThreshold(data) // 金币代理设置最小收入金额
selectedAmount.value = {}
getFreightThreshold() //获取金币代理最小收入金额
} catch (error) {
console.log('设置失败:', error)
}
}
// 初始化用户信息
const initializeUser = () => {
const savedUser = getSelectedUser()
if (savedUser && savedUser.id) {
selectedUser.value = { ...savedUser }
}
}
// 充值金额
const rechargeAmount = ref('')
const isRecharging = ref(false)
const currentCoinBalance = computed(() => {
return Number(coinsAmount.value) || 0
})
const rechargeDollarAmount = computed(() => {
return Number(rechargeAmount.value) || 0
})
const rechargeCoinsNeeded = computed(() => {
return rechargeDollarAmount.value * (Number(rechargeRatio.value) || 0)
})
// 是否可以充值
const canRecharge = computed(() => {
return (
selectedUser.value.id &&
rechargeAmount.value &&
rechargeDollarAmount.value > 0 &&
Number(rechargeRatio.value) > 0 &&
rechargeCoinsNeeded.value > 0 &&
rechargeCoinsNeeded.value <= currentCoinBalance.value &&
!isRecharging.value
)
})
// 防止输入小数点的函数
const preventDecimalInput = (event) => {
let value = event.target.value
// 移除小数点和其他非整数字符
value = value.replace(/[^0-9]/g, '')
// 确保最小值为1
if (value === '' || parseInt(value) < 1) {
value = ''
}
// 更新输入框的值
event.target.value = value
rechargeAmount.value = parseFloat(value) || 0
}
// 跳转到设置
const goToRecords = () => {
router.push('/seller-records')
}
// 搜索用户
const searchUser = () => {
router.push('/coin-seller-search')
}
// 币商充值金币
const rechargeNow = useThrottle(async () => {
if (!canRecharge.value || isRecharging.value) return
const amount = rechargeDollarAmount.value
const quantity = rechargeCoinsNeeded.value
if (amount <= 0) {
showError(t('please_enter_valid_amount'))
return
}
if (quantity <= 0 || quantity > currentCoinBalance.value) {
return
}
// 需要获取接收用户的ID从用户搜索结果中获取
const acceptUserId = selectedUser.value.id
if (!acceptUserId) {
showError(t('user_not_found_or_search_failed'))
return
}
try {
isRecharging.value = true
const rechargeData = {
acceptUserId: acceptUserId,
quantity: quantity,
amount: amount,
}
const response = await freightRecharge(rechargeData)
if (response && response.status) {
// 充值金币成功,更新余额
if (response.body !== undefined) {
coinsAmount.value = response.body.toString()
}
showSuccess(
t('transfer_coin_success', {
amount: quantity,
name: selectedUser.value.name,
}),
)
// 重置表单
rechargeAmount.value = ''
} else {
showError(
t('transfer_coin_failed', {
amount: quantity,
}),
)
}
} catch (error) {
console.error('币商充值金币失败:', error)
showError(
error.response.errorMsg ||
t('transfer_coin_failed', {
amount: quantity,
}),
)
} finally {
isRecharging.value = false
}
}, 2000)
const { userInfo } = usePageInitializationWithConfig('COIN_SELLER', {
onDataLoaded: (data) => {
checkDealerAccess()
fetchFreightBalance()
initializeUser()
getFreightThreshold() //获取金币代理最小收入金额
},
})
onMounted(() => {})
</script>
<style scoped>
* {
color: rgba(0, 0, 0, 0.8);
}
/* 加载状态 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
color: #666;
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 用户信息卡片 */
.user-card {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.user-info {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
margin-bottom: 12px;
}
.avatar {
width: 50px;
height: 50px;
border-radius: 25px;
margin-right: 12px;
}
.user-details {
min-width: 0;
flex: 1;
}
.user-details h3 {
margin-bottom: 4px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-details p {
color: rgba(0, 0, 0, 0.4);
font-size: 0.9em;
font-weight: 500;
}
.settings-btn {
width: 32px;
height: 32px;
border: none;
background: none;
cursor: pointer;
}
.account-tags {
display: flex;
gap: 8px;
}
.tag {
padding: 4px 12px;
border-radius: 16px;
font-size: 0.8em;
font-weight: 600;
}
.seller-tag {
background-color: #fef3c7;
color: #d97706;
}
/* 用户搜索部分 */
.user-section {
background-color: white;
padding: 16px;
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.selected-user {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
border-radius: 12px;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
}
.selected-user .user-info {
display: flex;
align-items: center;
margin-bottom: 0;
}
.user-avatar {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
}
.selected-user .user-details {
min-width: 0;
flex: 1;
}
.user-name {
font-size: 0.9em;
font-weight: 700;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-id {
font-size: 0.8em;
font-weight: 500;
color: rgba(0, 0, 0, 0.4);
}
.change-btn {
border-radius: 12px;
background: linear-gradient(
152deg,
rgba(198, 112, 255, 0.6) 7.01%,
rgba(119, 38, 255, 0.6) 92.99%
);
color: white;
border: none;
padding: 6px 12px;
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
.change-btn:active {
background-color: #a855f7;
}
.search-placeholder {
padding: 12px 20px;
border: 1px solid #ddd;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.2s;
}
.search-placeholder:hover {
border-color: #f59e0b;
background-color: #fef5e7;
}
.search-placeholder span {
color: #666;
font-size: 0.9em;
font-weight: 500;
}
.amount-input {
width: 100%;
border: none;
font-size: 0.9em;
box-sizing: border-box;
}
.amount-input:focus {
outline: none;
}
.amount-input::placeholder {
font-size: 0.7em;
color: rgba(0, 0, 0, 0.4);
font-weight: 500;
}
input[type='number'] {
font-family: 'Arial', sans-serif; /* 使用支持西方数字的字体 */
direction: ltr; /* 强制从左到右 */
}
.currency-suffix {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
font-size: 0.8em;
color: #666;
}
.recharge-btn {
width: 80%;
padding: 12px;
border-radius: 32px;
background: linear-gradient(135deg, #bb92ff 2.82%, #8b45ff 99.15%);
color: white;
border: none;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.recharge-btn:not(:disabled):active {
background: linear-gradient(135deg, #b080ff 2.82%, #6302ff 99.15%);
}
.recharge-btn:disabled {
background: #d1d5db;
cursor: not-allowed;
}
@media screen and (max-width: 360px) {
* {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
* {
font-size: 12px;
}
}
@media screen and (min-width: 768px) {
* {
font-size: 24px;
}
}
/* RTL支持 */
[dir='rtl'] .records-icon {
transform: scaleX(-1);
}
[dir='rtl'] .avatar {
margin-right: 0;
margin-left: 12px;
}
[dir='rtl'] .user-avatar {
margin-right: 0;
margin-left: 12px;
}
[dir='rtl'] .change-btn {
margin-left: 0;
margin-right: 12px;
}
</style>