471 lines
9.5 KiB
Vue
471 lines
9.5 KiB
Vue
<template>
|
||
<div class="coin-seller-search gradient-background-circles">
|
||
<MobileHeader title="Search user" />
|
||
|
||
<div class="content">
|
||
<!-- 搜索框 -->
|
||
<div class="search-section">
|
||
<div class="search-box">
|
||
<span class="search-icon">🔍</span>
|
||
<input
|
||
v-model="searchQuery"
|
||
type="text"
|
||
placeholder="Please enter the host ID"
|
||
class="search-input"
|
||
@keyup.enter="handleEnterPress"
|
||
@input="handleInputChange"
|
||
/>
|
||
<button
|
||
v-if="searchQuery"
|
||
@click="clearSearch"
|
||
class="clear-btn"
|
||
>
|
||
×
|
||
</button>
|
||
<button
|
||
class="action-btn"
|
||
:class="{ 'confirm': hasValidSelection }"
|
||
@click="handleActionButton"
|
||
:disabled="isSearching"
|
||
>
|
||
{{ getActionButtonText() }}
|
||
</button>
|
||
</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
|
||
v-if="user.userAvatar"
|
||
:src="user.userAvatar"
|
||
:alt="user.name"
|
||
class="avatar-image"
|
||
@error="(e) => e.target.style.display = 'none'"
|
||
/>
|
||
<span v-if="!user.userAvatar" class="avatar-text">{{ user.name.charAt(0) }}</span>
|
||
</div>
|
||
<div class="user-details">
|
||
<h4>{{ user.name }}</h4>
|
||
<p>ID: {{ user.id }}</p>
|
||
</div>
|
||
</div>
|
||
<div class="add-icon">
|
||
<span>➕</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 加载状态 -->
|
||
<div v-if="isSearching" class="loading-state">
|
||
<div class="loading-spinner"></div>
|
||
<p>Searching...</p>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<div v-else-if="searchQuery && !isSearching && searchResults.length === 0" class="empty-state">
|
||
<div class="empty-icon">🔍</div>
|
||
<p>No users found</p>
|
||
<span>Try searching with a different ID</span>
|
||
</div>
|
||
|
||
<!-- 搜索提示 -->
|
||
<div v-else-if="!searchQuery" class="search-hint">
|
||
<div class="hint-icon">👤</div>
|
||
<p>Search for users</p>
|
||
<span>Enter host ID to find users</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import MobileHeader from '../components/MobileHeader.vue'
|
||
import { setSelectedUser } from '../utils/coinSellerStore.js'
|
||
import { searchFreightUser } from '../api/wallet.js'
|
||
|
||
const router = useRouter()
|
||
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 'Searching...'
|
||
if (hasValidSelection.value) return 'Confirm'
|
||
if (searchQuery.value.trim() && !hasSearched.value) return 'Search'
|
||
return '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.account || profile.id,
|
||
userId: profile.id, // 保存数值型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,
|
||
userId: selectedUser.value.userId, // Numeric 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>
|
||
.coin-seller-search {
|
||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||
}
|
||
|
||
.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 2px 8px rgba(0,0,0,0.1);
|
||
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 .add-icon {
|
||
color: #8B5CF6;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.user-info {
|
||
display: flex;
|
||
align-items: center;
|
||
flex: 1;
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 50px;
|
||
height: 50px;
|
||
border-radius: 25px;
|
||
background-color: #F59E0B;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
margin-right: 12px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.avatar-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
border-radius: 25px;
|
||
}
|
||
|
||
.avatar-text {
|
||
color: white;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.user-details h4 {
|
||
margin: 0 0 4px 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.user-details p {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.add-icon {
|
||
color: #10B981;
|
||
font-size: 20px;
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.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;
|
||
}
|
||
|
||
/* 搜索提示 */
|
||
.search-hint {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 60px 20px;
|
||
color: #666;
|
||
text-align: center;
|
||
}
|
||
|
||
.hint-icon {
|
||
font-size: 48px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.search-hint p {
|
||
margin: 0 0 8px 0;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.search-hint span {
|
||
font-size: 14px;
|
||
color: #9ca3af;
|
||
}
|
||
</style>
|