This commit is contained in:
zhx 2026-07-21 14:36:20 +08:00
parent 721638c5bc
commit 6cb7132e22
13 changed files with 3770 additions and 12 deletions

View File

@ -0,0 +1,292 @@
<template>
<div>
<!-- 状态栏占位区域仅在APP中显示 -->
<div v-if="isInAppEnvironment" style="height: 30px"></div>
<!-- header内容 -->
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
height: 60px;
"
>
<!-- 返回键 -->
<div style="width: 1.5em; display: flex; align-items: center">
<button v-if="showBack" class="back-btn" @click="handleBack">
<img
v-smart-img
class="flipImg"
:src="backImg"
alt=""
style="width: 100%; aspect-ratio: 1/1; display: block"
/>
</button>
</div>
<!-- 标题 -->
<h1
style="
font-size: 1.2em;
font-weight: 600;
margin: 0;
text-align: center;
flex: 1;
letter-spacing: 0.3px;
"
:style="{ color }"
>
{{ title }}
</h1>
<!-- 右侧按钮 -->
<!-- 帮助按钮 -->
<button v-if="showHelp" class="help-btn" style="width: 1.5em" @click="$emit('help')">
<img v-smart-img :src="helpImg" alt="" style="width: 100%; display: block" />
</button>
<!-- 语言切换 -->
<div
v-if="showLanguageList"
style="
display: flex;
align-items: center;
position: relative;
color: rgba(187, 146, 255, 1) !important;
"
@click="showLang"
>
<div style="font-weight: bold">{{ currentLangName }}</div>
<transition name="slide-fade">
<div v-show="visibleList" class="extraList">
<div
style="padding: 8px; color: black !important; font-weight: 590"
v-for="lang in langStore.langList"
@click.stop="handleLanguageChange(lang)"
>
{{ lang.value }}
</div>
</div>
</transition>
</div>
<!-- 插槽 -->
<slot name="extraFunction"></slot>
</div>
</div>
</template>
<script setup>
import { computed, ref, onMounted, watch, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useLangStore } from '@/stores/lang'
import { setLocale } from '@/locales/i18n'
import { isInApp, closePage } from '@/utils/appBridge.js'
// props
const props = defineProps({
//
title: {
type: String,
required: true,
},
//
showBack: {
type: Boolean,
default: true,
},
//
showHelp: {
type: Boolean,
default: false,
},
//
isHomePage: {
type: Boolean,
default: false,
},
//
showLanguageList: {
type: Boolean,
default: false,
},
//
color: {
type: String,
default: '#fff',
},
//
backImg: {
type: String,
default: () => {
return new URL('../assets/icon/Azizi/arrowBackBlack.png', import.meta.url).href
},
},
//
helpImg: {
type: String,
default: () => {
return new URL('../assets/icon/Azizi/helpWhite.png', import.meta.url).href
},
},
//
backQuery: {
type: Object,
default: null,
},
})
// emits
defineEmits(['help'])
const router = useRouter()
const route = useRoute()
const langStore = useLangStore()
const visibleList = ref(false) //
//
const currentLangName = computed(() => {
return langStore.selectedLang?.value || 'EN'
})
// UI
watch(
() => langStore.selectedLang,
(newLang) => {
console.log('Language changed in header:', newLang)
},
{ deep: true },
)
const handleLanguageChange = (item) => {
visibleList.value = false
console.log('visibleList.value', visibleList.value)
console.log('Changing language to:', item)
setLocale(langStore, item.type)
// visibleList
}
// APP
const isInAppEnvironment = ref(false)
//
const isCurrentlyHomePage = computed(() => {
return !!props.isHomePage
})
const handleBack = () => {
if (isCurrentlyHomePage.value && isInApp()) {
// APP
console.log('home back')
closePage()
} else {
//
router.go(-1)
if (props.backQuery != null) {
console.log('props.backQuery', props.backQuery)
// 使 nextTick
nextTick(() => {
// 使 replace
router.replace({
path: props.backQuery.from,
query: {
activeAction: props.backQuery.activeAction,
},
})
})
}
}
}
//
const showLang = () => {
visibleList.value = !visibleList.value
console.log('visibleList.value', visibleList.value)
}
//
onMounted(() => {
isInAppEnvironment.value = isInApp()
})
</script>
<style scoped>
.back-btn,
.help-btn {
border: none;
background: transparent;
transition: all 0.2s ease;
}
.back-btn:active,
.help-btn:active {
transform: scale(0.9);
}
.extraList {
position: absolute;
z-index: 9;
right: 0;
top: 100%;
padding: 10px;
display: flex;
flex-direction: column;
width: max-content;
background-color: rgba(255, 255, 255);
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin-top: 5px;
backdrop-filter: blur(8px);
}
.extraList > div:not(:last-child) {
border-bottom: 0.1px solid black;
}
[dir='rtl'] .flipImg {
transform: scaleX(-1);
}
[dir='rtl'] .extraList {
right: auto;
left: 0;
}
@media screen and (max-width: 360px) {
* {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
* {
font-size: 16px;
}
}
@media screen and (min-width: 768px) {
* {
font-size: 24px;
}
}
@media screen and (min-width: 1024px) {
* {
font-size: 32px;
}
}
</style>

View File

@ -0,0 +1,392 @@
<template>
<div class="fullPage">
<div class="bg">
<!-- 顶部导航 -->
<GeneralHeader
:showLanguageList="true"
:title="t('item_distribution')"
color="black"
style="box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); z-index: 999"
/>
<!-- 标签 -->
<div
style="
display: flex;
justify-content: space-around;
background-color: transparent;
position: relative;
"
>
<div
v-for="(item, index) in tabList"
:key="index"
style="font-size: 1.3em; padding: 10px 0; flex: 1; text-align: center"
class="tab-item"
:class="activeIndex == index ? 'tabName-active' : 'tabName'"
ref="tabItems"
@click="setActiveTab(index)"
>
{{ t(item.name) }}
</div>
<!-- 下划线元素 -->
<div
style="width: 15px; height: 3px; background-color: #bb92ff"
:style="underlineStyle"
></div>
</div>
<!-- 商品列表 -->
<div
style="
width: 100%;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
padding: 15px;
"
>
<div v-for="(product, index) in productList" :key="index" @click="showSendDialog(product)">
<div
style="
background-color: white;
border-radius: 10px;
box-shadow: 0px 0px 3px 1px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
align-items: center;
"
>
<img :src="product.cover" alt="" style="width: 60%; margin: 10px 0" />
<button
style="
background-color: #bb92ff;
color: white;
margin-bottom: 10px;
border: 0;
border-radius: 20px;
padding: 5px 10px;
"
>
{{ t('send') }}
</button>
</div>
</div>
</div>
<!-- 弹窗 -->
<!-- 遮罩层 -->
<maskLayer :maskLayerShow="dialogshow" @click="closedPopup">
<div
style="
padding: 20px;
background-color: white;
display: flex;
flex-direction: column;
align-items: center;
border-radius: 20px;
margin: 20% 5%;
"
@click.stop
>
<img :src="selectedGoods.cover" alt="" style="width: 60%; margin: 5px 0" />
<div style="font-weight: 600; margin: 10px 0">
<span style="color: #00000080; margin-right: 10px">{{ t('validity') }}:</span>
<span style="font-weight: 700; color: #000">{{ validityPeriod }}{{ t('day') }}</span>
</div>
<div
style="
width: 90%;
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
"
>
<input
type="text"
:placeholder="t('enter_user_id_placeholder')"
style="
flex: 1;
min-width: 0;
align-self: stretch;
border-radius: 8px;
border: 0;
outline: none;
background-color: #f1f1f1b2;
padding: 8px;
margin-right: 4px;
font-size: 1em;
font-weight: 590;
"
v-model="targetUserId"
/>
<button
style="
width: auto;
min-width: 0;
align-self: stretch;
border-radius: 8px;
border: 0;
padding: 8px;
color: white;
font-size: 1em;
font-weight: 590;
"
:style="{ backgroundColor: confirmClick ? '#bb92ff' : '#00000080' }"
@click="sendGoods"
>
{{ t('confirm') }}
</button>
</div>
</div>
</maskLayer>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, computed } from 'vue'
import { showWarning, showSuccess } from '@/utils/toast.js'
import { useI18n } from 'vue-i18n'
import { getAdminCenterList, giveProps, hasSendGiftsPermission } from '@/api/itemDistribution'
import { searchUser } from '@/api/userInfo'
import GeneralHeader from '@/components/GeneralHeader.vue'
import maskLayer from '@/components/MaskLayer.vue'
const { t, locale } = useI18n()
const tabList = ref([
{
id: 1,
name: 'frames',
},
{
id: 2,
name: 'vehicles',
},
// {
// id: 3,
// name: 'chat_box',
// },
// {
// id: 4,
// name: 'theme',
// },
])
const activeIndex = ref(0)
const currentTab = ref('frames') //
const tabItems = ref([]) // DOM
const confirmClick = ref(true)
//
const setActiveTab = (index) => {
if (activeIndex.value != index) {
console.log('切换标签')
productList.value = [] //
activeIndex.value = index
currentTab.value = tabList.value[index].name
loadProps()
}
}
// 线
const underlineStyle = computed(() => {
console.log('计算属性')
// 访 locale.value 使
const currentLocale = locale.value
console.log('当前语言:', currentLocale)
if (tabItems.value.length === 0) return {} //
const activeTab = tabItems.value[activeIndex.value]
console.log('选中元素的左边距', activeTab.offsetLeft)
return {
position: 'absolute',
left: `${activeTab.offsetLeft + activeTab.offsetWidth / 2 - 15 / 2}px`,
bottom: '0',
transition: 'left 0.3s ease', //
}
})
const targetUserId = ref('')
//
const getHasSendVIPPermission = async (propsType) => {
const response = await hasSendGiftsPermission(propsType)
console.log('response:', response)
return response.body
}
const sendGoods = async () => {
if (confirmClick.value) {
confirmClick.value = false
console.log('赠送用户id', targetUserId.value)
console.log('赠送商品:', selectedGoods.value)
try {
const targetUserInfo = await searchUser(targetUserId.value)
console.log('targetUserInfo:', targetUserInfo)
if (targetUserInfo.status && targetUserInfo.body) {
const data = {
propsId: selectedGoods.value.id,
acceptUserId: targetUserInfo.body.id,
}
const res = await giveProps(data)
if (res.errorCode == 0 && res.status) {
closedPopup()
showSuccess(t('gift_delivery_successful'))
} else {
confirmClick.value = true
}
} else {
confirmClick.value = true
}
} catch (error) {
console.log('error:', error)
// id
showWarning(error.response.errorMsg)
confirmClick.value = true
throw error
}
}
}
//
const getPropsType = () => {
const typeMap = {
frames: 'AVATAR_FRAME',
vehicles: 'RIDE',
chat_box: 'CHAT_BUBBLE',
theme: 'THEME',
vip: 'NOBLE_VIP',
}
return typeMap[currentTab.value]
}
//
const loadProps = async () => {
try {
const params = {
propsType: getPropsType(),
currencyType: 'GOLD',
}
const response = await getAdminCenterList(params)
console.log('response:', response)
productList.value = response.body
} catch (error) {
throw error
}
}
//
const productList = ref([])
//
const selectedGoods = ref({})
const validityPeriod = computed(() => {
return currentTab.value == 'VIP' ? '3' : '7'
})
//
const getImageUrl = (imgUrl) => {
return new URL(imgUrl, import.meta.url).href
}
const dialogshow = ref(false)
//
const showSendDialog = (product) => {
console.log('product:', product)
selectedGoods.value = product
dialogshow.value = true
}
//
const closedPopup = () => {
confirmClick.value = true
selectedGoods.value = {}
targetUserId.value = ''
dialogshow.value = false
}
onMounted(() => {
console.log('onMounted')
// VIP
if (getHasSendVIPPermission('NOBLE_VIP')) {
tabList.value.push({
id: 3,
name: 'vip',
})
}
if (tabItems.value.length > 0) {
underlineStyle.value //
}
loadProps()
})
</script>
<style scoped>
* {
color: black;
}
.fullPage {
background-color: #ffffff;
min-height: 100vh;
position: relative;
}
.bg {
width: 100vw;
min-height: 100vh;
background-image: url(../../assets/images/Azizi/secondBg.png);
background-size: 100% auto;
background-repeat: no-repeat;
}
.tabName {
color: #00000066;
}
.tabName-active {
color: black;
font-weight: 500;
}
@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;
}
}
@media screen and (min-width: 1024px) {
* {
font-size: 32px;
}
}
</style>

