727 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div
style="
background: white;
border-radius: 16px 16px 0 0;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 10px;
padding: 15px;
"
>
<!-- 标题 -->
<div style="padding: 0 5px; display: flex; justify-content: center; align-items: center">
<div style="margin: 0; font-size: 18px; font-weight: 600; color: #333">
{{ t('team_member') }}
</div>
</div>
<!-- 日期和类型 -->
<div style="padding: 0 5px; display: flex; justify-content: space-between; align-items: center">
<div style="color: rgba(0, 0, 0, 0.5); font-weight: 600; font-size: 1.3em">
{{ billData?.date }}
</div>
<div
style="margin: 4px; padding: 0 8px; border-radius: 4px; display: flex; align-items: center"
:style="{ boxShadow }"
>
<div style="font-weight: 500" :style="{ color }">
{{ getStatusText(billData?.status) }}
</div>
</div>
</div>
<!-- 搜索栏 -->
<div style="padding: 0 5px; display: flex; align-items: center; gap: 8px">
<!-- 搜索框 -->
<div
style="
flex: 1;
min-width: 0;
border-radius: 32px;
border: 1px solid #fff;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
padding: 0 8px;
display: flex;
align-items: center;
height: max-content;
"
>
<img src="../../../assets/icon/search.png" alt="" width="24px" style="opacity: 0.6" />
<input
v-model="searchQuery"
type="text"
:placeholder="t('enter_host_id')"
style="
width: 100%;
outline: none;
border: none;
background-color: transparent;
color: black;
padding: 10px 8px;
"
@keyup.enter="performSearch"
/>
</div>
<!-- 按钮 -->
<div v-if="!hasSearched && searchQuery" style="font-weight: 600" @click="performSearch">
{{ t('search') }}
</div>
<div v-else style="font-weight: 600" @click="clearSearch">{{ t('cancel') }}</div>
</div>
<!-- 加载成员 -->
<div v-if="loading" class="loading-container">
<div class="loading-spinner"></div>
<p>{{ t('loading') }}...</p>
</div>
<!-- 成员列表 -->
<div v-if="searchResults && searchResults.length > 0" class="members-list">
<div
v-for="(member, index) in searchResults"
:key="member.memberProfile?.id || index"
style="
background-color: white;
padding: 16px;
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="
flex: 1;
min-width: 0;
display: flex;
align-items: center;
justify-content: space-around;
gap: 4px;
"
>
<!-- 头像 -->
<div style="width: 20%">
<img
v-smart-img
:src="member.memberProfile?.userAvatar || ''"
:alt="member.memberProfile?.userNickname"
@error="handleAvatarImageError"
style="
display: block;
width: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
object-fit: cover;
"
/>
</div>
<!-- 账号信息 -->
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px">
<div
style="
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.9em;
"
>
{{ member.memberProfile?.userNickname || t('unknown_user') }}
</div>
<div
style="
color: rgba(0, 0, 0, 0.4);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.9em;
"
>
{{ t('user_id_prefix') }} {{ member.memberProfile?.account || 'N/A' }}
</div>
</div>
<!-- 等级信息 -->
<div style="display: flex; align-items: center">
<div
style="
border-radius: 12px;
background: linear-gradient(
152deg,
rgba(198, 112, 255, 0.6) 7.01%,
rgba(119, 38, 255, 0.6) 92.99%
);
color: #fff;
padding: 2px 4px;
font-weight: 500;
border: 1px solid #d1d5db;
overflow: hidden;
white-space: nowrap;
font-size: 0.9em;
"
>
{{ formatTargetDisplay(member.target?.acceptGiftValue) }}
</div>
</div>
</div>
<!-- 数据区域 -->
<div style="min-width: 0; display: flex; justify-content: flex-end">
<div style="width: 100%; display: flex; flex-direction: column">
<div style="width: 100%; display: flex; align-items: center">
<img
src="../../../assets/icon/coin.png"
alt=""
height="16px"
style="display: block"
/>
<div
style="
flex: 1;
min-width: 0;
color: #ffb627;
font-weight: 510;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.9em;
"
>
{{ formatDiamondValue(member.target?.acceptGiftValue) }}
</div>
</div>
<div
style="
width: 100%;
min-width: 0;
color: rgba(0, 0, 0, 0.4);
font-weight: 510;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.7em;
"
>
{{ t('time_days') }}:{{ member.target?.effectiveDay }}
</div>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<div class="empty-icon">👥</div>
<p>{{ t('no_data_available') }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useLangStore } from '@/stores/lang'
import { useI18n } from 'vue-i18n'
import { setDocumentDirection } from '@/locales/i18n'
import { getTeamId } from '@/utils/userStore.js'
import { handleAvatarImageError } from '@/utils/imageHandler.js'
import { getTeamReleasePolicy } from '@/api/teamBill.js'
const { t, locale } = useI18n()
// 监听语言变化并设置文档方向
locale.value && setDocumentDirection(locale.value)
defineEmits(['close'])
const props = defineProps({
billData: {
type: Object,
required: true,
},
})
const loading = ref(false)
const policyData = ref(null)
const loadingPolicy = ref(false)
const searchQuery = ref('') //搜索目标id
const searchResults = ref([]) //展示结果
const isSearching = ref(false)
const hasSearched = ref(false)
// 样式
const color = ref('')
const boxShadow = ref('')
const typeShowText = ref('')
const langStore = useLangStore()
// 从 billData 中获取成员数据
const members = computed(() => {
return props.billData?.members || []
})
// 执行搜索
const performSearch = async () => {
console.log('searchQuery.value:', searchQuery.value)
if (!searchQuery.value.trim()) {
searchResults.value = members.value
return
}
isSearching.value = true
hasSearched.value = false
let ResultUser = members.value.find(
(member) => member.memberProfile.account == searchQuery.value.trim()
)
if (ResultUser) {
searchResults.value = [ResultUser]
} else {
searchResults.value = []
}
isSearching.value = false
hasSearched.value = true
}
// 清除搜索
const clearSearch = () => {
hasSearched.value = false
searchQuery.value = ''
searchResults.value = members.value
}
// 获取政策数据
const fetchPolicyData = async () => {
try {
loadingPolicy.value = true
const teamId = getTeamId()
if (!teamId) {
console.warn('No team ID available')
return
}
const response = await getTeamReleasePolicy(teamId)
if (response && response.status && response.body) {
policyData.value = response.body
console.debug('Policy data loaded:', policyData.value)
}
} catch (error) {
console.error('Failed to fetch policy data:', error)
} finally {
loadingPolicy.value = false
}
}
// 根据礼物价值匹配政策等级
const getTargetLevel = (giftValue) => {
if (!policyData.value?.policy || !giftValue) {
return 'N/A'
}
const numValue = parseFloat(giftValue)
if (isNaN(numValue)) {
return 'N/A'
}
// 按target从大到小排序找到第一个小于等于giftValue的level
const sortedPolicy = [...policyData.value.policy].sort((a, b) => {
return parseFloat(b.target) - parseFloat(a.target)
})
for (const policy of sortedPolicy) {
if (numValue >= parseFloat(policy.target)) {
return policy.level
}
}
// 如果都不匹配,返回最低等级
const minLevel = Math.min(...policyData.value.policy.map((p) => p.level))
return minLevel
}
// 格式化Target显示
const formatTargetDisplay = (giftValue) => {
if (loadingPolicy.value) {
return t('loading') + '...'
}
const level = getTargetLevel(giftValue)
return `${t('target')}: ${level}`
}
// 格式化状态文本
const getStatusText = (status) => {
// 使用翻译键而不是直接文本
switch (status) {
case 'Completed':
return t('completed')
case 'Pending':
return t('pending')
case 'Out of account':
return t('out_of_account')
case 'In Progress':
return t('in_progress')
default:
return statusInfo.textKey
}
}
// 格式化钻石数值
const formatDiamondValue = (value) => {
if (!value || value === '0') return '0'
// 如果已经是格式化的字符串,直接返回
if (typeof value === 'string' && (value.includes('K') || value.includes('M'))) {
return value.replace(/[^0-9KM.]/g, '')
}
const numValue = parseInt(value)
if (isNaN(numValue)) return '0'
return numValue.toLocaleString()
}
// 更新展示类型
const updateTypeShowText = () => {
console.log('更新展示类型')
let type = props.billData?.status
if (type == 'In Progress') {
typeShowText.value = t('in_progress')
} else if (type == 'Pending') {
typeShowText.value = t('pending')
} else if (type == 'Completed') {
typeShowText.value = t('completed')
} else if (type == 'Out of account') {
typeShowText.value = t('out_of_account')
}
}
// 监听语言变化确保UI更新
watch(
() => langStore.selectedLang,
(newLang) => {
console.log('Language changed in header:', newLang)
updateTypeShowText()
},
{ deep: true }
)
watch(
() => searchQuery.value,
(newQuery) => {
if (hasSearched.value) {
hasSearched.value = false
}
if (!newQuery.trim()) {
searchResults.value = members.value
}
},
{ immediate: true }
)
watch(
() => props.billData?.status,
(newType) => {
updateTypeShowText()
if (newType == 'In Progress') {
boxShadow.value = '0 0 1px 0 #6085FF'
color.value = 'rgba(61, 115, 255, 1)'
}
if (newType == 'Pending') {
boxShadow.value = '0 0 1px 0 #FF6062'
color.value = 'rgba(255, 61, 64, 1)'
}
if (newType == 'Completed') {
boxShadow.value = '0 0 1px 0 #60FF65'
color.value = 'rgba(61, 255, 84, 1)'
}
if (newType == 'Out of account') {
boxShadow.value = '0 0 1px 0 #6A6161'
color.value = 'rgba(174, 174, 174, 1)'
}
return
},
{
immediate: true,
}
)
// 组件挂载时获取政策数据
onMounted(() => {
console.log('locale.value:', locale.value)
fetchPolicyData()
})
</script>
<style scoped>
* {
color: rgba(0, 0, 0, 0.8);
}
input::placeholder {
font-weight: bold;
color: rgba(0, 0, 0, 0.4);
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
.bill-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.bill-status.in-progress {
background-color: #dbeafe;
color: #1d4ed8;
}
.bill-status.pending {
background-color: #fef3c7;
color: #d97706;
}
.bill-status.completed {
background-color: #d1fae5;
color: #059669;
}
/* 成员列表容器 */
.members-container {
padding: 8px;
}
.members-list {
padding: 5px;
display: flex;
flex-direction: column;
gap: 12px;
overflow-y: auto;
}
.members-list::-webkit-scrollbar {
display: none;
}
/* 成员项目 */
.member-item {
background-color: white;
display: flex;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
}
.member-item:hover {
background-color: #f9f9f9;
}
.member-item:first-child {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.member-item:last-child {
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
border-bottom: none;
}
/* 用户头像 */
.member-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
margin-right: 12px;
}
.member-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
background-color: #8b5cf6;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
font-weight: 600;
}
/* 用户信息区域 */
.member-details {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
/* 左侧用户信息 */
.user-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.user-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
.user-id {
font-size: 12px;
color: #666;
margin: 0;
}
/* 右侧统计信息 */
.member-stats {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.diamond-info {
display: flex;
align-items: center;
gap: 4px;
}
.diamond-icon {
font-size: 14px;
}
.diamond-value {
font-size: 14px;
font-weight: 600;
color: #333;
}
.time-info {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
}
.time-label {
color: #666;
}
.time-value {
font-weight: 600;
color: #333;
}
/* 加载状态 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
color: #666;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #8b5cf6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 空数据状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
color: #666;
}
.empty-icon {
font-size: 48px;
margin-bottom: 12px;
}
.empty-state p {
margin: 0;
font-size: 14px;
}
/* 响应式设计 */
@media (max-width: 480px) {
.member-details {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.member-stats {
align-self: flex-end;
align-items: flex-end;
}
}
@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>