Просмотр исходного кода

Add search history functionality and localization support

- Introduced search history feature in the search page, allowing users to save and load their recent search queries.
- Updated the layout to display search history with appropriate styling.
- Added localization strings for "History" in English, Indonesian, and Chinese to enhance user experience.
0es 3 недель назад
Родитель
Сommit
750695aafe
5 измененных файлов с 154 добавлено и 56 удалено
  1. 1 0
      app/constants.ts
  2. 150 56
      app/pages/search.vue
  3. 1 0
      i18n/locales/en.json
  4. 1 0
      i18n/locales/id.json
  5. 1 0
      i18n/locales/zh.json

+ 1 - 0
app/constants.ts

@@ -2,5 +2,6 @@ export const TOKEN_KEY = 'GAMI-web_auth_token'
 export const DEVICE_UDID_KEY = 'GAMI-web_device_udid'
 export const APP_LOCALE_KEY = 'GAMI-web_app_locale'
 export const CATEGORY_TREE_CACHE_KEY = 'GAMI-web_category_tree'
+export const SEARCH_HISTORY_KEY = 'GAMI-web_search_history'
 
 export const IM_ADMIN = '9998' // 管理员用户编号

+ 150 - 56
app/pages/search.vue

@@ -5,6 +5,7 @@ import DelSvg from '~/assets/icons/search/del.svg'
 import type { PlaymateSearchVo } from '~/types/api'
 import { playmateApi } from '~/api/playmate'
 import { useApi } from '~/composables/useApi'