View File

@ -0,0 +1,145 @@
<template>
<div
v-show="maskLayerShow"
class="mask-layer"
@click.stop
@mousedown.stop
@mouseup.stop
@pointerdown.stop
@pointerup.stop
@touchstart.stop
@touchend.stop
@touchmove.prevent
@wheel.prevent
>
<div class="mask-layer__content" @touchmove.stop>
<slot></slot>
</div>
</div>
</template>
<script setup>
import { onUnmounted, watch } from 'vue'
const props = defineProps({
maskLayerShow: {
type: Boolean,
required: true,
},
lockScroll: {
type: Boolean,
default: true,
},
})
let lockCount = 0
let lockedScrollY = 0
let previousBodyStyle = {}
let previousHtmlStyle = {}
const getStyleSnapshot = (el) => ({
overflow: el.style.overflow,
position: el.style.position,
top: el.style.top,
left: el.style.left,
right: el.style.right,
width: el.style.width,
touchAction: el.style.touchAction,
overscrollBehavior: el.style.overscrollBehavior,
})
const restoreStyleSnapshot = (el, style) => {
Object.keys(style).forEach((key) => {
el.style[key] = style[key]
})
}
// iOS body
const lockPageScroll = () => {
if (typeof window === 'undefined' || typeof document === 'undefined') return
if (lockCount === 0) {
const body = document.body
const html = document.documentElement
lockedScrollY = window.scrollY || html.scrollTop || body.scrollTop || 0
previousBodyStyle = getStyleSnapshot(body)
previousHtmlStyle = getStyleSnapshot(html)
html.style.overflow = 'hidden'
html.style.touchAction = 'none'
html.style.overscrollBehavior = 'none'
body.style.overflow = 'hidden'
body.style.position = 'fixed'
body.style.top = `-${lockedScrollY}px`
body.style.left = '0'
body.style.right = '0'
body.style.width = '100%'
body.style.touchAction = 'none'
body.style.overscrollBehavior = 'none'
}
lockCount += 1
}
const unlockPageScroll = () => {
if (typeof window === 'undefined' || typeof document === 'undefined') return
if (lockCount <= 0) return
lockCount -= 1
if (lockCount === 0) {
restoreStyleSnapshot(document.body, previousBodyStyle)
restoreStyleSnapshot(document.documentElement, previousHtmlStyle)
window.scrollTo(0, lockedScrollY)
}
}
watch(
() => props.maskLayerShow,
(newVal) => {
if (!props.lockScroll) return
if (newVal) {
lockPageScroll()
} else {
unlockPageScroll()
}
},
{ immediate: true }
)
onUnmounted(() => {
if (props.lockScroll && props.maskLayerShow) {
unlockPageScroll()
}
})
</script>
<style lang="scss" scoped>
* {
color: black;
}
.mask-layer {
width: 100vw;
height: 100vh;
background-color: #00000080;
position: fixed;
inset: 0;
z-index: 10000 !important;
overflow: hidden;
touch-action: none;
overscroll-behavior: none;
-webkit-tap-highlight-color: transparent;
}
.mask-layer__content {
position: absolute;
inset: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
}
</style>

View File

