Explorar el Código

Remove MCP configuration and add search functionality with localization support

- Deleted the MCP configuration file.
- Integrated a search icon in the index page for navigation.
- Added search-related localization strings in English, Indonesian, and Chinese for improved user experience.
0es hace 3 semanas
padre
commit
1ca19966d8

+ 0 - 7
.cursor/mcp.json

@@ -1,7 +0,0 @@
-{
-  "mcpServers": {
-    "Sentry": {
-      "url": "https://mcp.sentry.dev/mcp/gamivip/javascript-nuxt"
-    }
-  }
-}

+ 5 - 0
app/assets/icons/search.svg

@@ -0,0 +1,5 @@
+<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="15" cy="15" r="14.5" fill="white" fill-opacity="0.5" stroke="white"/>
+<path d="M20.8333 14.1667C20.8333 17.8486 17.8486 20.8333 14.1667 20.8333C10.4848 20.8333 7.5 17.8486 7.5 14.1667C7.5 10.4848 10.4848 7.5 14.1667 7.5C16.1578 7.5 17.9451 8.37292 19.1667 9.75696" stroke="#3FBFBD" stroke-width="1.66667" stroke-linecap="round"/>
+<path d="M18.75 19.5834L22.0833 22.9167" stroke="#3FBFBD" stroke-width="1.66667" stroke-linecap="round"/>
+</svg>

+ 4 - 0
app/assets/icons/search/search-detail.svg

@@ -0,0 +1,4 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.25 8.25C14.25 11.5637 11.5637 14.25 8.25 14.25C4.93629 14.25 2.25 11.5637 2.25 8.25C2.25 4.93629 4.93629 2.25 8.25 2.25C10.042 2.25 11.6506 3.03563 12.75 4.28126" stroke="#1D2129" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M12.375 13.125L15.375 16.125" stroke="#1D2129" stroke-width="1.5" stroke-linecap="round"/>
+</svg>

+ 5 - 0
app/pages/index.vue

@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'
 import LanguageSvg from '~/assets/icons/language.svg'
 import ProfileSvg from '~/assets/icons/profile.svg'
 import AboutSvg from '~/assets/icons/about.svg'
+import SearchSvg from '~/assets/icons/search.svg'
 import { useApi } from '~/composables/useApi'
 import { skillApi } from '~/api/skill'
 import type { SkillSearchDTO, SkillSearchVo } from '~/types/api'
@@ -231,6 +232,10 @@ const handleLanguageSelect = () => {
           >
         </div>
         <div class="flex items-center gap-[16px]">
+          <SearchSvg
+            class="cursor-pointer"
+            @click="router.push('/search')"
+          />
           <AboutSvg @click="handleAbout" />
           <LanguageSvg @click="handleLanguageSelect" />
           <ProfileSvg @click="handleProfile" />

+ 371 - 0
app/pages/search.vue

@@ -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>

+ 9 - 0
i18n/locales/en.json

@@ -1,4 +1,13 @@
 {
+  "search": {
+    "placeholder": "Search by ID or nickname",
+    "button": "Search",
+    "relatedContacts": "Related Contacts",
+    "follow": "Follow",
+    "followed": "Followed",
+    "fansCount": "Fans {count}",
+    "noResult": "No related users"
+  },
   "common": {
     "noMoreData": "No more data",
     "loading": "Loading...",

+ 9 - 0
i18n/locales/id.json

@@ -1,4 +1,13 @@
 {
+  "search": {
+    "placeholder": "Cari berdasarkan ID atau nama",
+    "button": "Search",
+    "relatedContacts": "Related Contacts",
+    "follow": "Follow",
+    "followed": "Followed",
+    "fansCount": "Penggemar {count}",
+    "noResult": "Tidak ada pengguna terkait"
+  },
   "common": {
     "noMoreData": "Tidak ada data lagi",
     "loading": "Memuat...",

+ 9 - 0
i18n/locales/zh.json

@@ -1,4 +1,13 @@
 {
+  "search": {
+    "placeholder": "输入ID或昵称搜索",
+    "button": "Search",
+    "relatedContacts": "Related Contacts",
+    "follow": "Follow",
+    "followed": "Followed",
+    "fansCount": "粉丝数 {count}",
+    "noResult": "暂无相关用户"
+  },
   "common": {
     "noMoreData": "没有更多数据了",
     "loading": "加载中...",