+import { SEARCH_HISTORY_KEY } from '~/constants'
 
 defineOptions({
   name: 'SearchPage',
@@ -27,6 +28,42 @@ const listFinished = ref(false)
 const searched = ref(false)
 const listRef = ref<{ check: () => void } | null>(null)
 
+const MAX_HISTORY = 10
+const searchHistory = ref<string[]>([])
+
+function loadSearchHistory() {
+  if (import.meta.client) {
+    try {
+      const raw = localStorage.getItem(SEARCH_HISTORY_KEY)
+      const parsed = raw ? JSON.parse(raw) : []
+      searchHistory.value = Array.isArray(parsed) ? parsed.slice(0, MAX_HISTORY) : []
+    }
+    catch {
+      searchHistory.value = []
+    }
+  }
+}
+
+function saveSearchHistory(keyword: string) {
+  const trimmed = keyword.trim()
+  if (!trimmed)
+    return
+  const next = [trimmed, ...searchHistory.value.filter(k => k !== trimmed)].slice(0, MAX_HISTORY)
+  searchHistory.value = next
+  if (import.meta.client) {
+    try {
+      localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(next))
+    }
+    catch {
+      // ignore
+    }
+  }
+}
+
+onMounted(() => {
+  loadSearchHistory()
+})
+
 const handleBack = () => {
   router.back()
 }
@@ -95,10 +132,18 @@ const loadList = async (isLoadMore = false) => {
 }
 
 const handleSearch = () => {
+  const q = keyword.value.trim()
+  if (q)
+    saveSearchHistory(q)
   resetList()
   loadList(false)
 }
 
+const handleHistoryClick = (item: string) => {
+  keyword.value = item
+  handleSearch()
+}
+
 const handleUserClick = (userNo: string) => {
   router.push(`/user/profile?id=${userNo}`)
 }
@@ -146,70 +191,93 @@ const handleUserClick = (userNo: string) => {
       </button>
     </header>
 
-    <!-- Related Contacts -->
     <section class="search-page__content">
-      <h2
-        v-if="searched"
-        class="search-page__section-title"
-      >
-        {{ t('search.relatedContacts') }}
-      </h2>
-
+      <!-- History (Figma: 搜索/记录标签) -->
       <div
-        v-if="listLoading && !list.length"
-        class="search-page__loading"
+        v-if="searchHistory.length > 0"
+        class="search-page__history"
       >
-        {{ t('common.loading') }}
-      </div>
-      <div
-        v-else-if="searched && !list.length"
-        class="search-page__empty"
-      >
-        {{ t('search.noResult') }}
-      </div>
-      <van-list
-        v-else-if="searched"
-        ref="listRef"
-        v-model:loading="listLoading"
-        :finished="listFinished"
-        :finished-text="t('common.noMoreData')"
-        :loading-text="t('common.loading')"
-        :immediate-check="false"
-        @load="loadList(true)"
-      >
-        <ul class="search-page__list">
-          <li
-            v-for="item in list"
-            :key="item.userNo"
-            class="search-page__row"
-            @click="handleUserClick(item.userNo)"
+        <h2 class="search-page__section-title">
+          {{ t('search.history') }}
+        </h2>
+        <div class="search-page__history-tags">
+          <button
+            v-for="item in searchHistory"
+            :key="item"
+            type="button"
+            class="search-page__history-tag"
+            @click="handleHistoryClick(item)"
           >
-            <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"
+            {{ item }}
+          </button>
+        </div>
+      </div>
+
+      <!-- Related Contacts -->
+      <div class="search-page__related">
+        <h2
+          v-if="searched"
+          class="search-page__section-title"
+        >
+          {{ t('search.relatedContacts') }}
+        </h2>
+
+        <div
+          v-if="listLoading && !list.length"
+          class="search-page__loading"
+        >
+          {{ t('common.loading') }}
+        </div>
+        <div
+          v-else-if="searched && !list.length"
+          class="search-page__empty"
+        >
+          {{ t('search.noResult') }}
+        </div>
+        <van-list
+          v-else-if="searched"
+          ref="listRef"
+          v-model:loading="listLoading"
+          :finished="listFinished"
+          :finished-text="t('common.noMoreData')"
+          :loading-text="t('common.loading')"
+          :immediate-check="false"
+          @load="loadList(true)"
+        >
+          <ul 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__meta">
-                  <span>ID {{ item.userNo }}</span>
-                  <!-- <span>{{ t('search.fansCount', { count: item.fansCount ?? 0 }) }}</span> -->
+                <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>
-            </div>
-          </li>
-        </ul>
-      </van-list>
+            </li>
+          </ul>
+        </van-list>
+      </div>
     </section>
   </div>
 </template>
@@ -300,6 +368,32 @@ const handleUserClick = (userNo: string) => {
 .search-page__content {
   padding: 0 16px;
   padding-top: 10px;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.search-page__history-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.search-page__history-tag {
+  flex-shrink: 0;
+  height: 22px;
+  padding: 3px 8px;
+  border: 1px solid #c9cdd4;
+  border-radius: 200px;
+  background: transparent;
+  font-size: 11px;
+  line-height: 14px;
+  color: #454a50;
+  -webkit-tap-highlight-color: transparent;
+  max-width: 120px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
 .search-page__section-title {

+ 1 - 0
i18n/locales/en.json

@@ -2,6 +2,7 @@
   "search": {
     "placeholder": "Search by ID or nickname",
     "button": "Search",
+    "history": "History",
     "relatedContacts": "Related users",
     "fansCount": "{count} fans",
     "noResult": "No related users"

+ 1 - 0
i18n/locales/id.json

@@ -2,6 +2,7 @@
   "search": {
     "placeholder": "Cari berdasarkan ID atau nama",
     "button": "Cari",
+    "history": "Riwayat",
     "relatedContacts": "Pengguna terkait",
     "fansCount": "{count} penggemar",
     "noResult": "Tidak ada pengguna terkait"

+ 1 - 0
i18n/locales/zh.json

@@ -2,6 +2,7 @@
   "search": {
     "placeholder": "输入ID或昵称搜索",
     "button": "搜索",
+    "history": "搜索历史",
     "relatedContacts": "相关用户",
     "fansCount": "{count} 粉丝",
     "noResult": "暂无相关用户"