@ -0,0 +1,522 @@
<template>
<div class="fullPage gradient-background-circles">
<!-- 顶部导航 -->
<GeneralHeader
:showLanguageList="true"
:title="t('my_agency_teams')"
color="black"
style="box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); z-index: 999"
/>
<!-- 主要内容 -->
<div
style="
padding: 16px;
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
gap: 12px;
"
>
<!-- 总收入信息 -->
<div
style="
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 16px;
border: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
gap: 10px;
"
>
<div style="display: flex; align-items: flex-end">
<div style="font-weight: 600; font-size: 0.9em">{{ t('total_income') }}</div>
<div style="font-weight: 700">${{ TeamOverview?.totalIncome || 0 }}</div>
</div>
<div style="display: flex; align-items: flex-end">
<div style="font-weight: 600; font-size: 0.9em">{{ t('previous_period_income') }}</div>
<div style="font-weight: 700">${{ TeamOverview?.previousPeriodIncome || 0 }}</div>
</div>
</div>
<!-- Agency团队收入信息 -->
<div
style="
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 16px;
border: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
gap: 10px;
"
>
<!-- 团队信息 -->
<div style="font-weight: 700">
{{ t('agencies_label') }} {{ TeamOverview?.memberCount || 0 }}
</div>
<div style="font-size: 0.9em; font-weight: 600">
{{ t('team_total_income_label') }} ${{ TeamOverview?.teamTotalIncome || 0 }}
</div>
<!-- Agency成员列表 -->
<div
v-for="AgencyInfo in TeamOverview?.members"
style="
position: relative;
background-color: white;
padding: 14px 10px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-around;
gap: 4px;
"
>
<!-- 头像 -->
<div
style="
width: 15%;
align-self: stretch;
display: flex;
align-items: center;
justify-content: center;
"
>
<img
:src="AgencyInfo.avatar || ''"
alt=""
style="
display: block;
width: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
object-fit: cover;
"
@error="handleAvatarImageError"
/>
</div>
<!-- 基本信息 -->
<div
style="
flex: 1;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: space-around;
gap: 4px;
"
>
<div
style="
font-weight: 700;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ AgencyInfo.userName || '-' }}
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-size: 0.8em; font-weight: 500">
{{ t('user_id_prefix') }}{{ AgencyInfo.account || '-' }}
</div>
</div>
<!-- 收入信息 -->
<div
style="
width: auto;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: center;
"
>
<div style="font-size: 0.8em; font-weight: 500; text-align: end">
{{ t('number_of_hosts_personnel_label') }} {{ AgencyInfo?.subMemberCount || 0 }}
</div>
<div style="font-size: 0.8em; font-weight: 500; text-align: end">
{{ t('total_income') }} ${{ AgencyInfo?.totalIncome || 0 }}
</div>
</div>
<!-- 更多按钮 -->
<div class="moreBt" @click="incomeShow(AgencyInfo)">
{{ t('more') }}
</div>
</div>
</div>
</div>
<!-- 遮罩层 -->
<maskLayer :maskLayerShow="maskLayerShow" @click="closedPopup">
<div style="position: fixed; bottom: 0; width: 100%">
<!-- 某个Agency的收益 -->
<div
v-if="showIncome"
style="
background: white;
border-radius: 16px 16px 0 0;
max-height: 80vh;
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px 12px 30px;
"
class="overflow-y-auto"
@click.stop
>
<div style="display: flex; justify-content: center; align-items: center">
<div style="margin: 0; font-weight: 600; color: #333">
{{ t('agency_id_label') }} {{ moreUserAccount }}
</div>
</div>
<!-- 总体信息 -->
<div
style="
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
border-radius: 8px;
padding: 12px;
margin: 0 4px;
"
>
<div>
<div style="display: grid; gap: 4px">
<div style="font-size: 0.9em; font-weight: 600">
{{ t('total_income') }} ${{ moreDetail?.totalIncome || 0 }}
</div>
<div style="font-size: 0.9em; font-weight: 600">
{{ t('previous_period_income') }}: ${{ moreDetail?.previousPeriodIncome || 0 }}
</div>
</div>
</div>
</div>
<div style="font-weight: 600">{{ t('income_details_label') }}</div>
<!-- 历史薪资列表 -->
<div style="overflow-y: auto" class="salaryList">
<div
style="display: flex; flex-direction: column; gap: 8px"
v-for="incomeInfo in moreDetail?.incomeDetails"
>
<!-- 进行中 -->
<historySalary
v-if="incomeInfo.status === 'In Progress'"
type="In Progress"
@showMore="historyShow(incomeInfo)"
>
<template v-slot:content>
<div>{{ incomeInfo.settlementPeriod }}</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px">
<div>{{ t('host_label') }} {{ incomeInfo.memberCount }}</div>
<!-- 团队收入 -->
<div>{{ t('team_total_income_text') }} ${{ incomeInfo.teamTotalIncome }}</div>
</div>
</template>
</historySalary>
<!-- 已完成 -->
<historySalary
v-if="incomeInfo.status === 'Completed'"
type="Completed"
@showMore="historyShow(incomeInfo)"
>
<template v-slot:content>
<div>{{ incomeInfo.settlementPeriod }}</div>
<div>{{ t('host_label') }} {{ incomeInfo.memberCount }}</div>
<!-- 团队收入 -->
<div>{{ t('team_total_income_text') }} ${{ incomeInfo.teamTotalIncome }}</div>
</template>
</historySalary>
</div>
</div>
</div>
<!-- 某个时期的Host收益详情 -->
<div
v-if="showHistory"
style="
background: white;
border-radius: 16px 16px 0 0;
max-height: 60vh;
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px 12px 30px;
"
class="overflow-y-auto"
@click.stop
>
<div style="font-weight: 600">{{ incomeDetailsItem.settlementPeriod }}</div>
<div style="display: flex; justify-content: space-between">
<div style="font-size: 0.8em; font-weight: 600">
{{ t('host_label') }}{{ incomeDetailsItem.memberCount }}
</div>
<div style="font-size: 0.8em; font-weight: 600">
{{ t('team_total_income_text_capitalized') }} ${{ incomeDetailsItem.teamTotalIncome }}
</div>
</div>
<!-- Host列表 -->
<div
v-for="member in incomeDetailsItem.members"
style="
background-color: white;
padding: 14px 10px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
"
>
<!-- 头像 -->
<div
style="
width: 15%;
align-self: stretch;
display: flex;
align-items: center;
justify-content: center;
"
>
<img
:src="member.avatar || ''"
alt=""
style="
display: block;
width: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
object-fit: cover;
"
@error="handleAvatarImageError"
/>
</div>
<!-- 基本信息 -->
<div
style="
flex: 1;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: space-around;
gap: 4px;
"
>
<div
style="
font-weight: 700;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ member.userName }}
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-size: 0.8em; font-weight: 500">
{{ t('user_id_prefix') }}{{ member.account }}
</div>
</div>
<!-- 收入信息 -->
<div
style="
width: auto;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: space-around;
"
>
<div style="font-size: 0.8em; font-weight: 500">{{ t('host_income_label') }}</div>
<div style="font-size: 0.8em; font-weight: 500">${{ member.income }}</div>
</div>
</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 { handleAvatarImageError } from '@/utils/image/imageHandler.js'
import {
apiGetTeamOverview, //BD
apiGetTeamMemberDetail, // BDMore
} from '@/api/admin.js'
import GeneralHeader from '@/components/GeneralHeader.vue'
import maskLayer from '@/components/MaskLayer.vue'
import historySalary from './components/historySalary.vue'
const { t, locale } = useI18n()
//
locale.value && setDocumentDirection(locale.value)
const TeamOverview = ref(null) //
const moreUserAccount = ref('') //id
const moreDetail = ref(null) //
const incomeDetailsItem = ref(null) //
const showIncome = ref(false)
const showHistory = ref(false)
//
const maskLayerShow = computed(() => showIncome.value || showHistory.value)
const incomeShow = (AgencyInfo) => {
console.log('点击了收入按钮')
console.log('AgencyInfo.userId:', AgencyInfo.userId)
moreUserAccount.value = AgencyInfo.account
getTeamMemberDetail(AgencyInfo.userId)
showIncome.value = true
}
const historyShow = (incomeInfo) => {
console.log('点击了历史薪资按钮')
console.log('incomeInfo:', incomeInfo)
incomeDetailsItem.value = incomeInfo
showIncome.value = false
showHistory.value = true
}
//
const closedPopup = () => {
showIncome.value = false
showHistory.value = false
moreUserAccount.value = ''
moreDetail.value = null
incomeDetailsItem.value = null
}
//
const getTeamOverview = async () => {
let data = {
type: 'AGENCY',
}
const resInvitedList = await apiGetTeamOverview(data)
console.log('任务列表:', resInvitedList)
if (resInvitedList.status && resInvitedList.body) {
TeamOverview.value = resInvitedList.body
}
}
// AGENCYMore
const getTeamMemberDetail = async (userId) => {
let data = {
targetId: userId,
type: 'AGENCY',
}
const resInvitedList = await apiGetTeamMemberDetail(data)
console.log('任务列表:', resInvitedList)
if (resInvitedList.status && resInvitedList.body) {
moreDetail.value = resInvitedList.body
}
}
//
const init = () => {
getTeamOverview()
}
onMounted(() => {
init()
})
</script>
<style lang="scss" scoped>
* {
color: rgba(0, 0, 0, 0.8);
}
.fullPage {
max-height: 100vh;
overflow-y: auto;
background-color: #ffffff;
background-image: url(../../assets/images/Azizi/secondBg.png);
}
.fullPage::-webkit-scrollbar {
display: none;
}
.overflow-y-auto {
overflow-y: auto;
}
.overflow-y-auto::-webkit-scrollbar {
display: none;
}
.salaryList::-webkit-scrollbar {
display: none;
}
.moreBt {
font-size: 0.8em;
position: absolute;
bottom: 4%;
right: 4%;
color: #bb92ff;
font-weight: 500;
}
[dir='rtl'] .moreBt {
left: 4%;
right: auto;
}
@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;
}
}
@media screen and (min-width: 1024px) {
* {
font-size: 32px;
}
}
</style>

View File

