feat: add sub coin seller management

This commit is contained in:
zhx 2026-07-09 13:36:36 +08:00
parent 572c4cee1b
commit 08ae81d5a8
11 changed files with 746 additions and 1 deletions

View File

@ -120,6 +120,15 @@ export function searchFreightUser(account) {
return get(`/wallet/freight/search-user?account=${account}`)
}
/**
* 经销商搜索已有 coin seller
* @param {string} account - 用户账号
* @returns {Promise} 返回 coin seller 用户信息
*/
export function searchDealerFreightUser(account) {
return get(`/wallet/freight/dealer/search-user?account=${account}`)
}
/**
* 金币充值
* @param {Object} data - 充值数据
@ -131,6 +140,35 @@ export function freightRecharge(data) {
return post('/wallet/freight/ship', data)
}
/**
* 获取当前超级经销商的子 coin seller 列表
* @returns {Promise} 返回子 coin seller 列表
*/
export function getFreightSubSellers() {
return get('/wallet/freight/super/sub-sellers')
}
/**
* 绑定已有 coin seller 为当前超级经销商的子 coin seller
* @param {Object} data
* @param {number|string} data.userId - coin seller 用户ID
* @returns {Promise}
*/
export function addFreightSubSeller(data) {
return post('/wallet/freight/super/sub-sellers', data)
}
/**
* 从当前超级经销商币商钱包转账给子 coin seller 币商钱包
* @param {Object} data
* @param {number|string} data.acceptUserId - coin seller 用户ID
* @param {number|string} data.quantity - 转账金币数量
* @returns {Promise} 返回当前超级经销商剩余余额
*/
export function transferFreightSubSeller(data) {
return post('/wallet/freight/super/sub-sellers/transfer', data)
}
/**
* 获取金币交易流水
* @param {string} data

View File

@ -92,7 +92,20 @@
"join_team_steps": "انا>مركز>طلب الانضمام إلى الفريق",
"enter_team_id": "أدخل id الفريق وانقر على إرسال للانضمام",
"recharge_agency": "وكيل الشحن",
"invite_sub_coin_seller": "Invite sub coin seller",
"my_coins": "عملاتي الذهبية",
"current_balance": "الرصيد الحالي",
"balance": "الرصيد",
"refresh": "تحديث",
"sub_coin_sellers": "بائعو العملات الفرعيون",
"sub_coin_seller_list": "قائمة بائعي العملات الفرعيين",
"enter_coin_seller_id": "أدخل معرف بائع العملات",
"bind_sub_coin_seller": "ربط",
"sub_coin_seller_bound": "تمت إضافة بائع العملات الفرعي",
"no_sub_coin_sellers": "لا يوجد بائعو عملات فرعيون",
"transfer_to_sub_coin_seller": "تحويل إلى بائع العملات الفرعي",
"enter_transfer_amount": "أدخل مبلغ التحويل",
"sub_coin_seller_transfer_success": "تم التحويل بنجاح",
"user": "مستخدم",
"search_user": "البحث عن المستخدم",
"enter_user_id": "يرجى إدخال id المستخدم",
@ -290,6 +303,7 @@
"prowide_income": "الدخل المقدم",
"copied_successfully": "تم النسخ بنجاح",
"please_enter_valid_invitation_code": "يرجى إدخال رمز دعوة صالح.",
"please_enter_valid_amount": "يرجى إدخال مبلغ صالح.",
"invitation_code_bound_successfully": "تم ربط رمز الدعوة بنجاح!",
"invitation_code_binding_failed": "فشل ربط رمز الدعوة.",
"user_id_prefix": "المعرف:",

View File

@ -92,7 +92,20 @@
"join_team_steps": "আমি > হোস্ট সেন্টার > দলে যোগদানের জন্য আবেদন করুন।",
"enter_team_id": "দল আইডি লিখুন, যোগদান করতে পাঠান এ ক্লিক করুন।",
"recharge_agency": "রিচার্জ এজেন্সি",
"invite_sub_coin_seller": "Invite sub coin seller",
"my_coins": "আমার মুদ্রা:",
"current_balance": "বর্তমান ব্যালেন্স",
"balance": "ব্যালেন্স",
"refresh": "রিফ্রেশ",
"sub_coin_sellers": "সাব coin seller",
"sub_coin_seller_list": "সাব coin seller তালিকা",
"enter_coin_seller_id": "coin seller ID লিখুন",
"bind_sub_coin_seller": "বাইন্ড",
"sub_coin_seller_bound": "সাব coin seller যোগ করা হয়েছে",
"no_sub_coin_sellers": "কোনো সাব coin seller নেই",
"transfer_to_sub_coin_seller": "সাব coin seller-এ ট্রান্সফার",
"enter_transfer_amount": "ট্রান্সফার পরিমাণ লিখুন",
"sub_coin_seller_transfer_success": "ট্রান্সফার সফল",
"user": "ব্যবহারকারী:",
"search_user": "ব্যবহারকারী অনুসন্ধান",
"enter_user_id": "দয়া করে ব্যবহারকারী আইডি লিখুন",
@ -290,6 +303,7 @@
"prowide_income": "প্রোওয়াইড আয়",
"copied_successfully": "সফলভাবে কপি করা হয়েছে",
"please_enter_valid_invitation_code": "দয়া করে একটি বৈধ আমন্ত্রণ কোড লিখুন।",
"please_enter_valid_amount": "একটি বৈধ পরিমাণ লিখুন।",
"invitation_code_bound_successfully": "আমন্ত্রণ কোড সফলভাবে বাউন্ড হয়েছে!",
"invitation_code_binding_failed": "আমন্ত্রণ কোড বাউন্ডিং ব্যর্থ।",
"user_id_prefix": "আইডি:",

View File

@ -92,7 +92,20 @@
"join_team_steps": "Me > Host Center >Apply to join the team.",
"enter_team_id": "Enter the team ID,click send to join.",
"recharge_agency": "Recharge agency",
"invite_sub_coin_seller": "Invite sub coin seller",
"my_coins": "My coins",
"current_balance": "Current balance",
"balance": "Balance",
"refresh": "Refresh",
"sub_coin_sellers": "Sub coin sellers",
"sub_coin_seller_list": "Sub coin seller list",
"enter_coin_seller_id": "Enter coin seller ID",
"bind_sub_coin_seller": "Bind",
"sub_coin_seller_bound": "Sub coin seller added",
"no_sub_coin_sellers": "No sub coin sellers",
"transfer_to_sub_coin_seller": "Transfer to sub coin seller",
"enter_transfer_amount": "Enter transfer amount",
"sub_coin_seller_transfer_success": "Transfer successful",
"user": "User ",
"search_user": "Search user",
"enter_user_id": "Please enter the user ID",
@ -290,6 +303,7 @@
"prowide_income": "Prowide income",
"copied_successfully": "Copied successfully",
"please_enter_valid_invitation_code": "Please enter a valid invitation code.",
"please_enter_valid_amount": "Please enter a valid amount.",
"invitation_code_bound_successfully": "Invitation code bound successfully!",
"invitation_code_binding_failed": "Invitation code binding failed.",
"user_id_prefix": "ID:",

View File

@ -92,7 +92,20 @@
"join_team_steps": "Ben > Host Merkezi > Takıma katılmak için başvur.",
"enter_team_id": "Takım ID'sini girin, katılmak için gönder'e tıklayın.",
"recharge_agency": "Yükleme acentesi",
"invite_sub_coin_seller": "Invite sub coin seller",
"my_coins": "Coinlerim:",
"current_balance": "Mevcut bakiye",
"balance": "Bakiye",
"refresh": "Yenile",
"sub_coin_sellers": "Alt coin seller",
"sub_coin_seller_list": "Alt coin seller listesi",
"enter_coin_seller_id": "Coin seller ID girin",
"bind_sub_coin_seller": "Bağla",
"sub_coin_seller_bound": "Alt coin seller eklendi",
"no_sub_coin_sellers": "Alt coin seller yok",
"transfer_to_sub_coin_seller": "Alt coin seller'a transfer",
"enter_transfer_amount": "Transfer tutarını girin",
"sub_coin_seller_transfer_success": "Transfer başarılı",
"user": "Kullanıcı:",
"search_user": "Kullanıcı ara",
"enter_user_id": "Lütfen kullanıcı ID'sini girin",
@ -290,6 +303,7 @@
"prowide_income": "Prowide geliri",
"copied_successfully": "Başarıyla kopyalandı",
"please_enter_valid_invitation_code": "Lütfen geçerli bir davet kodu girin.",
"please_enter_valid_amount": "Lütfen geçerli bir miktar girin.",
"invitation_code_bound_successfully": "Davet kodu başarıyla bağlandı!",
"invitation_code_binding_failed": "Davet kodu bağlama başarısız.",
"user_id_prefix": "ID:",

View File

@ -92,7 +92,20 @@
"join_team_steps": "我>主播中心>申请加入团队",
"enter_team_id": "输入团队ID点击发送加入。",
"recharge_agency": "充值代理",
"invite_sub_coin_seller": "Invite sub coin seller",
"my_coins": "我的金币",
"current_balance": "当前余额",
"balance": "余额",
"refresh": "刷新",
"sub_coin_sellers": "子 coin seller",
"sub_coin_seller_list": "子 coin seller 列表",
"enter_coin_seller_id": "请输入 coin seller ID",
"bind_sub_coin_seller": "绑定",
"sub_coin_seller_bound": "子 coin seller 已添加",
"no_sub_coin_sellers": "暂无子 coin seller",
"transfer_to_sub_coin_seller": "转账给子 coin seller",
"enter_transfer_amount": "请输入转账金额",
"sub_coin_seller_transfer_success": "转账成功",
"user": "用户",
"search_user": "搜索用户",
"enter_user_id": "请输入用户id",
@ -290,6 +303,7 @@
"prowide_income": "提供收入",
"copied_successfully": "复制成功",
"please_enter_valid_invitation_code": "请输入有效的邀请码。",
"please_enter_valid_amount": "请输入有效金额。",
"invitation_code_bound_successfully": "邀请码绑定成功!",
"invitation_code_binding_failed": "邀请码绑定失败。",
"user_id_prefix": "ID:",

View File

@ -143,6 +143,12 @@ const router = createRouter({
component: () => import('../views/RechargeAgency/SellerRecordsView.vue'),
meta: { requiresAuth: true },
},
{
path: '/coin-seller/sub-sellers',
name: 'coin-seller-sub-sellers',
component: () => import('../views/RechargeAgency/SubSellers.vue'),
meta: { requiresAuth: true, requiresSuperDealer: true },
},
{
path: '/recharge-agency-recruit',
name: 'recharge-agency-recruit',

View File

@ -17,6 +17,7 @@ export const USER_ROLES = {
// 页面路径常量
export const PAGES = {
COIN_SELLER: '/coin-seller',
COIN_SELLER_SUB_SELLERS: '/coin-seller/sub-sellers',
AGENCY_CENTER: '/agency-center',
BD_LEADER_CENTER: '/bd-leader-center',
BD_CENTER: '/bd-center',
@ -146,6 +147,7 @@ export const ROLE_PERMISSIONS = {
// Freight Agent (充值代理Rechange Agency/货运代理Freight Agent/金币代理Coin Seller)
[USER_ROLES.FREIGHT_AGENT]: [
PAGES.COIN_SELLER, // 主页面
PAGES.COIN_SELLER_SUB_SELLERS, // 超级经销商子coin seller
'/seller-records',
'/coin-seller-search',
],

View File

@ -5,7 +5,7 @@
* 3. redirecting users to pages they can access
*/
import { getUserIdentity, getMemberProfile } from '../api/wallet.js'
import { apiGetFreightThreshold, getUserIdentity, getMemberProfile } from '../api/wallet.js'
import { isPublicPath } from '@/config/security.js'
import { isDebugMode } from './env.js'
import { permissionManager, PAGES, USER_ROLES } from './permissionManager.js'
@ -156,6 +156,15 @@ class IdentityChecker {
}
}
class SuperDealerRouteChecker {
async check(userId) {
if (!userId) return false
const response = await apiGetFreightThreshold(userId)
return Boolean(response?.status && response.body?.superDealer)
}
}
class PageRedirector {
async redirectToCorrectPage(router, targetPath, currentPath) {
const hasPermission = permissionManager.hasPagePermission(targetPath)
@ -183,6 +192,7 @@ class RouteGuard {
constructor() {
this.appConnectionChecker = new AppConnectionChecker()
this.identityChecker = new IdentityChecker()
this.superDealerRouteChecker = new SuperDealerRouteChecker()
this.pageRedirector = new PageRedirector()
this.isGuardActive = false
}
@ -233,6 +243,16 @@ class RouteGuard {
}
if (permissionManager.hasPagePermission(to.path)) {
if (to.meta?.requiresSuperDealer) {
// Super dealer pages need a second check because the base role only tells us the user is a coin seller.
const isSuperDealer = await this.superDealerRouteChecker.check(getUserId())
if (!isSuperDealer) {
console.groupEnd()
next(PAGES.COIN_SELLER)
return
}
}
console.groupEnd()
next()
return

View File

@ -0,0 +1,566 @@
<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>

View File

@ -92,6 +92,12 @@
</div>
</div>
<!-- 超级经销商子 coin seller 管理入口 -->
<button v-if="isSuperDealer" class="sub-seller-entry" @click="goToSubSellers">
<span>{{ t('invite_sub_coin_seller') }}</span>
<span class="entry-arrow"></span>
</button>
<!-- 设置最小接收金额 -->
<div
style="
@ -422,6 +428,7 @@ const hasRechargeRatio = computed(() => {
Object.prototype.hasOwnProperty.call(freightThreshold.value, 'rechargeRatio')
)
})
const isSuperDealer = computed(() => Boolean(freightThreshold.value?.superDealer))
//
const amountStyle = (amountItem) => {
@ -647,6 +654,11 @@ const searchUser = () => {
router.push('/coin-seller-search')
}
// .
const goToSubSellers = () => {
router.push('/coin-seller/sub-sellers')
}
//
const rechargeNow = useThrottle(async () => {
if (!canRecharge.value || isRecharging.value) return
@ -820,6 +832,37 @@ onMounted(() => {})
color: #d97706;
}
.sub-seller-entry {
width: 100%;
border: none;
background: #ffffff;
padding: 14px 16px;
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font-weight: 800;
text-align: left;
cursor: pointer;
}
.sub-seller-entry span:first-child {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.entry-arrow {
flex: 0 0 auto;
color: #8b45ff;
font-size: 1.6em;
line-height: 1;
}
/* 用户搜索部分 */
.user-section {
background-color: white;