aslan-h5/src/views/RechargeAgency/SearchUser.vue

544 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<div class="coin-seller-search gradient-background-circles">
<!-- 使用 GeneralHeader 替换 MobileHeader -->
<GeneralHeader
:showLanguageList="true"
:title="t('search_user')"
color="black"
style="box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); z-index: 999"
/>
<div class="content">
<!-- 搜索框 -->
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px">
<div
style="
flex: 1;
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"
class="search-icon"
/>
<input
v-model="searchQuery"
type="text"
name=""
id=""
:placeholder="t('enter_user_id')"
@keyup.enter="handleEnterPress"
@input="handleInputChange"
style="
width: 100%;
color: rgba(0, 0, 0, 0.4);
font-weight: 600;
font-size: 14px;
outline: none;
border: none;
background-color: transparent;
padding: 10px 8px;
"
/>
<button v-if="searchQuery" @click="clearSearch" class="clear-btn">×</button>
</div>
<div style="font-weight: 600" @click="handleActionButton" :disabled="isSearching">
{{ getActionButtonText() }}
</div>
</div>
<!-- 搜索结果 -->
<div v-if="searchResults.length > 0" class="results-section">
<div
v-for="user in searchResults"
:key="user.id"
class="user-item"
:class="{ selected: selectedUser?.id === user.id }"
@click="toggleUserSelection(user)"
>
<div class="user-info">
<div class="user-avatar">
<img
:src="user.userAvatar || ''"
:alt="user.name"
style="
display: block;
object-fit: cover;
width: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
"
@error="handleAvatarImageError"
/>
</div>
<div style="min-width: 0; flex: 1">
<div
style="
margin-bottom: 4px;
font-weight: 700;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ user.name }}
</div>
<div style="font-size: 14px; color: rgba(0, 0, 0, 0.4); font-weight: 500">
<span style="display: flex; align-items: center">
<span>ID</span>
<span style="margin: 0 2px">:</span>
<span>{{ user.account }}</span>
</span>
</div>
</div>
</div>
<div style="display: flex; justify-content: center; align-items: center">
<img src="../../assets/icon/add.png" alt="" width="32px" class="add-icon" />
</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="isSearching" class="loading-state">
<div class="loading-spinner"></div>
<p>{{ t('searching') }}...</p>
</div>
<!-- 空状态 -->
<div
v-else-if="hasSearched && searchResults.length === 0 && !isSearching"
class="empty-state"
>
<div class="empty-icon">🔍</div>
<p>{{ t('no_users_found') }}</p>
</div>
<!-- 初始状态 -->
<div v-else-if="!searchQuery" class="initial-state">
<div class="initial-icon">👤</div>
<p>{{ t('enter_user_id_to_search') }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { setSelectedUser } from '@/utils/coinSellerStore.js'
import { setDocumentDirection } from '@/locales/i18n'
import { handleAvatarImageError } from '@/utils/imageHandler.js'
import { searchFreightUser } from '@/api/wallet.js'
import GeneralHeader from '@/components/GeneralHeader.vue'
const { t, locale } = useI18n()
const router = useRouter()
// 监听语言变化并设置文档方向
locale.value && setDocumentDirection(locale.value)
const searchQuery = ref('')
const searchResults = ref([])
const isSearching = ref(false)
const hasSearched = ref(false)
const selectedUser = ref(null)
// Computed properties
const hasValidSelection = computed(() => {
return selectedUser.value !== null
})
// Handle input changes
const handleInputChange = () => {
if (!searchQuery.value.trim()) {
searchResults.value = []
hasSearched.value = false
selectedUser.value = null
}
}
// Handle enter key press
const handleEnterPress = () => {
if (searchQuery.value.trim()) {
performSearch()
}
}
// Handle action button click
const handleActionButton = () => {
if (hasValidSelection.value) {
confirmSelection()
} else if (searchQuery.value.trim() && !hasSearched.value) {
performSearch()
} else {
cancelSearch()
}
}
// Get button text
const getActionButtonText = () => {
if (isSearching.value) return t('searching')
if (hasValidSelection.value) return t('confirm')
if (searchQuery.value.trim() && !hasSearched.value) return t('search')
return t('cancel')
}
// Perform search
const performSearch = async () => {
if (!searchQuery.value.trim()) {
searchResults.value = []
return
}
isSearching.value = true
try {
const response = await searchFreightUser(searchQuery.value.trim())
if (response && response.status && response.body && response.body.userProfile) {
const profile = response.body.userProfile
searchResults.value = [
{
id: profile.id,
name: profile.userNickname || 'User',
userNickname: profile.userNickname,
account: profile.account,
userAvatar: profile.userAvatar,
},
]
} else {
searchResults.value = []
}
hasSearched.value = true
selectedUser.value = null // Reset selection
} catch (error) {
console.error('Search user failed:', error)
searchResults.value = []
hasSearched.value = true
} finally {
isSearching.value = false
}
}
// Clear search
const clearSearch = () => {
searchQuery.value = ''
searchResults.value = []
hasSearched.value = false
selectedUser.value = null
}
// Cancel search
const cancelSearch = () => {
router.go(-1)
}
// Toggle user selection
const toggleUserSelection = (user) => {
if (selectedUser.value?.id === user.id) {
selectedUser.value = null // Deselect
} else {
selectedUser.value = user // Select user
}
}
// Confirm selection
const confirmSelection = () => {
if (selectedUser.value) {
// Save selected user info to global state
setSelectedUser({
id: selectedUser.value.id,
name: selectedUser.value.name,
userNickname: selectedUser.value.userNickname,
account: selectedUser.value.account,
userAvatar: selectedUser.value.userAvatar,
})
// Go back to previous page
router.go(-1)
}
}
</script>
<style scoped>
* {
color: rgba(0, 0, 0, 0.8);
}
input::placeholder {
font-weight: bold;
color: rgba(0, 0, 0, 0.4);
}
.content {
padding: 16px;
position: relative;
z-index: 2;
}
/* 搜索部分 */
.search-section {
margin-bottom: 20px;
}
.search-box {
display: flex;
align-items: center;
background-color: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 8px 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.search-icon {
margin-right: 8px;
color: #9ca3af;
}
.search-input {
flex: 1;
border: none;
background: none;
outline: none;
font-size: 14px;
color: #333;
}
.search-input::placeholder {
color: #9ca3af;
}
.clear-btn {
background: none;
border: none;
font-size: 18px;
color: #9ca3af;
cursor: pointer;
padding: 0 8px;
}
.action-btn {
background: none;
border: none;
color: #8b5cf6;
font-size: 14px;
font-weight: 500;
cursor: pointer;
padding: 4px 8px;
margin-left: 8px;
}
.action-btn:active {
color: #7c3aed;
}
.action-btn.confirm {
background-color: #8b5cf6;
color: white;
border-radius: 6px;
padding: 6px 12px;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 搜索结果 */
.results-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.user-item {
background-color: white;
padding: 16px;
border-radius: 12px;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: space-between;
}
.user-item:active {
transform: scale(0.98);
}
.user-item.selected {
border: 2px solid #8b5cf6;
background-color: #f3f4f6;
}
.user-item.selected {
color: #8b5cf6;
font-weight: bold;
}
.user-info {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
}
.user-avatar {
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
}
.avatar-text {
color: white;
font-size: 18px;
font-weight: 600;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #f59e0b;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-state p {
margin: 0;
font-size: 14px;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-state p {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 500;
}
.empty-state span {
font-size: 14px;
color: #9ca3af;
}
/* 搜索提示 */
.initial-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
text-align: center;
}
.initial-icon {
font-size: 48px;
margin-bottom: 16px;
}
.initial-state p {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 500;
}
.initial-state span {
font-size: 14px;
color: #9ca3af;
}
@media screen and (max-width: 360px) {
* {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
* {
font-size: 12px;
}
}
@media screen and (min-width: 768px) {
* {
font-size: 24px;
}
}
/* RTL支持 */
[dir='rtl'] .search-icon {
transform: scaleX(-1);
}
[dir='rtl'] .add-icon {
transform: scaleX(-1);
}
[dir='rtl'] .user-avatar {
margin-right: 0;
margin-left: 12px;
}
[dir='rtl'] .clear-btn {
margin-right: 8px;
margin-left: 0;
}
</style>