@ -0,0 +1,524 @@
<template>
<div class="fullPage gradient-background-circles">
<!-- 顶部导航 -->
<GeneralHeader
:showLanguageList="true"
:title="t('my_bd_leader_teams')"
color="black"
style="box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); z-index: 999"
/>
<!-- 主要内容 -->
<div
style="
padding: 16px;
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
gap: 12px;
"
>
<!-- 总收入信息 -->
<div
style="
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 16px;
border: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
gap: 10px;
"
>
<div style="display: flex; align-items: flex-end">
<div style="font-weight: 600; font-size: 0.9em">{{ t('total_income') }}</div>
<div style="font-weight: 700">${{ TeamOverview?.totalIncome || 0 }}</div>
</div>
<div style="display: flex; align-items: flex-end">
<div style="font-weight: 600; font-size: 0.9em">{{ t('previous_period_income') }}</div>
<div style="font-weight: 700">${{ TeamOverview?.previousPeriodIncome || 0 }}</div>
</div>
</div>
<!-- BDLeader团队收入信息 -->
<div
style="
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 16px;
border: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
gap: 10px;
"
>
<!-- 团队信息 -->
<div style="font-weight: 700">
{{ t('bd_leaders_label') }} {{ TeamOverview?.memberCount || 0 }}
</div>
<div style="font-size: 0.9em; font-weight: 600">
{{ t('team_total_income_label') }} ${{ TeamOverview?.teamTotalIncome || 0 }}
</div>
<!-- BDLeader成员列表 -->
<div
v-for="BDLeaderInfo in TeamOverview?.members"
style="
position: relative;
background-color: white;
padding: 14px 10px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-around;
gap: 4px;
"
>
<!-- 头像 -->
<div
style="
width: 15%;
align-self: stretch;
display: flex;
align-items: center;
justify-content: center;
"
>
<img
:src="BDLeaderInfo.avatar || ''"
alt=""
style="
display: block;
width: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
object-fit: cover;
"
@error="handleAvatarImageError"
/>
</div>
<!-- 基本信息 -->
<div
style="
flex: 1;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: space-around;
gap: 4px;
"
>
<div
style="
font-weight: 700;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ BDLeaderInfo.userName || '-' }}
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-size: 0.8em; font-weight: 500">
{{ t('user_id_prefix') }} {{ BDLeaderInfo.account || '-' }}
</div>
</div>
<!-- 收入信息 -->
<div
style="
width: auto;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: center;
"
>
<div style="font-size: 0.8em; font-weight: 500; text-align: end">
{{ t('number_of_agencies_personnel_label') }} {{ BDLeaderInfo?.subMemberCount || 0 }}
</div>
<div style="font-size: 0.8em; font-weight: 500; text-align: end">
{{ t('total_income') }} ${{ BDLeaderInfo?.totalIncome || 0 }}
</div>
</div>
<!-- 更多按钮 -->
<div class="moreBt" @click="incomeShow(BDLeaderInfo)">
{{ t('more') }}
</div>
</div>
</div>
</div>
<!-- 遮罩层 -->
<maskLayer :maskLayerShow="maskLayerShow" @click="closedPopup">
<div style="position: fixed; bottom: 0; width: 100%">
<!-- 某个BDLeader的收益 -->
<div
v-if="showIncome"
style="
background: white;
border-radius: 16px 16px 0 0;
max-height: 80vh;
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px 12px 30px;
"
class="overflow-y-auto"
@click.stop
>
<div style="display: flex; justify-content: center; align-items: center">
<div style="margin: 0; font-weight: 600; color: #333">
{{ t('bd_leader_id_label') }} {{ moreUserAccount }}
</div>
</div>
<!-- 总体信息 -->
<div
style="
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
border-radius: 8px;
padding: 12px;
margin: 0 4px;
"
>
<div>
<div style="display: grid; gap: 4px">
<div style="font-size: 0.9em; font-weight: 600">
{{ t('total_income') }} ${{ moreDetail?.totalIncome || 0 }}
</div>
<div style="font-size: 0.9em; font-weight: 600">
{{ t('previous_period_income') }}: ${{ moreDetail?.previousPeriodIncome || 0 }}
</div>
</div>
</div>
</div>
<div style="font-weight: 600">{{ t('income_details_label') }}</div>
<!-- 历史薪资列表 -->
<div style="overflow-y: auto" class="salaryList">
<div
style="display: flex; flex-direction: column; gap: 8px"
v-for="incomeInfo in moreDetail?.incomeDetails"
>
<!-- 进行中 -->
<historySalary
v-if="incomeInfo.status === 'In Progress'"
type="In Progress"
@showMore="historyShow(incomeInfo)"
>
<template v-slot:content>
<div>{{ incomeInfo.settlementPeriod }}</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px">
<div>{{ t('agency_label') }} {{ incomeInfo.memberCount }}</div>
<!-- 团队收入 -->
<div>{{ t('team_total_income_text') }} ${{ incomeInfo.teamTotalIncome }}</div>
</div>
</template>
</historySalary>
<!-- 已完成 -->
<historySalary
v-if="incomeInfo.status === 'Completed'"
type="Completed"
@showMore="historyShow(incomeInfo)"
>
<template v-slot:content>
<div>{{ incomeInfo.settlementPeriod }}</div>
<div>{{ t('agency_label') }} {{ incomeInfo.memberCount }}</div>
<!-- 团队收入 -->
<div>
{{ t('team_total_income_text_capitalized') }}${{ incomeInfo.teamTotalIncome }}
</div>
</template>
</historySalary>
</div>
</div>
</div>
<!-- 某个时期的BD收益详情 -->
<div
v-if="showHistory"
style="
background: white;
border-radius: 16px 16px 0 0;
max-height: 60vh;
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px 12px 30px;
"
class="overflow-y-auto"
@click.stop
>
<div style="font-weight: 600">{{ incomeDetailsItem.settlementPeriod }}</div>
<div style="display: flex; justify-content: space-between">
<div style="font-size: 0.8em; font-weight: 600">
{{ t('agency_label') }} {{ incomeDetailsItem.memberCount }}
</div>
<div style="font-size: 0.8em; font-weight: 600">
{{ t('team_total_income_text_capitalized') }} ${{ incomeDetailsItem.teamTotalIncome }}
</div>
</div>
<!-- BD列表 -->
<div
v-for="member in incomeDetailsItem.members"
style="
background-color: white;
padding: 14px 10px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
"
>
<!-- 头像 -->
<div
style="
width: 15%;
align-self: stretch;
display: flex;
align-items: center;
justify-content: center;
"
>
<img
:src="member.avatar || ''"
alt=""
style="
display: block;
width: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
object-fit: cover;
"
@error="handleAvatarImageError"
/>
</div>
<!-- 基本信息 -->
<div
style="
flex: 1;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: space-around;
gap: 4px;
"
>
<div
style="
font-weight: 700;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ member.userName }}
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-size: 0.8em; font-weight: 500">
{{ t('user_id_prefix') }} {{ member.account }}
</div>
</div>
<!-- 收入信息 -->
<div
style="
width: auto;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: space-around;
"
>
<div style="font-size: 0.8em; font-weight: 500">{{ t('agency_team_income') }}</div>
<div style="font-size: 0.8em; font-weight: 500">${{ member.income }}</div>
</div>
</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 { handleAvatarImageError } from '@/utils/image/imageHandler.js'
import {
apiGetTeamOverview, //BD
apiGetTeamMemberDetail, // BDMore
} from '@/api/admin.js'
import GeneralHeader from '@/components/GeneralHeader.vue'
import maskLayer from '@/components/MaskLayer.vue'
import historySalary from './components/historySalary.vue'
const { t, locale } = useI18n()
//
locale.value && setDocumentDirection(locale.value)
const TeamOverview = ref(null) //
const moreUserAccount = ref('') //id
const moreDetail = ref(null) //
const incomeDetailsItem = ref(null) //
const showIncome = ref(false)
const showHistory = ref(false)
//
const maskLayerShow = computed(() => showIncome.value || showHistory.value)
const incomeShow = (BDLeaderInfo) => {
console.log('点击了收入按钮')
console.log('BDLeaderInfo.userId:', BDLeaderInfo.userId)
moreUserAccount.value = BDLeaderInfo.account
getTeamMemberDetail(BDLeaderInfo.userId)
showIncome.value = true
}
const historyShow = (incomeInfo) => {
console.log('点击了历史薪资按钮')
console.log('incomeInfo:', incomeInfo)
incomeDetailsItem.value = incomeInfo
showIncome.value = false
showHistory.value = true
}
//
const closedPopup = () => {
showIncome.value = false
showHistory.value = false
moreUserAccount.value = ''
moreDetail.value = null
incomeDetailsItem.value = null
}
//
const getTeamOverview = async () => {
let data = {
type: 'BD_LEADER',
}
const resInvitedList = await apiGetTeamOverview(data)
console.log('任务列表:', resInvitedList)
if (resInvitedList.status && resInvitedList.body) {
TeamOverview.value = resInvitedList.body
}
}
// BDMore
const getTeamMemberDetail = async (userId) => {
let data = {
targetId: userId,
type: 'BD',
}
const resInvitedList = await apiGetTeamMemberDetail(data)
console.log('任务列表:', resInvitedList)
if (resInvitedList.status && resInvitedList.body) {
moreDetail.value = resInvitedList.body
}
}
//
const init = () => {
getTeamOverview()
}
onMounted(() => {
init()
})
</script>
<style lang="scss" scoped>
* {
color: rgba(0, 0, 0, 0.8);
}
.fullPage {
max-height: 100vh;
overflow-y: auto;
background-color: #ffffff;
background-image: url(../../assets/images/Azizi/secondBg.png);
}
.fullPage::-webkit-scrollbar {
display: none;
}
.overflow-y-auto {
overflow-y: auto;
}
.overflow-y-auto::-webkit-scrollbar {
display: none;
}
.salaryList::-webkit-scrollbar {
display: none;
}
.moreBt {
font-size: 0.8em;
position: absolute;
bottom: 4%;
right: 4%;
color: #bb92ff;
font-weight: 500;
}
@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;
}
}
@media screen and (min-width: 1024px) {
* {
font-size: 32px;
}
}
[dir='rtl'] .moreBt {
left: 4%;
right: auto;
}
</style>

View File

