|
|
@@ -0,0 +1,371 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { useI18n } from 'vue-i18n'
|
|
|
+import SearchSvg from '~/assets/icons/search/search-detail.svg'
|
|
|
+import type { UserInfoItemVO } from '~/types/api'
|
|
|
+import { userApi } from '~/api/user'
|
|
|
+import { useApi } from '~/composables/useApi'
|
|
|
+
|
|
|
+defineOptions({
|
|
|
+ name: 'SearchPage',
|
|
|
+})
|
|
|
+
|
|
|
+definePageMeta({
|
|
|
+ auth: false,
|
|
|
+})
|
|
|
+
|
|
|
+const { t } = useI18n()
|
|
|
+const router = useRouter()
|
|
|
+const { request } = useApi()
|
|
|
+
|
|
|
+const keyword = ref('')
|
|
|
+const list = ref<UserInfoItemVO[]>([])
|
|
|
+const searching = ref(false)
|
|
|
+const searched = ref(false)
|
|
|
+
|
|
|
+const handleBack = () => {
|
|
|
+ router.back()
|
|
|
+}
|
|
|
+
|
|
|
+const handleClear = () => {
|
|
|
+ keyword.value = ''
|
|
|
+}
|
|
|
+
|
|
|
+const doSearch = async () => {
|
|
|
+ const q = keyword.value.trim()
|
|
|
+ if (!q) {
|
|
|
+ list.value = []
|
|
|
+ searched.value = true
|
|
|
+ return
|
|
|
+ }
|
|
|
+ searching.value = true
|
|
|
+ searched.value = true
|
|
|
+ try {
|
|
|
+ const res = await request(() => userApi.getUserInfos([q]))
|
|
|
+ list.value = res?.list ?? []
|
|
|
+ }
|
|
|
+ finally {
|
|
|
+ searching.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleSearch = () => {
|
|
|
+ doSearch()
|
|
|
+}
|
|
|
+
|
|
|
+const handleUserClick = (userNo: string) => {
|
|
|
+ router.push(`/user/profile?id=${userNo}`)
|
|
|
+}
|
|
|
+
|
|
|
+// Follow button is display-only until follow API is available
|
|
|
+const toggleFollow = (_item: UserInfoItemVO) => {
|
|
|
+ // TODO: call follow/unfollow API when available
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="search-page min-h-screen bg-white text-[#1d2129]">
|
|
|
+ <!-- Search bar: back, input, Search button (Figma 01_搜索页搜索栏) -->
|
|
|
+ <header class="search-page__bar">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="search-page__back"
|
|
|
+ aria-label="back"
|
|
|
+ @click="handleBack"
|
|
|
+ >
|
|
|
+ <van-icon
|
|
|
+ name="arrow-left"
|
|
|
+ :size="24"
|
|
|
+ />
|
|
|
+ </button>
|
|
|
+ <div class="search-page__input-wrap">
|
|
|
+ <SearchSvg class="search-page__input-icon" />
|
|
|
+ <input
|
|
|
+ v-model="keyword"
|
|
|
+ type="text"
|
|
|
+ class="search-page__input"
|
|
|
+ :placeholder="t('search.placeholder')"
|
|
|
+ @keydown.enter.prevent="handleSearch"
|
|
|
+ >
|
|
|
+ <button
|
|
|
+ v-if="keyword"
|
|
|
+ type="button"
|
|
|
+ class="search-page__clear"
|
|
|
+ aria-label="clear"
|
|
|
+ @click="handleClear"
|
|
|
+ >
|
|
|
+ <van-icon
|
|
|
+ name="cross"
|
|
|
+ :size="16"
|
|
|
+ />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="search-page__btn-search"
|
|
|
+ @click="handleSearch"
|
|
|
+ >
|
|
|
+ {{ t('search.button') }}
|
|
|
+ </button>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <!-- Related Contacts -->
|
|
|
+ <section class="search-page__content">
|
|
|
+ <h2 class="search-page__section-title">
|
|
|
+ {{ t('search.relatedContacts') }}
|
|
|
+ </h2>
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-if="searching"
|
|
|
+ class="search-page__loading"
|
|
|
+ >
|
|
|
+ {{ t('common.loading') }}
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-else-if="searched && !list.length"
|
|
|
+ class="search-page__empty"
|
|
|
+ >
|
|
|
+ {{ t('search.noResult') }}
|
|
|
+ </div>
|
|
|
+ <ul
|
|
|
+ v-else
|
|
|
+ class="search-page__list"
|
|
|
+ >
|
|
|
+ <li
|
|
|
+ v-for="item in list"
|
|
|
+ :key="item.userNo"
|
|
|
+ class="search-page__row"
|
|
|
+ @click="handleUserClick(item.userNo)"
|
|
|
+ >
|
|
|
+ <div class="search-page__row-left">
|
|
|
+ <div class="search-page__avatar-wrap">
|
|
|
+ <NuxtImg
|
|
|
+ :src="item.avatar"
|
|
|
+ :alt="item.nickname"
|
|
|
+ class="search-page__avatar"
|
|
|
+ loading="lazy"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="search-page__info">
|
|
|
+ <div class="search-page__name-row">
|
|
|
+ <span class="search-page__name">{{ item.nickname }}</span>
|
|
|
+ <CommonGender
|
|
|
+ :gender="item.gender"
|
|
|
+ :age="item.age"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="search-page__meta">
|
|
|
+ <span>ID {{ item.userNo }}</span>
|
|
|
+ <span>{{ t('search.fansCount', { count: item.fansCount ?? 0 }) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="search-page__follow-btn"
|
|
|
+ :class="{ 'search-page__follow-btn--followed': item.follow }"
|
|
|
+ @click.stop="toggleFollow(item)"
|
|
|
+ >
|
|
|
+ {{ item.follow ? t('search.followed') : t('search.follow') }}
|
|
|
+ </button>
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.search-page {
|
|
|
+ padding-top: env(safe-area-inset-top);
|
|
|
+ padding-bottom: env(safe-area-inset-bottom);
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__bar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ height: 54px;
|
|
|
+ padding: 9px 16px;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__back {
|
|
|
+ flex-shrink: 0;
|
|
|
+ padding: 0;
|
|
|
+ border: none;
|
|
|
+ background: transparent;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ -webkit-tap-highlight-color: transparent;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__input-wrap {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ height: 36px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 0 10px;
|
|
|
+ background: #f2f3f5;
|
|
|
+ border-radius: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__input-icon {
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 18px;
|
|
|
+ height: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__input {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ border: none;
|
|
|
+ background: transparent;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 16px;
|
|
|
+ color: #1d2129;
|
|
|
+ outline: none;
|
|
|
+
|
|
|
+ &::placeholder {
|
|
|
+ color: #c9cdd4;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__clear {
|
|
|
+ flex-shrink: 0;
|
|
|
+ padding: 0;
|
|
|
+ border: none;
|
|
|
+ background: transparent;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: #86909c;
|
|
|
+ -webkit-tap-highlight-color: transparent;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__btn-search {
|
|
|
+ flex-shrink: 0;
|
|
|
+ padding: 0;
|
|
|
+ border: none;
|
|
|
+ background: transparent;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: normal;
|
|
|
+ color: #1d2129;
|
|
|
+ -webkit-tap-highlight-color: transparent;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__content {
|
|
|
+ padding: 0 16px;
|
|
|
+ padding-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__section-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1d2129;
|
|
|
+ margin: 0 0 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__loading,
|
|
|
+.search-page__empty {
|
|
|
+ padding: 24px 0;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #86909c;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__list {
|
|
|
+ list-style: none;
|
|
|
+ margin: 0;
|
|
|
+ padding: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 10px;
|
|
|
+ min-height: 44px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__row-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ min-width: 0;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__avatar-wrap {
|
|
|
+ position: relative;
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 44px;
|
|
|
+ height: 44px;
|
|
|
+ border-radius: 50%;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 0.5px solid #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__avatar {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__info {
|
|
|
+ min-width: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__name-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__name {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1d2129;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__meta {
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 14px;
|
|
|
+ color: #86909c;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 7px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-page__follow-btn {
|
|
|
+ flex-shrink: 0;
|
|
|
+ height: 22px;
|
|
|
+ padding: 0 10px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 200px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 22px;
|
|
|
+ color: #fff;
|
|
|
+ background: linear-gradient(90deg, #4ed2ff 0%, #b1ef5d 137.08%);
|
|
|
+ -webkit-tap-highlight-color: transparent;
|
|
|
+
|
|
|
+ &--followed {
|
|
|
+ color: #c9cdd4;
|
|
|
+ background: #f2f3f5;
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 11px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|