544 lines
11 KiB
Vue
544 lines
11 KiB
Vue
<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>
|