@ -0,0 +1,519 @@
<template>
<div class="fullPage gradient-background-circles">
<!-- 顶部导航 -->
<GeneralHeader
:showLanguageList="true"
:title="t('my_bd_teams')"
color="black"
style="box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); z-index: 999"
/>
<!-- 主要内容 -->
<div
style="
padding: 16px;
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
gap: 12px;
"
>
<!-- 总收入信息 -->
<div
style="
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 16px;
border: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
gap: 10px;
"
>
<div style="display: flex; align-items: flex-end">
<div style="font-weight: 600; font-size: 0.9em">{{ t('total_income') }}</div>
<div style="font-weight: 700">${{ TeamOverview?.totalIncome || 0 }}</div>
</div>
<div style="display: flex; align-items: flex-end">
<div style="font-weight: 600; font-size: 0.9em">{{ t('previous_period_income') }}</div>
<div style="font-weight: 700">${{ TeamOverview?.previousPeriodIncome || 0 }}</div>
</div>
</div>
<!-- BD团队收入信息 -->
<div
style="
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 16px;
border: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
gap: 10px;
"
>
<!-- 团队信息 -->
<div style="font-weight: 700">{{ t('bds_label') }}{{ TeamOverview?.memberCount || 0 }}</div>
<div style="font-size: 0.9em; font-weight: 600">
{{ t('team_total_income_label') }} ${{ TeamOverview?.teamTotalIncome || 0 }}
</div>
<!-- BD成员列表 -->
<div
v-for="BDInfo in TeamOverview?.members"
style="
position: relative;
background-color: white;
padding: 14px 10px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-around;
gap: 4px;
"
>
<!-- 头像 -->
<div
style="
width: 15%;
align-self: stretch;
display: flex;
align-items: center;
justify-content: center;
"
>
<img
:src="BDInfo.avatar || ''"
alt=""
style="
display: block;
width: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
object-fit: cover;
"
@error="handleAvatarImageError"
/>
</div>
<!-- 基本信息 -->
<div
style="
flex: 1;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: space-around;
gap: 4px;
"
>
<div
style="
font-weight: 700;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ BDInfo.userName || '-' }}
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-size: 0.8em; font-weight: 500">
{{ t('user_id_prefix') }} {{ BDInfo.account || '-' }}
</div>
</div>
<!-- 收入信息 -->
<div
style="
width: auto;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: center;
"
>
<div style="font-size: 0.8em; font-weight: 500; text-align: end">
{{ t('number_of_agencies_personnel_label') }} {{ BDInfo?.subMemberCount || 0 }}
</div>
<div style="font-size: 0.8em; font-weight: 500; text-align: end">
{{ t('total_income') }} ${{ BDInfo?.totalIncome || 0 }}
</div>
</div>
<!-- 更多按钮 -->
<div class="moreBt" @click="incomeShow(BDInfo)">
{{ t('more') }}
</div>
</div>
</div>
</div>
<!-- 遮罩层 -->
<maskLayer :maskLayerShow="maskLayerShow" @click="closedPopup">
<div style="position: fixed; bottom: 0; width: 100%">
<!-- 某个BD的收益 -->
<div
v-if="showIncome"
style="
background: white;
border-radius: 16px 16px 0 0;
max-height: 80vh;
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px 12px 30px;
"
class="overflow-y-auto"
@click.stop
>
<div style="display: flex; justify-content: center; align-items: center">
<div style="margin: 0; font-weight: 600; color: #333">
{{ t('bd_id_label') }} {{ moreUserAccount }}
</div>
</div>
<!-- 总体信息 -->
<div
style="
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
border-radius: 8px;
padding: 12px;
margin: 0 4px;
"
>
<div>
<div style="display: grid; gap: 4px">
<div style="font-size: 0.9em; font-weight: 600">
{{ t('total_income') }} ${{ moreDetail?.totalIncome || 0 }}
</div>
<div style="font-size: 0.9em; font-weight: 600">
{{ t('previous_period_income') }}: ${{ moreDetail?.previousPeriodIncome || 0 }}
</div>
</div>
</div>
</div>
<div style="font-weight: 600">{{ t('income_details_label') }}</div>
<!-- 历史薪资列表 -->
<div style="overflow-y: auto" class="salaryList">
<div
style="display: flex; flex-direction: column; gap: 8px"
v-for="incomeInfo in moreDetail?.incomeDetails"
>
<!-- 进行中 -->
<historySalary
v-if="incomeInfo.status === 'In Progress'"
type="In Progress"
@showMore="historyShow(incomeInfo)"
>
<template v-slot:content>
<div>{{ incomeInfo.settlementPeriod }}</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px">
<div>{{ t('agency_label') }} {{ incomeInfo.memberCount }}</div>
<!-- 团队收入 -->
<div>{{ t('team_total_income_text') }} ${{ incomeInfo.teamTotalIncome }}</div>
</div>
</template>
</historySalary>
<!-- 已完成 -->
<historySalary
v-if="incomeInfo.status === 'Completed'"
type="Completed"
@showMore="historyShow(incomeInfo)"
>
<template v-slot:content>
<div>{{ incomeInfo.settlementPeriod }}</div>
<div>{{ t('agency_label') }} {{ incomeInfo.memberCount }}</div>
<!-- 团队收入 -->
<div>{{ t('team_total_income_text') }} ${{ incomeInfo.teamTotalIncome }}</div>
</template>
</historySalary>
</div>
</div>
</div>
<!-- 某个时期的Agency收益详情 -->
<div
v-if="showHistory"
style="
background: white;
border-radius: 16px 16px 0 0;
max-height: 60vh;
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px 12px 30px;
"
class="overflow-y-auto"
@click.stop
>
<div style="font-weight: 600">{{ incomeDetailsItem.settlementPeriod }}</div>
<div style="display: flex; justify-content: space-between">
<div style="font-size: 0.8em; font-weight: 600">
{{ t('agency_label') }} {{ incomeDetailsItem.memberCount }}
</div>
<div style="font-size: 0.8em; font-weight: 600">
{{ t('team_total_income_text_capitalized') }} ${{ incomeDetailsItem.teamTotalIncome }}
</div>
</div>
<!-- BD列表 -->
<div
v-for="member in incomeDetailsItem.members"
style="
background-color: white;
padding: 14px 10px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
"
>
<!-- 头像 -->
<div
style="
width: 15%;
align-self: stretch;
display: flex;
align-items: center;
justify-content: center;
"
>
<img
:src="member.avatar || ''"
alt=""
style="
display: block;
width: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
object-fit: cover;
"
@error="handleAvatarImageError"
/>
</div>
<!-- 基本信息 -->
<div
style="
flex: 1;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: space-around;
gap: 4px;
"
>
<div
style="
font-weight: 700;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ member.userName }}
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-size: 0.8em; font-weight: 500">
{{ t('user_id_prefix') }} {{ member.account }}
</div>
</div>
<!-- 收入信息 -->
<div
style="
width: auto;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: space-around;
"
>
<div style="font-size: 0.8em; font-weight: 500">{{ t('agency_team_income') }}</div>
<div style="font-size: 0.8em; font-weight: 500">${{ member.income }}</div>
</div>
</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 { handleAvatarImageError } from '@/utils/image/imageHandler.js'
import {
apiGetTeamOverview, //BD
apiGetTeamMemberDetail, // BDMore
} from '@/api/admin.js'
import GeneralHeader from '@/components/GeneralHeader.vue'
import maskLayer from '@/components/MaskLayer.vue'
import historySalary from './components/historySalary.vue'
const { t, locale } = useI18n()
//
locale.value && setDocumentDirection(locale.value)
const TeamOverview = ref(null) //
const moreUserAccount = ref('') //id
const moreDetail = ref(null) //
const incomeDetailsItem = ref(null) //
const showIncome = ref(false)
const showHistory = ref(false)
//
const maskLayerShow = computed(() => showIncome.value || showHistory.value)
const incomeShow = (BDInfo) => {
console.log('点击了收入按钮')
console.log('BDInfo.userId:', BDInfo.userId)
moreUserAccount.value = BDInfo.account
getTeamMemberDetail(BDInfo.userId)
showIncome.value = true
}
const historyShow = (incomeInfo) => {
console.log('点击了历史薪资按钮')
console.log('incomeInfo:', incomeInfo)
incomeDetailsItem.value = incomeInfo
showIncome.value = false
showHistory.value = true
}
//
const closedPopup = () => {
showIncome.value = false
showHistory.value = false
moreUserAccount.value = ''
moreDetail.value = null
incomeDetailsItem.value = null
}
//
const getTeamOverview = async () => {
let data = {
type: 'BD',
}
const resInvitedList = await apiGetTeamOverview(data)
console.log('任务列表:', resInvitedList)
if (resInvitedList.status && resInvitedList.body) {
TeamOverview.value = resInvitedList.body
}
}
// BDMore
const getTeamMemberDetail = async (userId) => {
let data = {
targetId: userId,
type: 'BD',
}
const resInvitedList = await apiGetTeamMemberDetail(data)
console.log('任务列表:', resInvitedList)
if (resInvitedList.status && resInvitedList.body) {
moreDetail.value = resInvitedList.body
}
}
//
const init = () => {
getTeamOverview()
}
onMounted(() => {
init()
})
</script>
<style lang="scss" scoped>
* {
color: rgba(0, 0, 0, 0.8);
}
.fullPage {
max-height: 100vh;
overflow-y: auto;
background-color: #ffffff;
background-image: url(../../assets/images/Azizi/secondBg.png);
}
.fullPage::-webkit-scrollbar {
display: none;
}
.overflow-y-auto {
overflow-y: auto;
}
.overflow-y-auto::-webkit-scrollbar {
display: none;
}
.salaryList::-webkit-scrollbar {
display: none;
}
.moreBt {
font-size: 0.8em;
position: absolute;
bottom: 4%;
right: 4%;
color: #bb92ff;
font-weight: 500;
}
[dir='rtl'] .moreBt {
left: 4%;
right: auto;
}
@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;
}
}
@media screen and (min-width: 1024px) {
* {
font-size: 32px;
}
}
</style>

View File

