567 lines
13 KiB
Vue
567 lines
13 KiB
Vue
<template>
|
|
<div class="sub-sellers-page gradient-background-circles">
|
|
<GeneralHeader
|
|
:showLanguageList="true"
|
|
:title="t('sub_coin_sellers')"
|
|
color="black"
|
|
style="box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); z-index: 999"
|
|
/>
|
|
|
|
<div class="content">
|
|
<div class="balance-card">
|
|
<div class="section-title">{{ t('current_balance') }}</div>
|
|
<div class="balance-row">
|
|
<img src="../../assets/icon/Azizi/coin.png" alt="" height="20" />
|
|
<span>{{ currentBalance }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="search-card">
|
|
<div class="section-title">{{ t('invite_sub_coin_seller') }}</div>
|
|
<div class="search-row">
|
|
<div class="search-box">
|
|
<img src="../../assets/icon/Azizi/search.png" alt="" />
|
|
<input
|
|
v-model="searchQuery"
|
|
:placeholder="t('enter_coin_seller_id')"
|
|
@keyup.enter="searchCoinSeller"
|
|
@input="onSearchInput"
|
|
dir="ltr"
|
|
/>
|
|
</div>
|
|
<button class="primary-btn compact" :disabled="isSearching" @click="searchCoinSeller">
|
|
{{ isSearching ? t('loading') : t('search') }}
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="searchResultProfile" class="user-card">
|
|
<div class="profile-main">
|
|
<img
|
|
:src="searchResultProfile.userAvatar || ''"
|
|
alt=""
|
|
@error="handleAvatarImageError"
|
|
/>
|
|
<div class="profile-text">
|
|
<div class="profile-name">
|
|
{{ searchResultProfile.userNickname || searchResultProfile.name }}
|
|
</div>
|
|
<div class="profile-id">ID: {{ searchResultProfile.account }}</div>
|
|
</div>
|
|
</div>
|
|
<button class="primary-btn" :disabled="isBinding" @click="bindSubSeller">
|
|
{{ isBinding ? t('loading') : t('bind_sub_coin_seller') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="list-header">
|
|
<div class="section-title">{{ t('sub_coin_seller_list') }}</div>
|
|
<button class="text-btn" :disabled="isLoadingList" @click="fetchSubSellers">
|
|
{{ t('refresh') }}
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="isLoadingList" class="empty-state">{{ t('loading') }}</div>
|
|
<div v-else-if="subSellers.length === 0" class="empty-state">
|
|
{{ t('no_sub_coin_sellers') }}
|
|
</div>
|
|
<div v-else class="seller-list">
|
|
<div
|
|
v-for="seller in subSellers"
|
|
:key="seller.relationId || seller.childUserId"
|
|
class="seller-item"
|
|
>
|
|
<div class="profile-main">
|
|
<img
|
|
:src="profileOf(seller).userAvatar || ''"
|
|
alt=""
|
|
@error="handleAvatarImageError"
|
|
/>
|
|
<div class="profile-text">
|
|
<div class="profile-name">
|
|
{{ profileOf(seller).userNickname || profileOf(seller).name }}
|
|
</div>
|
|
<div class="profile-id">ID: {{ profileOf(seller).account }}</div>
|
|
<div class="profile-id">{{ t('balance') }}: {{ seller.balance || 0 }}</div>
|
|
</div>
|
|
</div>
|
|
<button class="primary-btn" @click="openTransfer(seller)">
|
|
{{ t('transfer') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<maskLayer :maskLayerShow="Boolean(transferTarget)" @click="closeTransfer">
|
|
<div class="modal-wrap">
|
|
<div v-if="transferTarget" class="transfer-modal" @click.stop>
|
|
<div class="modal-title">{{ t('transfer_to_sub_coin_seller') }}</div>
|
|
<div class="modal-target">
|
|
{{ profileOf(transferTarget).userNickname || profileOf(transferTarget).account }}
|
|
</div>
|
|
<input
|
|
v-model="transferAmount"
|
|
class="amount-input"
|
|
:placeholder="t('enter_transfer_amount')"
|
|
inputmode="numeric"
|
|
dir="ltr"
|
|
@input="onAmountInput"
|
|
@keydown="onKeyDown"
|
|
/>
|
|
<div class="modal-actions">
|
|
<button class="ghost-btn" @click="closeTransfer">{{ t('cancel') }}</button>
|
|
<button class="primary-btn" :disabled="!canTransfer" @click="confirmTransfer">
|
|
{{ isTransferring ? t('loading') : t('confirm') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</maskLayer>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { setDocumentDirection } from '@/locales/i18n'
|
|
import { showError, showSuccess } from '@/utils/toast.js'
|
|
import { useDebounce, useThrottle } from '@/utils/useDebounce'
|
|
import { handleAvatarImageError } from '@/utils/image/imageHandler.js'
|
|
import {
|
|
addFreightSubSeller,
|
|
getFreightBalance,
|
|
getFreightSubSellers,
|
|
searchDealerFreightUser,
|
|
transferFreightSubSeller,
|
|
} from '@/api/wallet.js'
|
|
|
|
import GeneralHeader from '@/components/GeneralHeader.vue'
|
|
import maskLayer from '@/components/MaskLayer.vue'
|
|
|
|
const { t, locale } = useI18n()
|
|
locale.value && setDocumentDirection(locale.value)
|
|
|
|
const currentBalance = ref('0')
|
|
const subSellers = ref([])
|
|
const isLoadingList = ref(false)
|
|
const searchQuery = ref('')
|
|
const searchResult = ref(null)
|
|
const searchResultAccount = ref('')
|
|
const isSearching = ref(false)
|
|
const isBinding = ref(false)
|
|
const transferTarget = ref(null)
|
|
const transferAmount = ref('')
|
|
const isTransferring = ref(false)
|
|
|
|
const searchResultProfile = computed(() => searchResult.value?.userProfile || null)
|
|
const positiveIntegerPattern = /^[1-9]\d*$/
|
|
const canTransfer = computed(() => {
|
|
return (
|
|
transferTarget.value &&
|
|
positiveIntegerPattern.test(transferAmount.value) &&
|
|
Number(transferAmount.value) <= Number(currentBalance.value || 0) &&
|
|
!isTransferring.value
|
|
)
|
|
})
|
|
|
|
const profileOf = (seller) => seller?.userProfile || {}
|
|
|
|
const onKeyDown = (event) => {
|
|
if (['.', 'e', '+', '-'].includes(event.key)) {
|
|
event.preventDefault()
|
|
}
|
|
}
|
|
|
|
const onAmountInput = (event) => {
|
|
const rawValue = event.target.value.trim()
|
|
if (rawValue === '') {
|
|
transferAmount.value = ''
|
|
event.target.value = ''
|
|
return
|
|
}
|
|
|
|
if (!/^\d+$/.test(rawValue)) {
|
|
event.target.value = transferAmount.value
|
|
return
|
|
}
|
|
|
|
let value = rawValue.replace(/^0+(?=\d)/, '')
|
|
if (value === '0') {
|
|
value = ''
|
|
}
|
|
const maxTransferAmount = Math.floor(Number(currentBalance.value || 0))
|
|
if (value !== '' && Number(value) > maxTransferAmount) {
|
|
value = maxTransferAmount > 0 ? String(maxTransferAmount) : ''
|
|
}
|
|
event.target.value = value
|
|
transferAmount.value = value
|
|
}
|
|
|
|
const onSearchInput = () => {
|
|
const account = searchQuery.value.trim()
|
|
if (!account || account !== searchResultAccount.value) {
|
|
searchResult.value = null
|
|
searchResultAccount.value = ''
|
|
}
|
|
}
|
|
|
|
const fetchBalance = async () => {
|
|
try {
|
|
const response = await getFreightBalance()
|
|
if (response?.status && response.body !== undefined) {
|
|
currentBalance.value = response.body.toString()
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch freight balance:', error)
|
|
}
|
|
}
|
|
|
|
const fetchSubSellers = async () => {
|
|
try {
|
|
isLoadingList.value = true
|
|
const response = await getFreightSubSellers()
|
|
subSellers.value = response?.status && response.body ? response.body : []
|
|
} catch (error) {
|
|
console.error('Failed to fetch sub coin sellers:', error)
|
|
showError(error.response?.errorMsg || t('something_went_wrong'))
|
|
subSellers.value = []
|
|
} finally {
|
|
isLoadingList.value = false
|
|
}
|
|
}
|
|
|
|
const searchCoinSeller = useDebounce(
|
|
async () => {
|
|
const account = searchQuery.value.trim()
|
|
if (!account || isSearching.value) return
|
|
|
|
try {
|
|
isSearching.value = true
|
|
searchResult.value = null
|
|
searchResultAccount.value = ''
|
|
const response = await searchDealerFreightUser(account)
|
|
if (response?.status && response.body && searchQuery.value.trim() === account) {
|
|
searchResult.value = response.body
|
|
searchResultAccount.value = account
|
|
}
|
|
} catch (error) {
|
|
showError(error.response?.errorMsg || t('something_went_wrong'))
|
|
} finally {
|
|
isSearching.value = false
|
|
}
|
|
},
|
|
500,
|
|
true,
|
|
)
|
|
|
|
const bindSubSeller = async () => {
|
|
const userId = searchResultProfile.value?.id
|
|
if (!userId || isBinding.value || searchQuery.value.trim() !== searchResultAccount.value) {
|
|
searchResult.value = null
|
|
searchResultAccount.value = ''
|
|
return
|
|
}
|
|
|
|
try {
|
|
isBinding.value = true
|
|
await addFreightSubSeller({ userId })
|
|
showSuccess(t('sub_coin_seller_bound'))
|
|
searchQuery.value = ''
|
|
searchResult.value = null
|
|
searchResultAccount.value = ''
|
|
await fetchSubSellers()
|
|
} catch (error) {
|
|
showError(error.response?.errorMsg || t('something_went_wrong'))
|
|
} finally {
|
|
isBinding.value = false
|
|
}
|
|
}
|
|
|
|
const openTransfer = (seller) => {
|
|
transferTarget.value = seller
|
|
transferAmount.value = ''
|
|
}
|
|
|
|
const closeTransfer = () => {
|
|
transferTarget.value = null
|
|
transferAmount.value = ''
|
|
}
|
|
|
|
const confirmTransfer = useThrottle(async () => {
|
|
if (!canTransfer.value) return
|
|
|
|
const acceptUserId = transferTarget.value.childUserId || transferTarget.value.userProfile?.id
|
|
const quantity = Number(transferAmount.value)
|
|
try {
|
|
isTransferring.value = true
|
|
const response = await transferFreightSubSeller({ acceptUserId, quantity })
|
|
if (response?.status) {
|
|
if (response.body !== undefined) {
|
|
currentBalance.value = response.body.toString()
|
|
}
|
|
showSuccess(t('sub_coin_seller_transfer_success'))
|
|
closeTransfer()
|
|
await fetchSubSellers()
|
|
}
|
|
} catch (error) {
|
|
showError(error.response?.errorMsg || t('something_went_wrong'))
|
|
} finally {
|
|
isTransferring.value = false
|
|
}
|
|
}, 2000)
|
|
|
|
onMounted(() => {
|
|
fetchBalance()
|
|
fetchSubSellers()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
* {
|
|
color: rgba(0, 0, 0, 0.8);
|
|
}
|
|
|
|
.sub-sellers-page {
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.content {
|
|
padding: 16px;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.balance-card,
|
|
.search-card,
|
|
.seller-item {
|
|
background: #ffffff;
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
margin-bottom: 16px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.section-title {
|
|
font-weight: 800;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.balance-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: #ffb627;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.balance-row span {
|
|
color: #ffb627;
|
|
}
|
|
|
|
.search-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.search-box {
|
|
flex: 1;
|
|
min-width: 0;
|
|
height: 40px;
|
|
border-radius: 24px;
|
|
border: 1px solid #ffffff;
|
|
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 10px;
|
|
}
|
|
|
|
.search-box img {
|
|
width: 22px;
|
|
aspect-ratio: 1/1;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.search-box input,
|
|
.amount-input {
|
|
width: 100%;
|
|
border: none;
|
|
outline: none;
|
|
background: transparent;
|
|
font-weight: 600;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.search-box input {
|
|
padding: 8px;
|
|
}
|
|
|
|
.user-card,
|
|
.seller-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
.profile-main {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.profile-main img {
|
|
width: 44px;
|
|
aspect-ratio: 1/1;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.profile-text {
|
|
min-width: 0;
|
|
}
|
|
|
|
.profile-name {
|
|
font-weight: 800;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.profile-id {
|
|
margin-top: 3px;
|
|
font-size: 0.9em;
|
|
color: rgba(0, 0, 0, 0.45);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.primary-btn,
|
|
.ghost-btn,
|
|
.text-btn {
|
|
border: none;
|
|
font-weight: 800;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.primary-btn {
|
|
flex: 0 0 auto;
|
|
border-radius: 24px;
|
|
padding: 9px 14px;
|
|
background: linear-gradient(135deg, #bb92ff 2.82%, #8b45ff 99.15%);
|
|
color: white;
|
|
}
|
|
|
|
.primary-btn:disabled {
|
|
background: #d1d5db;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.compact {
|
|
min-width: 76px;
|
|
}
|
|
|
|
.text-btn {
|
|
background: transparent;
|
|
color: #8b45ff;
|
|
}
|
|
|
|
.ghost-btn {
|
|
border-radius: 24px;
|
|
padding: 9px 14px;
|
|
background: #f3f4f6;
|
|
color: rgba(0, 0, 0, 0.55);
|
|
}
|
|
|
|
.list-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.seller-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.seller-item {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.empty-state {
|
|
padding: 24px 12px;
|
|
text-align: center;
|
|
color: rgba(0, 0, 0, 0.45);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.modal-wrap {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.transfer-modal {
|
|
width: 82%;
|
|
border-radius: 12px;
|
|
background: #ffffff;
|
|
padding: 20px;
|
|
}
|
|
|
|
.modal-title {
|
|
text-align: center;
|
|
font-weight: 900;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.modal-target {
|
|
text-align: center;
|
|
color: rgba(0, 0, 0, 0.5);
|
|
font-weight: 700;
|
|
margin-bottom: 16px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.amount-input {
|
|
height: 44px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
|
|
padding: 0 14px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.modal-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-around;
|
|
gap: 12px;
|
|
}
|
|
|
|
@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: 14px;
|
|
}
|
|
}
|
|
</style>
|