@ -0,0 +1,320 @@
<template>
<div class="fullPage">
<div class="bg">
<!-- 顶部导航 -->
<GeneralHeader
:showLanguageList="true"
:title="t('my_linked_recharge_agent')"
color="black"
style="box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); z-index: 999"
/>
<!-- 主要内容 -->
<div
style="
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
"
>
<!-- 这个月总充值 -->
<div
style="
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 12px;
border: 1px solid #e5e7eb;
"
>
<div style="font: 0.8em; font-weight: 500; color: rgba(0, 0, 0, 0.4)">
{{ t('my_total_income') }}
</div>
<div style="font: 0.9em; font-weight: 600; color: rgba(0, 0, 0, 0.8)">
${{ totalRechargeDetail?.totalIncome || 0 }}
</div>
<div style="font: 0.8em; font-weight: 500; color: rgba(0, 0, 0, 0.4)">
{{ t('this_period_recharge') }}
</div>
<div style="font: 0.9em; font-weight: 600; color: rgba(0, 0, 0, 0.8)">
${{ totalRechargeDetail?.currentMonthRecharge || 0 }}
</div>
</div>
<div style="font-weight: 700">{{ t('recharge_agency_list') }}</div>
<div
v-for="(agency, aIndex) in agencyList"
:key="aIndex"
style="
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 12px;
border: 1px solid #e5e7eb;
margin-bottom: 10px;
transition: all 0.3s ease-out;
"
@click="showAgencyInfo(aIndex)"
>
<div style="width: 100%; display: flex; justify-content: space-between">
<!-- 基本信息 -->
<div
style="
flex: 1;
min-width: 0;
align-self: stretch;
display: flex;
align-items: center;
gap: 4px;
"
>
<img
:src="agency.userAvatar"
alt=""
style="display: block; object-fit: cover; width: 3em; border-radius: 50%"
/>
<div
style="
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: space-around;
gap: 4px;
"
>
<div
style="
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ agency.userNickname }}
</div>
<div style="font-weight: 700; color: rgba(0, 0, 0, 0.4)">
{{ t('user_id_prefix') }}{{ agency.account }}
</div>
</div>
</div>
<!-- 展开按钮 -->
<div
style="
width: auto;
min-width: 0;
align-self: stretch;
display: flex;
align-items: center;
"
>
<img
src="../../assets/icon/Azizi/arrow.png"
alt=""
style="
width: 24px;
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
"
:class="{ rotated: aIndex == selectedAgencyIndex }"
/>
</div>
</div>
<!-- 展示信息 -->
<transition name="info-fade">
<div style="display: flex" v-show="aIndex == selectedAgencyIndex">
<div style="flex: 1">
<div style="font-weight: 500; color: rgba(0, 0, 0, 0.4)">
{{ t('this_month') }}:
</div>
<div style="font-weight: 600; color: rgba(0, 0, 0, 0.8)">
${{ formatPrice(agency.thisMonth) || 0 }}
</div>
</div>
<div style="flex: 1">
<div style="font-weight: 500; color: rgba(0, 0, 0, 0.4)">
{{ t('last_month') }}:
</div>
<div style="font-weight: 600; color: rgba(0, 0, 0, 0.8)">
${{ formatPrice(agency.lastMonth) || 0 }}
</div>
</div>
</div>
</transition>
</div>
<!-- 触发加载功能 -->
<div ref="Agloadmore" v-if="showAgLoading"></div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useThrottle } from '@/utils/useDebounce'
import { getAgencyList, getAgencyTotalRecharge } from '@/api/userInfo'
import GeneralHeader from '@/components/GeneralHeader.vue'
const router = useRouter()
const { t, locale } = useI18n()
const selectedAgencyIndex = ref(-1) //agency
const showAgencyInfo = (index) => {
selectedAgencyIndex.value = selectedAgencyIndex.value != index ? index : -1
} //agency
const send = async () => {
router.push({ path: '/item-distribution' })
}
const totalRechargeDetail = ref({}) //
const agencyList = ref([]) //
const agencyCurrent = ref(1) //
const showAgLoading = ref(true) //
onMounted(async () => {
getAgencise() //
getAgencyMonthsRecharge() //
//
if (Agloadmore.value) {
observer.observe(Agloadmore.value)
}
})
//
const getAgencyMonthsRecharge = async () => {
const resTotalRecharge = await getAgencyTotalRecharge()
console.log('resTotalRecharge:', resTotalRecharge)
if (resTotalRecharge.status && resTotalRecharge.body) {
totalRechargeDetail.value = resTotalRecharge.body
}
}
//
const getAgencise = async () => {
let data = {
cursor: agencyCurrent.value,
limit: 20,
}
const resagencyList = await getAgencyList(data)
console.log('resagencyList:', resagencyList)
if (resagencyList.status && resagencyList.body) {
agencyList.value.push(...resagencyList.body.records)
if (resagencyList.body.total <= resagencyList.body.current * resagencyList.body.size) {
showAgLoading.value = false
}
}
}
const Agloadmore = ref(null)
const debouceGetAgList = useThrottle(getAgencise, 1000)
// IntersectionObserver
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.intersectionRatio > 0) {
console.log('监控')
if (entry.target == Agloadmore.value && agencyList.value.length != 0) {
agencyCurrent.value++
debouceGetAgList()
}
}
})
})
const formatPrice = (value) => {
if (value && value != 0) {
return Number(value).toFixed(2)
}
return 0
}
</script>
<style scoped>
* {
color: black;
}
.fullPage {
background-color: #ffffff;
min-height: 100vh;
position: relative;
}
.bg {
width: 100vw;
min-height: 100vh;
background-image: url(../../assets/images/Azizi/secondBg.png);
background-size: 100% auto;
background-repeat: no-repeat;
}
.tabName {
color: #00000066;
}
.tabName-active {
color: black;
font-weight: 500;
}
.info-fade-enter-from,
.info-fade-leave-to {
transform: translateY(-10px);
opacity: 0;
}
.info-fade-enter-active {
transition: all 0.5s ease-out;
}
.info-fade-leave-active {
transition: all 0.5s cubic-bezier(1, 0.5, 0.8, 1);
}
.rotated {
transform: rotate(90deg);
}
@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;
}
}
@media screen and (min-width: 1024px) {
* {
font-size: 32px;
}
}
</style>

View File

@ -0,0 +1,172 @@
<template>
<div style="position: relative; width: 100%">
<img :src="bgUrl" alt="" width="100%" style="display: block" class="flipImg" />
<div class="status">
<div
style="width: 50%; height: 50%; display: flex; justify-content: center; align-items: center"
>
<div style="color: #fff; font-weight: 600" :style="{ fontSize }">
{{ getTypeText(type) }}
</div>
</div>
</div>
<div style="position: absolute; inset: 0; padding: 3% 4%">
<div
style="height: 100%; display: flex; flex-direction: column; justify-content: space-around"
>
<slot name="content"></slot>
</div>
<div class="moreBt" @click="btMore">{{ t('more') }}</div>
</div>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { setDocumentDirection } from '@/locales/i18n'
const { t, locale } = useI18n()
//
locale.value && setDocumentDirection(locale.value)
const progress = new URL('@/assets/images/Azizi/BDCenter/progress.png', import.meta.url).href
const completed = new URL('@/assets/images/Azizi/BDCenter/completed.png', import.meta.url).href
const progressLong = new URL('@/assets/images/Azizi/BDCenter/progressLong.png', import.meta.url)
.href
const props = defineProps({
type: {
type: String,
default: '',
},
long: {
type: Boolean,
default: false,
},
date: {
type: String,
default: '',
},
})
const bgUrl = computed(() => {
if (props.type == 'In Progress' && props.long) {
return progressLong
} else if (props.type == 'In Progress') {
return progress
} else if (props.type == 'Completed') {
return completed
}
})
const emit = defineEmits(['showMore'])
const btMore = () => {
emit('showMore')
}
const backgroundImage = ref('')
const fontSize = ref('')
watch(
() => props.type,
(newType) => {
if (newType == 'In Progress') {
fontSize.value = '0.9em'
}
if (newType == 'Completed') {
fontSize.value = '1em'
}
return
},
{ immediate: true },
)
//
const getTypeText = (type) => {
console.log('type:', type)
switch (type) {
case 'In Progress':
return t('in_progress')
case 'Completed':
return t('completed')
case 'Out of account':
return t('out_of_account')
case 'Pending':
return t('pending')
default:
return type
}
}
</script>
<style lang="scss" scoped>
* {
color: rgba(0, 0, 0, 0.8);
font-weight: 700;
}
.contentTime {
font-size: 1.1em;
}
.contentText {
font-size: 1em;
}
.moreBt {
font-size: 0.8em;
position: absolute;
bottom: 4%;
right: 4%;
color: #bb92ff;
font-weight: 500;
}
.status {
width: 48%;
height: 40%;
position: absolute;
top: 0;
right: 3%;
display: flex;
justify-content: flex-end;
align-items: center;
}
[dir='rtl'] .status {
right: auto;
left: 3%;
}
[dir='rtl'] .flipImg {
transform: scaleX(-1);
}
[dir='rtl'] .moreBt {
right: auto;
left: 4%;
}
@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;
}
}
</style>

View File

@ -0,0 +1,609 @@
<template>
<div class="fullPage">
<div class="bg gradient-background-circles">
<!-- 顶部导航 -->
<GeneralHeader
:isHomePage="true"
:showLanguageList="true"
:title="t('admin_center')"
color="black"
style="box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); z-index: 999"
/>
<!-- 主要内容 -->
<div
style="
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
"
>
<!-- 用户信息卡片 -->
<div
style="
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 16px;
border: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
gap: 10px;
"
>
<!-- 标题 -->
<div style="font-weight: 600">{{ t('my_leader') }}</div>
<!-- 用户信息 -->
<div style="display: flex; align-items: center; gap: 4px">
<!-- 头像 -->
<div
style="
width: 3.5em;
align-self: stretch;
display: flex;
align-items: center;
justify-content: center;
"
>
<img
:src="userInfo.userAvatar || ''"
style="
display: block;
width: 100%;
aspect-ratio: 1/1;
object-fit: cover;
border-radius: 50%;
"
@error="handleAvatarImageError"
/>
</div>
<!-- 基本信息 -->
<div
style="
flex: 1;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: space-around;
gap: 4px;
"
>
<div
style="
font-weight: 700;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ userInfo.userNickname || '-' }}
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-size: 0.8em; font-weight: 500">
{{ t('user_id_prefix') }} {{ userInfo.account || '-' }}
</div>
</div>
</div>
</div>
<!-- 角色模块 -->
<div
style="
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 16px;
border: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
gap: 4px;
"
>
<!-- 标题 -->
<div style="font-weight: 600; color: #1f2937">{{ t('my_role') }}</div>
<!-- 角色身份 -->
<div style="display: flex; justify-content: space-between; align-items: center">
<div style="font-size: 1.1em; font-weight: 600">{{ t('admin') }}</div>
<img
src="../../assets/icon/Azizi/detail.png"
alt=""
style="display: block; width: 1.5em"
class="flipImg"
@click="gotoPolicy"
/>
</div>
<!-- 分割线 -->
<div style="border-bottom: 1px solid #e6e6e6"></div>
<!-- 已完成任务 -->
<div style="display: flex; justify-content: space-between; align-items: center">
<div style="font-weight: 600">{{ t('task_completion_status') }}</div>
<div style="font-weight: 600">({{ taskData.completedTaskCount || 0 }})</div>
</div>
<div v-if="taskData.tasks" style="display: flex; flex-direction: column; gap: 4px">
<div style="font-weight: 500">
{{ taskData.cycleStartDate }}-{{ taskData.cycleEndDate }}
</div>
<!-- 任务列表与完成状态 -->
<div
v-for="task in taskData.tasks"
style="display: flex; justify-content: space-between; align-items: center"
>
<div style="color: rgba(0, 0, 0, 0.4); font-size: 0.8em; font-weight: 500">
{{ taskName(task) }}
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-size: 0.8em; font-weight: 500">
{{ taskCompletedText(task) }}
</div>
</div>
</div>
</div>
<!-- 本期团队收入 -->
<div
style="
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 16px;
border: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
gap: 4px;
"
>
<!-- 总收入 -->
<div style="display: flex; justify-content: space-between; align-items: center">
<!-- 基本信息 -->
<div style="display: flex; flex-direction: column; gap: 4px">
<div style="font-weight: 600">
{{ t('team_earnings_this_period') }} ${{ teamSummary.teamEarningsThisPeriod || 0 }}
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-size: 0.8em; font-weight: 500">
{{ t('available_salaries') }} ${{ availableIncome?.availableBalance || 0 }}
</div>
</div>
<!-- 跳转按钮 -->
<img
src="../../assets/icon/Azizi/arrowBlack.png"
alt=""
style="display: block; width: 2em"
class="flipImg"
@click="gotoIncome"
/>
</div>
<!-- 分割线 -->
<div style="border-bottom: 1px solid #e6e6e6"></div>
<!-- 我的BD Leader团队 -->
<div style="display: flex; justify-content: space-between; align-items: center">
<!-- 基本信息 -->
<div style="display: flex; flex-direction: column; gap: 4px">
<div style="font-weight: 600">
{{ t('my_bd_leader_teams') }}({{ teamSummary.bdLeaderTeams?.count || 0 }})
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-size: 0.8em; font-weight: 500">
{{ t('total_income') }} ${{ teamSummary.bdLeaderTeams?.totalIncome || 0 }}
</div>
</div>
<!-- 跳转按钮 -->
<div style="align-self: stretch; display: flex; align-items: center; gap: 4px">
<img
src="../../assets/icon/Azizi/addUser.png"
alt=""
style="display: block; width: 1.6em"
class="flipImg"
@click="gotoPage('/invite-bd-leader')"
/>
<img
src="../../assets/icon/Azizi/list.png"
alt=""
style="display: block; width: 1.6em"
class="flipImg"
@click="gotoPage('/my-BDLeader-teams')"
/>
</div>
</div>
<!-- 分割线 -->
<div style="border-bottom: 1px solid #e6e6e6"></div>
<!-- 我的BD团队 -->
<div style="display: flex; justify-content: space-between; align-items: center">
<!-- 基本信息 -->
<div style="display: flex; flex-direction: column; gap: 4px">
<div style="font-weight: 600">
{{ t('my_bd_teams') }}({{ teamSummary.bdTeams?.count || 0 }})
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-size: 0.8em; font-weight: 500">
{{ t('total_income') }} ${{ teamSummary.bdTeams?.totalIncome || 0 }}
</div>
</div>
<!-- 跳转按钮 -->
<div style="align-self: stretch; display: flex; align-items: center; gap: 4px">
<img
src="../../assets/icon/Azizi/addUser.png"
alt=""
style="display: block; width: 1.6em"
class="flipImg"
@click="gotoPage('/invite-bd')"
/>
<img
src="../../assets/icon/Azizi/list.png"
alt=""
style="display: block; width: 1.6em"
class="flipImg"
@click="gotoPage('/my-BD-teams')"
/>
</div>
</div>
<!-- 分割线 -->
<div style="border-bottom: 1px solid #e6e6e6"></div>
<!-- 我的Agency团队 -->
<div style="display: flex; justify-content: space-between; align-items: center">
<!-- 基本信息 -->
<div style="display: flex; flex-direction: column; gap: 4px">
<div style="font-weight: 600">
{{ t('my_agency_teams') }}({{ teamSummary.agencyTeams?.count || 0 }})
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-size: 0.8em; font-weight: 500">
{{ t('total_income') }} ${{ teamSummary.agencyTeams?.totalIncome || 0 }}
</div>
</div>
<!-- 跳转按钮 -->
<div style="align-self: stretch; display: flex; align-items: center; gap: 4px">
<img
src="../../assets/icon/Azizi/addUser.png"
alt=""
style="display: block; width: 1.6em"
class="flipImg"
@click="gotoPage('/invite-agency')"
/>
<img
src="../../assets/icon/Azizi/list.png"
alt=""
style="display: block; width: 1.6em"
class="flipImg"
@click="gotoPage('/my-agency-teams')"
/>
</div>
</div>
<!-- 分割线 -->
<div style="border-bottom: 1px solid #e6e6e6"></div>
<!-- 我的Recharge Agency团队 -->
<div style="display: flex; justify-content: space-between; align-items: center">
<!-- 基本信息 -->
<div style="display: flex; flex-direction: column; gap: 4px">
<div style="font-weight: 600">
{{ t('number_of_recharge_agents_invited') }}({{
teamSummary.rechargeTeams?.count || 0
}})
</div>
<div style="color: rgba(0, 0, 0, 0.4); font-size: 0.8em; font-weight: 500">
{{ t('total_recharge') }}: ${{ teamSummary.rechargeTeams?.totalIncome || 0 }}
</div>
</div>
<!-- 跳转按钮 -->
<div style="align-self: stretch; display: flex; align-items: center; gap: 4px">
<img
src="../../assets/icon/Azizi/addUser.png"
alt=""
style="display: block; width: 1.6em"
class="flipImg"
@click="gotoPage('/invite-recharge-agency')"
/>
<img
src="../../assets/icon/Azizi/list.png"
alt=""
style="display: block; width: 1.6em"
class="flipImg"
@click="gotoPage('/my-recharge-agency')"
/>
</div>
</div>
<!-- 分割线 -->
<div style="border-bottom: 1px solid #e6e6e6"></div>
</div>
<!-- 赠送礼物 -->
<div
style="
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 16px;
border: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
gap: 4px;
"
@click="send"
>
<div style="font-weight: 600">{{ t('send_welcome_gift') }}</div>
<img
src="../../assets/icon/Azizi/arrowBlack.png"
alt=""
style="display: block; width: 1.5em"
class="flipImg"
/>
</div>
<!-- 前往用户编辑 -->
<div
v-if="teamPermissionCheck.hasUserEditingPermission"
style="
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 16px;
border: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
gap: 4px;
"
@click="gotoAppPage('editingUser')"
>
<div style="font-weight: 600">{{ t('user_editing') }}</div>
<img
src="../../assets/icon/Azizi/arrowBlack.png"
alt=""
style="display: block; width: 1.5em"
class="flipImg"
/>
</div>
<!-- 前往房间编辑 -->
<div
v-if="teamPermissionCheck.hasRoomEditingPermission"
style="
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 16px;
border: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
gap: 4px;
"
@click="gotoAppPage('editingRoom')"
>
<div style="font-weight: 600">{{ t('room_editing') }}</div>
<img
src="../../assets/icon/Azizi/arrowBlack.png"
alt=""
style="display: block; width: 1.5em"
class="flipImg"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { setDocumentDirection } from '@/locales/i18n'
import { gotoAppPage } from '@/utils/appBridge.js'
import { getUserId } from '@/utils/userStore.js'
import { usePageInitializationWithConfig } from '@/utils/pageConfig.js'
import { handleAvatarImageError } from '@/utils/image/imageHandler.js'
import { useIdentityStore } from '@/stores/identity.js'
import { showError } from '@/utils/toast.js'
import {
apiTaskList, //
apiGetTeamSummary, // BD
apiGetTeamPermissionCheck, //
} from '@/api/admin.js'
import {
apiGetBankBalance, //
} from '@/api/wallet.js'
import GeneralHeader from '@/components/GeneralHeader.vue'
const { t, locale } = useI18n()
const router = useRouter()
const identityStore = useIdentityStore()
//
locale.value && setDocumentDirection(locale.value)
const taskData = ref({}) //
const availableIncome = ref({})
const teamSummary = ref({}) //
const teamPermissionCheck = ref({}) //
//
const taskName = (task) => {
if (task.taskType == 'MIC_USAGE') {
return t('mic_usage_time')
} else if (task.taskType == 'WORKING_DAYS') {
return t('working_days_desc')
} else if (task.taskType == 'NEW_BD_LEADERS') {
return t('new_bd_leaders_bound')
} else if (task.taskType == 'NEW_BDS') {
return t('new_bds_bound')
} else if (task.taskType == 'NEW_AGENCIES') {
return t('new_agencies_bound')
}
}
//
const taskCompletedText = (task) => {
if (task.taskType == 'MIC_USAGE') {
return `(${task.completedCount}/${task.targetCount}h)`
} else {
return `(${task.completedCount}/${task.targetCount})`
}
}
const send = () => {
router.push({ path: '/item-distribution' })
}
//
const gotoIncome = () => {
router.push({ path: '/available-income' })
}
//
const gotoPolicy = () => {
router.push({ path: '/admin-policy' })
}
//
const gotoPage = (path) => {
router.push({ path: path })
}
//
const getTaskList = async () => {
let data = {
userId: getUserId(),
}
const resInvitedList = await apiTaskList(data)
console.log('任务列表:', resInvitedList)
if (resInvitedList.status && resInvitedList.body) {
taskData.value = resInvitedList.body
}
}
//
const fetchBankBalance = async () => {
try {
const response = await apiGetBankBalance('ADMIN_SALARY')
if (response.status && response.body) {
availableIncome.value = response.body || {}
}
} catch (error) {
console.error('Failed to fetch bank balance:', error)
showError(t('failed_to_load_balance'))
}
}
//
const getTeamSummary = async () => {
const resTeamSummary = await apiGetTeamSummary()
console.log('团队总览:', resTeamSummary)
if (resTeamSummary.status && resTeamSummary.body) {
teamSummary.value = resTeamSummary.body
}
}
//
const getTeamPermissionCheck = async () => {
const resTeamPermissionCheck = await apiGetTeamPermissionCheck()
console.log('权限列表:', resTeamPermissionCheck)
if (resTeamPermissionCheck.status && resTeamPermissionCheck.body) {
teamPermissionCheck.value = resTeamPermissionCheck.body
}
}
//
const init = () => {
getTaskList() //
fetchBankBalance() //
getTeamSummary() // BD
getTeamPermissionCheck() //
}
const { userInfo } = usePageInitializationWithConfig('ADMIN_CENTER', {
forceRefresh: true, //
needsTeamInfo: true,
needsBankBalance: false,
onDataLoaded: () => {},
})
onMounted(() => {
init()
identityStore.setIdentity('ADMIN')
})
</script>
<style scoped>
* {
color: rgba(0, 0, 0, 0.8);
}
.fullPage {
position: relative;
max-height: 100vh;
overflow-y: auto;
background-color: #ffffff;
}
.fullPage::-webkit-scrollbar {
display: none;
}
.bg {
width: 100vw;
min-height: 100vh;
background-image: url(../../assets/images/Azizi/secondBg.png);
background-size: 100% auto;
background-repeat: no-repeat;
}
@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;
}
}
[dir='rtl'] .flipImg {
transform: scaleX(-1);
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<div class="policy-container">
<!-- 顶部导航 -->
<GeneralHeader
:title="t('admin_policy')"
color="black"
style="box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); z-index: 999"
/>
<div v-if="userRegion">
<div v-if="userRegion.regionCode != 'AR'">
<img v-smart-img :src="imageUrl('NOT_AR_Policy')" alt="Policy Image" class="policy-image" />
</div>
<div v-else>
<img v-smart-img :src="imageUrl(getImgName('policy0'))" alt="" class="policy-image" />
<img v-smart-img :src="imageUrl(getImgName('policy1'))" alt="" class="policy-image" />
<img v-smart-img :src="imageUrl(getImgName('policy2'))" alt="" class="policy-image" />
<img v-smart-img :src="imageUrl(getImgName('policy3'))" alt="" class="policy-image" />
</div>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useLangStore } from '@/stores/lang'
import { getPngUrl } from '@/config/imagePaths.js'
import { preloadImages } from '@/utils/image/imagePreloader.js'
import { getUserId } from '@/utils/userStore.js'
import { getRegionByUserId } from '@/api/wallet.js'
import GeneralHeader from '@/components/GeneralHeader.vue'
// OSSURL
const imageUrl = (filename) => getPngUrl('AdminCenter/Policy/', filename)
//
const getImgName = (filename) => {
return currentLangType.value === 'ar' ? `${filename}_ar` : filename
}
const langStore = useLangStore()
const { t, locale } = useI18n()
//
const currentLangType = computed(() => {
return langStore.selectedLang?.type || 'en'
})
const userRegion = ref(null) //
//
const preloadCriticalImages = () => {
const criticalImages = [
imageUrl(getImgName('policy0')),
imageUrl(getImgName('policy1')),
imageUrl(getImgName('policy2')),
imageUrl(getImgName('policy3')),
]
preloadImages(criticalImages)
}
//
const apiGetRegionByUserId = async () => {
const response = await getRegionByUserId(getUserId())
if (response.status) {
userRegion.value = response.body
}
}
onMounted(() => {
apiGetRegionByUserId()
preloadCriticalImages() //
})
</script>
<style lang="scss" scoped>
.policy-container {
height: 100vh;
background-image: url(../../assets/images/Azizi/secondBg.png);
overflow-y: auto;
}
.policy-container::-webkit-scrollbar {
display: none;
}
.policy-image {
display: block;
width: 100vw;
object-fit: cover;
}
</style>

View File

@ -20,7 +20,8 @@
"build:azizi-prod": "vite build --mode production-azizi && node scripts/generate-version.js",
"preview": "vite preview",
"lint": "eslint . --fix",
"docs:sync:security": "node scripts/sync-security-docs.js"
"docs:sync:security": "node scripts/sync-security-docs.js",
"aslan:webview": "node scripts/aslan-webview-sim.mjs"
},
"dependencies": {
"@vueuse/core": "^13.9.0",

View File

@ -0,0 +1,176 @@
#!/usr/bin/env node
const LOGIN_URL =
'https://api.atuchat.com/e64f27b9ba6b37881120f4584a5444a5c684d8491b703d0af953e5cc6a5f4cec'
const account = process.env.ASLAN_ACCOUNT || '6666'
const password = process.env.ASLAN_PASSWORD || '123456'
const targetUrl =
process.env.ASLAN_URL || 'https://h5.atuchat.com/#/admin-center?lang=en'
const chromePath =
process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
const headless = process.env.ASLAN_HEADLESS === '1'
const exitAfterLoad = process.env.ASLAN_EXIT_AFTER_LOAD === '1'
async function loadPlaywright() {
try {
return await import('playwright')
} catch {
return await import(
'/Users/hy/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright/index.mjs'
)
}
}
function appHeaders(imei = 'aslan-webview-sim') {
return {
'req-lang': 'en',
'X-Forwarded-Proto': 'http',
'User-Agent': 'Dart/3.7.2 (dart:io)',
'req-imei': imei,
'req-app-intel':
'version=1.0.0;build=1;model=CodexWebView;sysVersion=1;channel=Google',
'req-client': 'Android',
'req-sys-origin': 'origin=ATYOU;originChild=ATYOU',
'Req-Atyou': 'true',
}
}
function publicUserProfile(profile = {}) {
return {
id: profile.id || profile.userId,
account: profile.account,
specialAccount: profile.ownSpecialId?.account,
userNickname: profile.userNickname,
countryName: profile.countryName,
originSys: profile.originSys || profile.sysOrigin,
sysOriginChild: profile.sysOriginChild,
}
}
async function login() {
const response = await fetch(LOGIN_URL, {
method: 'POST',
headers: {
...appHeaders('aslan-webview-sim-login'),
'content-type': 'application/json',
},
body: JSON.stringify({ account, pwd: password }),
})
const data = await response.json()
if (!response.ok || !data.status || !data.body?.token) {
const error = {
httpStatus: response.status,
status: data.status,
errorCode: data.errorCode,
errorMsg: data.errorMsg,
}
throw new Error(`Aslan login failed: ${JSON.stringify(error)}`)
}
return data.body
}
async function main() {
const auth = await login()
const profile = auth.userProfile || {}
const { chromium } = await loadPlaywright()
const browser = await chromium.launch({
headless,
executablePath: chromePath,
args: ['--no-sandbox'],
})
const context = await browser.newContext({
viewport: { width: 390, height: 844 },
isMobile: true,
hasTouch: true,
userAgent:
'Mozilla/5.0 (Linux; Android 12; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
})
await context.addInitScript(
({ token, profile }) => {
window.__aslanWebViewMessages = []
const postBridgeMessage = (message) => {
const text = String(message)
window.__aslanWebViewMessages.push(text)
console.info(`[AslanWebViewSim] FlutterPageControl.postMessage: ${text}`)
}
window.FlutterPageControl = {
postMessage: postBridgeMessage,
}
window.app = {
getAccessOrigin: () =>
JSON.stringify({
Authorization: `Bearer ${token}`,
'Req-Lang': 'en',
'Req-App-Intel':
'build=1;version=1.0.0;model=CodexWebView;channel=Google;Req-Imei=aslan-webview-sim',
'Req-Sys-Origin': 'origin=ATYOU;originChild=ATYOU',
}),
closePage: () => postBridgeMessage('close_page'),
back: () => history.back(),
gotoRoom: (roomId) => postBridgeMessage(`go_to_room:${roomId}`),
gotoPrivateChat: (userId) => postBridgeMessage(`private_chat:${userId}`),
openUserInfo: (userId) => postBridgeMessage(`view_user_info:${userId}`),
gotoAppPage: (pageName) => postBridgeMessage(pageName),
goToFlutterPage: (pageName) => postBridgeMessage(pageName),
uploadImgFile: () => postBridgeMessage('uploadImgFile'),
}
try {
localStorage.setItem('userProfile', JSON.stringify(profile))
} catch {}
},
{ token: auth.token, profile },
)
const page = await context.newPage()
page.on('console', (msg) => {
const text = msg.text()
if (
text.includes('[AslanWebViewSim]') ||
/error|warn|Route|Permission|APP|Failed|identity/i.test(text)
) {
console.log(`[browser:${msg.type()}] ${text}`)
}
})
page.on('pageerror', (error) => {
console.log(`[browser:pageerror] ${error.message}`)
})
console.log('Aslan WebView simulator started')
console.log(`account=${account}`)
console.log(`profile=${JSON.stringify(publicUserProfile(profile))}`)
console.log(`url=${targetUrl}`)
console.log('Close the Chrome window to stop the simulator.')
await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 45_000 })
if (exitAfterLoad) {
await page.waitForTimeout(6_000)
const state = await page.evaluate(() => ({
href: location.href,
text: (document.body?.innerText || '').replace(/\s+/g, ' ').slice(0, 800),
bridgeMessages: window.__aslanWebViewMessages || [],
}))
console.log(`loaded=${JSON.stringify(state)}`)
await browser.close()
return
}
await new Promise((resolve) => {
browser.on('disconnected', resolve)
})
}
main().catch((error) => {
console.error(error.message || error)
process.exit(1)
})

View File

@ -138,18 +138,10 @@ const fetchTeamPolicy = async () => {
}
}
//
// Target 使 K/M
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()
if (target === null || target === undefined || target === '') return '0'
return String(target)
}
//