Browse Source

2025年06月06日:01)完成 “常见问题” 和 “文章详情” 页开发;02)网页内容优化

yankun 1 month ago
parent
commit
7de914e00b

+ 1 - 1
.env.development

@@ -1,5 +1,5 @@
 # 请求 url
-VITE_BASE_URL=
+VITE_BASE_URL=https://vvbuy.csvip.top/api
 
 # 标题
 VITE_APP_TITLE="VAVA BUY"

+ 1 - 1
.env.production

@@ -1,5 +1,5 @@
 # 请求 url
-VITE_BASE_URL=
+VITE_BASE_URL=https://get.vavabuy.com/api/
 
 # 标题
 VITE_APP_TITLE="VAVA BUY"

+ 9 - 0
index.html

@@ -9,5 +9,14 @@
   <body>
     <div id="app"></div>
     <script type="module" src="/src/main.ts"></script>
+    
+    <!-- Twitter conversion tracking base code -->
+    <script>
+      !function(e,t,n,s,u,a){e.twq||(s=e.twq=function(){s.exe?s.exe.apply(s,arguments):s.queue.push(arguments);
+      },s.version='1.1',s.queue=[],u=t.createElement(n),u.async=!0,u.src='https://static.ads-twitter.com/uwt.js',
+      a=t.getElementsByTagName(n)[0],a.parentNode.insertBefore(u,a))}(window,document,'script');
+      twq('config','prkh1');
+    </script>
+  <!-- End Twitter conversion tracking base code -->
   </body>
 </html>

+ 19 - 0
src/App.vue

@@ -1,5 +1,24 @@
 <script setup lang="ts">
+import { onMounted, onBeforeMount } from 'vue'
 import { RouterView } from 'vue-router'
+import useSystemStore from '@/stores/useSystemStore'
+
+const systemStore = useSystemStore()
+const { updateScreenWidth } = systemStore
+
+onMounted(() => {
+  window.addEventListener('resize', onWindowResize)
+})
+
+onBeforeMount(() => {
+  window.removeEventListener('resize', onWindowResize)
+})
+
+function onWindowResize() {
+  const clientWidth = document.documentElement.clientWidth
+  updateScreenWidth(clientWidth)
+}
+onWindowResize()
 </script>
 
 <template>

+ 32 - 0
src/apis/article.ts

@@ -0,0 +1,32 @@
+import request from '@/utils/request'
+
+
+
+export const getContentHelpApi = (params: Object) => request({
+  url: '/content/footer',
+  method: 'get',
+  params: params
+})
+
+
+export const getContentDetailApi = (params: Object) => request({
+  url: '/content/detail',
+  method: 'get',
+  params: params
+})
+
+export const getContentDetailByKeywordApi = (params: Object) => request({
+  url: '/other/about',
+  method: 'get',
+  params: params
+})
+
+
+
+export const getFAQCateListApi = (params: Object) => request({
+  url: '/content/faq',
+  method: 'get',
+  params: params
+})
+
+

+ 0 - 9
src/apis/system.ts

@@ -1,9 +0,0 @@
-import request from '@/utils/request'
-
-
-
-export const getInfo = (params: Object) => request({
-  url: '/api/cate/getCateListProduct',
-  method: 'get',
-  params: params
-})

+ 2 - 2
src/locale/index.ts

@@ -5,7 +5,7 @@ import zh from './lang/zh'
 
 // 获取当前语言
 export function getCurrentLang(): string | null {
-  return !!localStorage.getItem('locale') && localStorage.getItem('locale') !== 'null' && localStorage.getItem('locale') !== 'undefine' ? localStorage.getItem('locale') : 'zh-CN'
+  return !!localStorage.getItem('locale') && localStorage.getItem('locale') !== 'null' && localStorage.getItem('locale') !== 'undefine' ? localStorage.getItem('locale') : 'en'
 }
 
 
@@ -13,7 +13,7 @@ export function getCurrentLang(): string | null {
 const i18n = createI18n({
   legacy: false,
   locale: (getCurrentLang() as string),
-  fallbackLocale: 'zh-CN',
+  fallbackLocale: 'en',
   messages: {
     en: en,
     "zh-CN": zh,

+ 19 - 5
src/locale/lang/en.ts

@@ -18,7 +18,7 @@ export default {
     "加入VAVA BUY 以获取您的收货地址": "Join VAVA BUY to obtain your delivery address",
     "下单购买": "Place an order for purchase",
     "在商店购物,运送到您的新VAVA BUY地址": "Make a purchase at the store and have it delivered to your new VAVA BUY address",
-    "收货转运": "Delivery transfer",
+    "收货转运": "Delivery and transfer",
     "合并包裹并节省高达80%的运费": "Combine packages and save up to 80% on shipping costs",
     "VAVA BUY 优势对比": "VAVA BUY Advantage Comparison",
     "包裹运送到全球,而且还在不断增加...": "Packages are being delivered worldwide, and the number is constantly increasing...",
@@ -27,8 +27,17 @@ export default {
     "新西兰": "New Zealand",
     "澳大利亚": "Australia",
     "意大利": "Italy",
-    "囧二": "Guang Er",
-    updateTimeTxt: 'Updated on {updateTime}'
+    "Lawrence": "Lawrence",
+    "Anna": "Anna",
+    "John": "John",
+    updateTimeTxt: 'Updated on {updateTime}',
+    companyName: 'Guangzhou Wawamai Trading Co., Ltd.',
+    addressLabel: 'Address',
+    companyAddress: 'Room D390, 401-466, No. 401-1, Tianyuan Road, Tianhe District, Guangzhou City',
+    hongkongAddressLabel: 'Address (Hong Kong)',
+    hongkongCompanyAddress: 'UNIT 60 3/F YAU LEE CENTER NO.45 HOI YUEN ROAD Kwun Tong Hong Kong',
+    emailLabel: 'Email',
+    hongkongEmailLabel: 'Email (Hong Kong)',
 
   },
   textLink: {
@@ -59,9 +68,14 @@ export default {
     "获取 VAVA BUY 应用程序": "Get the VAVA BUY app",
   },
   paragraph: {
-    "国际运转·一站解决": "International operation · One-stop solution",
-    "连接全球优质商品,一键下单无忧,实时查看订单进程,享受便捷购物体验。": "Connect to high-quality products from all over the world. Place orders with one click and enjoy worry-free shopping. View the order progress in real time and experience the convenient shopping experience.",
+    "bannerTitle": "International operation · One-stop solution",
+    "bannerDesc": `
+      <div>Access high-quality products from around the world.</div>
+      <div>Place one-click orders and enjoy worry-free shopping.</div>
+      <div>Track your order in real time and enjoy a seamless shopping experience.</div>
+    `,
     "如果你想买其他东西,没有比 Vava buy 更好的了": "If you want to buy anything else, there's no better option than Vava Buy",
+    "太棒了!一切都很顺利,接收订单并快速发货的速度让人印象深刻": "Great! Everything went smoothly. The speed of receiving orders and shipping them quickly was impressive",
     "非常易于使用,包装很精美,会收获很多惊喜": "Very easy to use, with exquisite packaging, and you will receive many surprises",
     "我们使用 cookie 来提供更好的在线体验。 访问和使用 VAVA BUY.com,即表示您同意我们使用 Cookie。 通过阅读我们的条款和条件、使用条款、 和隐私政策。": "We use cookies to provide a better online experience. By visiting and using VAVA BUY.com, you are agreeing to our use of cookies. Please read our terms and conditions, usage terms, and privacy policy.",
   },

+ 16 - 5
src/locale/lang/zh.ts

@@ -19,7 +19,7 @@ export default {
     "下单购买": "下单购买",
     "在商店购物,运送到您的新VAVA BUY地址": "在商店购物,运送到您的新VAVA BUY地址",
     "收货转运": "收货转运",
-    "合并包裹并节省高达80%的运费": "合并包裹并节省高达80%的运费",
+    "合并包裹并节省高达80%的运费": "合并包裹并节省高达 80% 的运费",
     "VAVA BUY 优势对比": "VAVA BUY 优势对比",
     "包裹运送到全球,而且还在不断增加...": "包裹运送到全球,而且还在不断增加...",
     "自 Vava buy.com 成立以来": "自 Vava buy.com 成立以来",
@@ -27,8 +27,18 @@ export default {
     "新西兰": "新西兰",
     "澳大利亚": "澳大利亚",
     "意大利": "意大利",
-    "囧二": "囧二",
-    updateTimeTxt: '更新于 {updateTime}'
+    "Lawrence": "劳伦斯",
+    "Anna": "安娜",
+    "John": "约翰",
+    updateTimeTxt: '更新于 {updateTime}',
+    companyName: '广州哇哇买商贸有限公司',
+    addressLabel: '地址',
+    companyAddress: '广州市天河区天源路401号之一401-466房D390',
+    hongkongAddressLabel: '地址(香港)',
+    hongkongCompanyAddress: '香港观塘海园道45号友利中心3楼60室',
+    emailLabel: '邮箱',
+    hongkongEmailLabel: '邮箱(香港)',
+
   },
   textLink: {
     "常见问题": "常见问题",
@@ -58,9 +68,10 @@ export default {
     "获取 VAVA BUY 应用程序": "获取 VAVA BUY 应用程序",
   },
   paragraph: {
-    "国际运转·一站解决": "国际运转·一站解决",
-    "连接全球优质商品,一键下单无忧,实时查看订单进程,享受便捷购物体验。": "连接全球优质商品,一键下单无忧,实时查看订单进程,享受便捷购物体验。",
+    "bannerTitle": "国际运转·一站解决",
+    "bannerDesc": "连接全球优质商品,一键下单无忧,实时查看订单进程,享受便捷购物体验。",
     "如果你想买其他东西,没有比 Vava buy 更好的了": "如果你想买其他东西,没有比 Vava buy 更好的了",
+    "太棒了!一切都很顺利,接收订单并快速发货的速度让人印象深刻": "太棒了!一切都很顺利,接收订单并快速发货的速度让人印象深刻",
     "非常易于使用,包装很精美,会收获很多惊喜": "非常易于使用,包装很精美,会收获很多惊喜",
     "我们使用 cookie 来提供更好的在线体验。 访问和使用 VAVA BUY.com,即表示您同意我们使用 Cookie。 通过阅读我们的条款和条件、使用条款、 和隐私政策。": "我们使用 cookie 来提供更好的在线体验。 访问和使用 VAVA BUY.com,即表示您同意我们使用 Cookie。 通过阅读我们的条款和条件、使用条款、 和隐私政策。",
   },

BIN
src/public/images/avatar_1.jpg


+ 0 - 0
src/public/images/avatar01.png → src/public/images/avatar_2.png


BIN
src/public/images/avatar_3.jpg


BIN
src/public/images/avatar_4.jpg


BIN
src/public/images/icons/aodaliya_logo_icon.png


BIN
src/public/images/icons/xinxilan_logo_icon.png


BIN
src/public/images/icons/yidali_logo_icon.png


BIN
src/public/images/logo_icon.png


+ 123 - 0
src/stores/useFooterStore.ts

@@ -0,0 +1,123 @@
+import { defineStore } from "pinia"
+import { useRoute, useRouter } from 'vue-router'
+import { getContentHelpApi } from '@/apis/article'
+
+
+
+export interface ILinkInfo {
+  links: string;
+  title: string;
+  id: string | number;
+}
+
+export interface IData {
+  id: number;
+  pid: number;
+  name: string;
+  sort: number;
+  code: string | null;
+  lists: Array<ILinkInfo>; 
+}
+
+// store state
+export interface IFooterState {
+  linkList: Array<IData>;
+  activeLinkId: string | number;
+  activeLinkInfo: ILinkInfo | null;
+  activeLinkCateInfo: IData | null;
+}
+
+
+
+const useFooterStores = defineStore('footer', {
+  state: (): IFooterState => ({
+    linkList: [],
+    activeLinkId: '',
+    activeLinkInfo: null,
+    activeLinkCateInfo: null,
+  }),
+
+  getters: {
+    activeInfo(): ILinkInfo | null {
+      if (this.activeLinkInfo) return this.activeLinkInfo
+      if (this.activeLinkCateInfo && this.activeLinkId) {
+        const result = this.activeLinkCateInfo.lists.find((info: ILinkInfo) => (info.id == this.activeLinkId))
+        return result ? result : null
+      }
+      return null
+    }
+  },
+
+  actions: {
+
+    // 获取数据列表
+    fetchHelpDatas(lang: string) {
+      const route = useRoute()
+      const cateId = route.query.cateId ? route.query.cateId : ''
+      const id = route.query.id ? route.query.id : ''
+
+      switch(lang) {
+        case 'zh-CN':
+          lang = 'zh'; break;
+        default:
+          break;
+      }
+  
+      getContentHelpApi({ lang })
+        .then((result) => {
+          this.linkList = result.data
+          console.log('this.linkList', this.linkList)
+          if (this.linkList.length > 0) {
+            if (cateId) {
+              const cateInfo: IData | undefined = this.linkList.find((item: IData) => (`${item.id}` == cateId))
+              const linkInfo: ILinkInfo | undefined = cateInfo ? cateInfo.lists.find((info: ILinkInfo) => (`${info.id}` == id)) : undefined
+              this.updateActiveInfo(cateInfo ? cateInfo : null, linkInfo ? linkInfo : null)
+            }
+            else if (id) {
+              let linkInfo: ILinkInfo | null = null
+              for (let i = 0; i < this.linkList.length; i++) {
+                for (let j = 0; j < this.linkList[i].lists.length; j++) {
+                  if (`${this.linkList[i].lists[j].id}` == id) {
+                    linkInfo = this.linkList[i].lists[j]
+                    break
+                  }
+                }
+                if (linkInfo) {
+                  break
+                }
+              }
+              this.updateActiveInfo(null, linkInfo)
+            }
+          }
+        })
+        .catch((err) => {
+  
+        })
+    },
+
+    // 更新 active 信息
+    updateActiveInfo(cateInfo: IData | null, linkInfo: ILinkInfo | null, callback?: Function) {
+      if (!linkInfo) {
+        this.activeLinkId = ''
+        this.activeLinkInfo = null
+        this.activeLinkCateInfo = null
+      }
+      else if (linkInfo.links) {
+        console.log(linkInfo.links)
+        window.open(linkInfo.links, '__blank')
+        return
+      }
+      else {
+        this.activeLinkId = linkInfo.id
+        this.activeLinkInfo = linkInfo
+        this.activeLinkCateInfo = cateInfo
+      }
+      callback && callback()
+    }
+  }
+})
+
+
+
+
+export default useFooterStores

+ 13 - 2
src/stores/useSystemStore.ts

@@ -6,6 +6,7 @@ import { getCurrentLang } from '@/locale/index'
 export interface ISystemState {
   locale: string;
   localeList: ILocale[];
+  screenWidth: number;
 }
 
 
@@ -23,13 +24,14 @@ const useSystemStore = defineStore('system', {
     localeList: [
       { name: '简体中文', value: 'zh-CN' },
       { name: 'English', value: 'en' },
-    ]
+    ],
+    screenWidth: 1920
   }),
 
   getters: {
     localeInfo: function (): ILocale {
       const result = this.localeList.find((item: ILocale) => (item.value == this.locale))
-      return result ? result : this.localeList[0]
+      return result ? result : this.localeList[1]
     }
   },
 
@@ -39,6 +41,15 @@ const useSystemStore = defineStore('system', {
         this.locale = localeInfo.value
         localStorage.setItem('locale', localeInfo.value)
       }
+    },
+
+    updateScreenWidth(width: number) {
+      if (width < 325) {
+        this.screenWidth = 325
+      }
+      else {
+        this.screenWidth = width
+      }
     }
   }
 })

+ 3 - 0
src/utils/rem.ts

@@ -1,3 +1,6 @@
+
+
+
 // 基准大小
 const baseSize = 16
 

+ 5 - 1
src/utils/request.ts

@@ -21,6 +21,9 @@ http.interceptors.request.use(
     if (token) {
       config.headers.token = token
     }
+    let lang = localStorage.getItem('locale')
+    lang = lang && lang == 'zh-CN' ? 'zh' : lang
+    config.headers.lang = lang ? lang : 'en'
     return config
   },
   err => {
@@ -31,7 +34,8 @@ http.interceptors.request.use(
 // 响应拦截器
 http.interceptors.response.use(
   res => {
-    if (res.data.code == 1) {
+    // console.log('res', res)
+    if (res.data.ret == 1) {
       return Promise.resolve(res.data)
     }
     else{

+ 112 - 0
src/views/ArticleDetails/hooks/useData.ts

@@ -0,0 +1,112 @@
+import { ref, watch } from 'vue'
+import { useRoute } from 'vue-router'
+import { getContentDetailApi, getContentDetailByKeywordApi } from '@/apis/article'
+
+
+export interface IDataInfo {
+  id: number;
+  title: string;
+  desc: string;
+  content: string;
+  picture: string;
+  inorder: number;
+  show: number;
+  indate: number;
+  update: number;
+  links: any;
+  linkType: any;
+  position: string;
+  type: string;
+  category: number;
+  lang: string; 
+}
+
+
+function useData() {
+
+  // route 对象
+  const route = useRoute()
+  console.log('route', route)
+  const articleId = ref<string | number>('')
+  const articleType = ref<string>('')
+  const dataInfo = ref<IDataInfo | string | null>(null)
+  const loading = ref<boolean>(false)
+
+
+  // 监听路由 id 变化
+  watch(
+    () => route.query,
+    (newVal) => {
+      console.log('newVal', newVal)
+      if (newVal.type) {
+        articleType.value = newVal.type as string
+      }
+      else {
+        articleType.value = ''
+      }
+      if (newVal.id) {
+        articleId.value = (newVal.id as (string | number))
+        fetchData()
+      }
+    },
+    {
+      immediate: true
+    }
+  )
+
+  function fetchData() {
+    switch(articleType.value) {
+      case 'header':
+        fetchArticleDetailByKeyword()
+        break
+      default:
+        fetchArticleDetailById()
+        break
+    }
+  }
+
+
+  // 根据 id 获取文章详情
+  function fetchArticleDetailById() {
+    loading.value = true
+    getContentDetailApi({ id: articleId.value })
+      .then(result => {
+        dataInfo.value = result.data
+      })
+      .catch(err => {
+
+      })
+      .finally(() => {
+        loading.value = false
+      })
+  }
+
+
+  // 根据关键词获取文章详情
+  function fetchArticleDetailByKeyword() {
+    loading.value = true
+    getContentDetailByKeywordApi({ type: articleId.value })
+      .then(result => {
+        dataInfo.value = result.data
+      })
+      .catch(err => {
+
+      })
+      .finally(() => {
+        loading.value = false
+      })
+  }
+
+
+  return {
+    articleId,
+    articleType,
+    dataInfo,
+    loading,
+    fetchArticleDetailById
+  }
+}
+
+
+
+export default useData

+ 58 - 98
src/views/ArticleDetails/index.vue

@@ -1,103 +1,54 @@
 <template>
   <section class="article-container">
-    <!-- 文章标题 -->
-    <h1 class="article-title">{{ article.title }}</h1>
-
-    <!-- 文章元信息 -->
-    <div class="article-meta">
-      <!--  <div class="author-info">
-        <img :src="article.author.avatar" class="author-avatar" alt="作者头像" />
-        <span class="author-name">{{ article.author.name }}</span>
-      </div> -->
-      <div class="publish-time">
-        <span>{{
-          $t('system["updateTimeTxt"]', {
-            updateTime: formatDate(article.publishTime, locale),
-          })
-        }}</span>
-      </div>
-    </div>
-
-    <!-- 富文本内容区域 -->
-    <div class="article-content" v-html="article.content"></div>
-
-    <!-- 标签 -->
-    <div class="article-tags" v-if="article.tags && article.tags.length">
-      <div class="tag" v-for="(tag, i) in article.tags" :key="i">
-        <span>{{ tag }}</span>
-      </div>
+    <div class="loading-box" v-show="loading">
+      <a-spin :spinning="loading" size="large" />
     </div>
+    <template v-if="!loading">
+      <template v-if="articleType != 'header'">
+        <!-- 文章标题 -->
+        <h1 class="article-title">{{ dataInfo ? dataInfo.title : '' }}</h1>
+
+        <!-- 文章元信息 -->
+        <!-- <div class="article-meta">
+          <div class="author-info">
+            <img :src="article.author.avatar" class="author-avatar" alt="作者头像" />
+            <span class="author-name">{{ article.author.name }}</span>
+          </div>
+          <div class="publish-time" v-if="activeInfo">
+            <span>{{
+              $t('system["updateTimeTxt"]', {
+                updateTime: formatDate(activeInfo.publishTime, locale),
+              })
+            }}</span>
+          </div>
+        </div> -->
+      </template>
+
+      <!-- 富文本内容区域 -->
+      <div class="article-content" v-html="dataInfo" v-if="articleType == 'header'"></div>
+      <div class="article-content" v-html="dataInfo ? dataInfo.content : ''" v-else></div>
+
+      <!-- 标签 -->
+      <!-- <div class="article-tags" v-if="activeInfo.tags && activeInfo.tags.length">
+        <div class="tag" v-for="(tag, i) in activeInfo.tags" :key="i">
+          <span>{{ tag }}</span>
+        </div>
+      </div> -->
+    </template>
   </section>
 </template>
 
 <script setup lang="ts">
 import { storeToRefs } from 'pinia'
-import { ref } from 'vue'
 import useSystemStore from '@/stores/useSystemStore'
+import useData from './hooks/useData'
+import type { IDataInfo } from './hooks/useData'
 
+// system store
 const systemStore = useSystemStore()
 const { locale } = storeToRefs(systemStore)
 
-// 模拟文章数据
-const article = ref({
-  title: '《平凡的世界》节选:黄土高原上的青春与奋斗',
-  author: {
-    name: '路遥',
-    avatar: 'https://via.placeholder.com/40',
-  },
-  publishTime: '2023-05-15T10:30:00',
-  content: `
-  <h2>第一章</h2>
-  
-  <p>一九七五年二三月间,一个平平常常的日子,细蒙蒙的雨丝夹着一星半点的雪花,正纷纷淋淋地向大地飘洒着。时令已快到惊蛰,雪当然再不会存留,往往还没等落地,就已经消失得无踪无影了。黄土高原严寒而漫长的冬天看来就要过去,但那真正温暖的春天还远远地没有到来。</p>
-  
-  <p>在这样雨雪交加的日子里,如果没有什么紧要事,人们宁愿一整天足不出户。因此,县城的大街小巷倒也比平时少了许多嘈杂。街巷背阴的地方,冬天残留的积雪和冰溜子正在雨点的敲击下蚀化,石板街上到处都漫流着肮脏的污水。</p>
-  
-  <blockquote>
-    "生活包含着更广阔的意义,而不在于我们实际得到了什么;关键是我们的心灵是否充实。"
-  </blockquote>
-  
-  <h3>孙少平的出场</h3>
-  
-  <p>就在这时候,在空旷的院坝的北头,走过来一个瘦高个的青年人。他胳膊窝里夹着一只碗,缩着脖子在泥地里蹒跚而行。小伙子脸色黄瘦,而且两颊有点塌陷,显得鼻子像希腊人一样又高又直。脸上看来才刚刚褪掉少年的稚气——显然由于营养不良,还没有焕发出他这种年龄所特有的那种青春光彩。</p>
-  
-  <p>他撩开两条瘦长的腿,扑踏扑踏地踩着泥水走着。这也许就是那几个黑面馍的主人?看他那一身可怜的穿戴想必也只能吃这种伙食。瞧吧,他那身衣服尽管式样裁剪得勉强还算是学生装,但分明是自家织出的那种老土粗布,而且黑颜料染得很不均匀,给人一种肮肮脏脏的感觉。脚上的一双旧黄胶鞋已经没有了鞋带,凑合着系两根白线绳;一只鞋帮上甚至还缀补着一块蓝布补丁。裤子显然是前两年缝的,人长布缩,现在已经短窄得吊在了半腿把上;幸亏袜腰高,否则就要露肉了。</p>
-  
-  <img src="https://via.placeholder.com/600x400?text=黄土高原风光" alt="黄土高原风光" style="margin: 20px auto; display: block; border-radius: 8px;">
-  
-  <h2>青春的苦涩与尊严</h2>
-  
-  <p>他独个儿来到馍筐前,先怔了一下,然后便弯腰拾了两个高粱面馍。筐里还剩两个,不知他为什么没有拿。他直起身子来,眼睛不由地朝三只空荡荡的菜盆里瞥了一眼。他瞧见乙菜盆的底子上还有一点残汤剩水。房上的檐水滴答下来,盆底上的菜汤四处飞溅。他扭头瞧了瞧:雨雪迷蒙的大院坝里空无一人。他很快蹲下来,慌得如同偷窃一般,用勺子把盆底上混合着雨水的剩菜汤往自己的碗里舀。铁勺刮盆底的嘶啦声像炸弹的爆炸声一样令人惊心。血涌上了他黄瘦的脸。一滴很大的檐水落在盆底,溅了他一脸菜汤。他闭住眼,紧接着,就见两颗泪珠慢慢地从脸颊上滑落了下来——唉,我们姑且就认为这是他眼中溅进了辣子汤吧!</p>
-  
-  <p>他站起来,用手抹了一把脸,端着半碗剩菜汤,来到西南拐角处的开水房前,在水房后墙上伸出来的管子上给菜汤里搀了一些开水,然后把高粱面馍掰碎泡进去,就蹲在房檐下狼吞虎咽地吃起来。</p>
-  
-  <pre><code>这就是《平凡的世界》开篇展现的,
-一个普通农村青年的日常生活。
-没有华丽的辞藻,
-只有真实得令人心痛的细节描写。</code></pre>
-  
-  <h3>关于尊严的思考</h3>
-  
-  <p>突然间,他听见后面传来一阵低低的议论声:"瞧,这就是那个每天最后一个来拿饭的......"</p>
-  
-  <p>孙少平猛地转过身,眼睛紧张地扫视着刚才声音传来的方向。他发现是两个低年级的女生正看着他交头接耳——她们显然没有料到他会突然转身,立刻尴尬地低下头,装作若无其事地从他身边匆匆走过。</p>
-  
-  <p>孙少平站在那里,手里端着那碗被雨水稀释了的菜汤,感到一种难以言喻的屈辱。他知道,在这所县立高中,像他这样从农村来的学生并不多,而像他这样穷困的就更少了。大多数同学至少还能吃上玉米面馍,像他这样顿顿啃高粱面馍的,恐怕全校也就他一个。</p>
-  
-  <ul>
-    <li>贫穷不是耻辱,但确实带来了难以言说的痛苦</li>
-    <li>青春期的自尊心在这种环境下显得格外脆弱</li>
-    <li>物质匮乏却无法阻挡精神世界的成长</li>
-  </ul>
-  
-  <h2>结语</h2>
-  
-  <p>这就是路遥笔下的《平凡的世界》,它展现了中国社会变革时期普通人的生存状态和精神世界。孙少平的形象,代表了一代农村青年在城乡二元结构下的挣扎与奋斗。小说通过这些看似平凡的日常生活描写,展现了不平凡的人性光辉。</p>
-  
-  <p>正如路遥在书中所写:"<strong>生活不能等待别人来安排,要自己去争取和奋斗;而不论其结果是喜是悲,但可以慰藉的是,你总不枉在这世界上活了一场。</strong>"</p>
-`,
-  tags: ['路遥', '《平凡的世界》', '茅盾文学奖', '现实主义'],
-})
+const { dataInfo, articleType, loading } = useData()
 
 // 格式化日期
 const formatDate = (dateString: string, lang: string) => {
@@ -108,8 +59,9 @@ const formatDate = (dateString: string, lang: string) => {
 
 <style scoped lang="less">
 .article-container {
-  max-width: 1200px;
+  max-width: 800px;
   max-width: 75rem;
+  max-width: 50rem;
   margin: 0 auto;
   padding: 60px 15px;
   padding: 3.75rem 0.9375rem;
@@ -117,6 +69,13 @@ const formatDate = (dateString: string, lang: string) => {
   color: #333;
 }
 
+.loading-box {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+}
+
 .article-title {
   font-size: var(--title-size);
   margin-bottom: 20px;
@@ -196,7 +155,7 @@ const formatDate = (dateString: string, lang: string) => {
 
 <style>
 /* 全局样式用于富文本内容 */
-.article-content >>> h2 {
+.article-content h2 {
   font-size: 1.8rem;
   margin: 30px 0 15px;
   margin: 1.875rem 0 0.9375rem;
@@ -206,42 +165,43 @@ const formatDate = (dateString: string, lang: string) => {
   padding-bottom: 0.625rem;
 }
 
-.article-content >>> h3 {
+.article-content h3 {
   font-size: 1.4rem;
   margin: 25px 0 12px;
   margin: 1.5625rem 0 0.75rem;
   color: #333;
 }
 
-.article-content >>> p {
+.article-content p {
   margin: 15px 0;
   margin: 0.9375rem 0;
   font-size: 1rem;
 }
 
-.article-content >>> ul,
-.article-content >>> ol {
+.article-content ul,
+.article-content ol {
   margin: 15px 0;
   margin: 0.9375rem 0;
   padding-left: 30px;
   padding-left: 1.875rem;
 }
 
-.article-content >>> li {
+.article-content li {
   margin: 8px 0;
   margin: 0.5rem 0;
 }
 
-.article-content >>> a {
+.article-content a {
   color: var(--color-active);
   text-decoration: none;
 }
 
-.article-content >>> a:hover {
+.article-content a:hover {
   text-decoration: underline;
 }
 
-.article-content >>> img {
+.article-content img {
+  width: 100%;
   max-width: 100%;
   height: auto;
   margin: 20px 0;
@@ -250,7 +210,7 @@ const formatDate = (dateString: string, lang: string) => {
   border-radius: 0.25rem;
 }
 
-.article-content >>> pre {
+.article-content pre {
   background-color: #f6f8fa;
   padding: 16px;
   padding: 1rem;

+ 78 - 186
src/views/FAQ/hooks/useData.ts

@@ -1,197 +1,51 @@
 import { ref, computed } from 'vue'
+import { getFAQCateListApi } from '@/apis/article'
 
 
-function useData() {
+// 问题
+export interface IFAQInfo {
+  id: number;
+  title: string;
+  links: string;
+  content: string; 
+}
+
+// 分类列表
+export interface ICateInfo {
+  id: number;
+  pid: number;
+  name: string;
+  sort: number;
+  code: number | string;
+  lists: Array<IFAQInfo>; 
+}
+
 
-  // 问题分类
-  const categoryList = ref([
-    { id: 1, name: '账号问题', count: 12 },
-    { id: 2, name: '支付问题', count: 8 },
-    { id: 3, name: '订单问题', count: 15 },
-    { id: 4, name: '产品使用', count: 23 },
-    { id: 5, name: '售后服务', count: 7 },
-    { id: 6, name: '其他问题', count: 5 },
-  ])
-  // 当前选中的分类 ID
-  const activeCategory = ref<string | number>(1)
-
-
-  // 模拟问题数据
-  const questions = ref([
-    {
-      id: 101,
-      categoryId: 1,
-      title: '如何注册账号?',
-      preview: '详细说明注册账号的步骤和注意事项...',
-      content: `
-      <h3>注册账号步骤</h3>
-      <ol>
-        <li>点击网站右上角的"注册"按钮</li>
-        <li>填写您的手机号码或电子邮箱</li>
-        <li>设置登录密码(6-20位字符,包含字母和数字)</li>
-        <li>输入收到的验证码</li>
-        <li>阅读并同意用户协议</li>
-        <li>点击"立即注册"完成注册</li>
-      </ol>
-      <p><strong>注意事项:</strong></p>
-      <ul>
-        <li>请使用常用手机号或邮箱,以便接收重要通知</li>
-        <li>密码请勿设置过于简单</li>
-        <li>如未收到验证码,请检查垃圾邮件或60秒后重新获取</li>
-      </ul>
-    `,
-      updateTime: '2023-05-15',
-      views: 1245,
-      isHot: true,
-      isNew: false,
-    },
-    {
-      id: 102,
-      categoryId: 1,
-      title: '忘记密码怎么办?',
-      preview: '密码找回的几种方法和步骤说明...',
-      content: `
-      <h3>找回密码方法</h3>
-      <p>如果您忘记了密码,可以通过以下方式找回:</p>
-      
-      <h4>方法一:通过手机找回</h4>
-      <ol>
-        <li>在登录页面点击"忘记密码"</li>
-        <li>选择"通过手机找回"</li>
-        <li>输入注册时使用的手机号码</li>
-        <li>获取并输入短信验证码</li>
-        <li>设置新密码并确认</li>
-      </ol>
-      
-      <h4>方法二:通过邮箱找回</h4>
-      <ol>
-        <li>在登录页面点击"忘记密码"</li>
-        <li>选择"通过邮箱找回"</li>
-        <li>输入注册时使用的邮箱地址</li>
-        <li>登录邮箱查收重置密码邮件</li>
-        <li>点击邮件中的链接设置新密码</li>
-      </ol>
-      
-      <p>如果以上方法都无法解决您的问题,请联系客服人员。</p>
-    `,
-      updateTime: '2023-06-02',
-      views: 876,
-      isHot: true,
-      isNew: false,
-    },
-    {
-      id: 201,
-      categoryId: 2,
-      title: '支持哪些支付方式?',
-      preview: '介绍平台目前支持的支付渠道...',
-      content: `
-      <h3>支持的支付方式</h3>
-      <p>我们目前支持以下支付方式:</p>
-      
-      <div class="payment-method">
-        <h4>1. 在线支付</h4>
-        <ul>
-          <li>微信支付</li>
-          <li>支付宝</li>
-          <li>银联在线支付</li>
-          <li>Apple Pay</li>
-        </ul>
-      </div>
-      
-      <div class="payment-method">
-        <h4>2. 银行卡支付</h4>
-        <p>支持以下银行的储蓄卡和信用卡:</p>
-        <ul>
-          <li>中国工商银行</li>
-          <li>中国建设银行</li>
-          <li>中国银行</li>
-          <li>招商银行</li>
-          <li>交通银行等主流银行</li>
-        </ul>
-      </div>
-      
-      <p><strong>注意:</strong>部分银行可能有支付限额,请以银行规定为准。</p>
-    `,
-      updateTime: '2023-04-28',
-      views: 1532,
-      isHot: false,
-      isNew: false,
-    },
-    {
-      id: 301,
-      categoryId: 3,
-      title: '如何查询订单状态?',
-      preview: '订单状态查询的几种途径和方法...',
-      content: `
-      <h3>查询订单状态</h3>
-      <p>您可以通过以下方式查询订单状态:</p>
-      
-      <h4>方法一:网站查询</h4>
-      <ol>
-        <li>登录您的账号</li>
-        <li>点击右上角"我的订单"</li>
-        <li>在订单列表中找到相应订单</li>
-        <li>点击"查看详情"查看订单状态</li>
-      </ol>
-      
-      <h4>方法二:手机APP查询</h4>
-      <ol>
-        <li>打开APP并登录</li>
-        <li>点击底部导航栏"我的"</li>
-        <li>选择"我的订单"</li>
-        <li>查看相应订单状态</li>
-      </ol>
-      
-      <h4>方法三:联系客服查询</h4>
-      <p>如果您无法通过以上方式查询,可以联系客服提供订单号查询。</p>
-      
-      <p><strong>常见订单状态说明:</strong></p>
-      <ul>
-        <li><span class="status waiting">待付款</span> - 订单已生成,等待支付</li>
-        <li><span class="status paid">已支付</span> - 付款成功,等待处理</li>
-        <li><span class="status shipped">已发货</span> - 商品已发出</li>
-        <li><span class="status completed">已完成</span> - 订单交易完成</li>
-        <li><span class="status cancelled">已取消</span> - 订单已取消</li>
-      </ul>
-    `,
-      updateTime: '2023-05-20',
-      views: 2104,
-      isHot: true,
-      isNew: true,
-    },
-  ])
-  // 当前查看的问题详情
-  const currentQuestion = ref(null)
+function useData() {
 
+  // 分类列表
+  const categoryList = ref<ICateInfo[]>([])
+  const isLoading = ref<boolean>(false)
+  // 当前选中的分类
+  const activeCategoryId = ref<string | number>(0)          // id
+  const activeCategoryInfo = ref<ICateInfo | null>(null)    // 分类信息
+   // 当前查看的问题详情
+   const currentQuestion = ref<IFAQInfo | null>(null)
 
   // 搜索查询词
-  const searchQuery = ref('')
+  const searchQuery = ref<string>('')
   // 搜索结果
-  const searchResults = ref<Array<typeof questions.value[0]>>([])
+  const searchResults = ref<Array<IFAQInfo>>([])
 
 
-  // 获取分类名称
-  const getCategoryName = (id: string | number) => {
-    const category = categoryList.value.find((c) => (c.id === id))
-    return category ? category.name : '所有问题'
-  }
-
   // 切换分类
-  const changeCategory = (categoryId: string | number) => {
-    activeCategory.value = categoryId
+  const changeCategory = (categoryInfo: ICateInfo) => {
+    activeCategoryInfo.value = categoryInfo
+    activeCategoryId.value = categoryInfo.id
     currentQuestion.value = null
     searchResults.value = []
   }
 
-  // 获取当前分类下的问题
-  const questionList = computed(() => {
-    return questions.value.filter((q: any) => q.categoryId === activeCategory.value)
-  })
-
-  // 显示问题详情
-  const showQuestionDetail = (question: any) => {
-    currentQuestion.value = question
-  }
 
   // 搜索问题
   const searchQuestions = () => {
@@ -201,26 +55,64 @@ function useData() {
     }
 
     const query = searchQuery.value.toLowerCase()
-    searchResults.value = questions.value.filter((q) => {
-      return q.title.toLowerCase().includes(query) || q.preview.toLowerCase().includes(query)
-    })
+    const list: IFAQInfo[] = []
+    for (let i = 0; i < categoryList.value.length; i++) {
+      let cateInfo: ICateInfo = categoryList.value[i]
+      for (let j = 0; j < cateInfo.lists.length; j++) {
+        let faqInfo: IFAQInfo = cateInfo.lists[j]
+        if (faqInfo.title.toLowerCase().includes(query) || faqInfo.content.toLowerCase().includes(query)) {
+          list.push(faqInfo)
+        }
+      }
+    }
+    searchResults.value = list
     currentQuestion.value = null
   }
 
+
+  // 显示问题详情
+  const showQuestionDetail = (question: IFAQInfo) => {
+    currentQuestion.value = question
+  }
+
   // 返回问题列表
   const backToList = () => {
     currentQuestion.value = null
   }
 
+
+  // 获取问题列表
+  function fetchFAQList() {
+    isLoading.value = true
+    getFAQCateListApi({})
+      .then((result) => {
+        // console.log('result', result)
+        if (result.data && result.data.length > 0) {
+          categoryList.value =  result.data
+          activeCategoryInfo.value = result.data[0]
+          activeCategoryId.value = result.data[0].id
+        }
+        else {
+          categoryList.value =  []
+          activeCategoryInfo.value = null
+          activeCategoryId.value = ''
+        }
+      })
+      .catch(err => {})
+      .finally(() => {
+        isLoading.value = false
+      })
+  }
+  fetchFAQList()
+
   return {
     categoryList,
-    activeCategory,
-    questionList,
+    isLoading,
+    activeCategoryId,
+    activeCategoryInfo,
     currentQuestion,
-    questions,
     searchQuery,
     searchResults,
-    getCategoryName,
     changeCategory,
     showQuestionDetail,
     searchQuestions,

+ 129 - 92
src/views/FAQ/index.vue

@@ -39,7 +39,7 @@
             <!-- 搜索结果列表 -->
             <div v-if="searchResults.length > 0" class="search-results question-list">
               <h2>
-                <span>{{
+                <span style="flex: 1; padding-right: 16px; padding-right: 1rem; line-height: 1.3">{{
                   $t('questionPage["searchResultTitle"]', { count: searchResults.length })
                 }}</span>
                 <div class="category-menu-btn" @click="showCategoryDrawer">
@@ -56,37 +56,41 @@
                 <div class="question-title">
                   {{ item.title }}
                 </div>
-                <div class="question-preview">{{ item.preview }}</div>
+                <div class="question-preview" v-html="item.content"></div>
               </div>
             </div>
 
             <!-- 问题列表 -->
             <div v-else class="question-list">
               <h2>
-                <span>{{ getCategoryName(activeCategory) }}</span>
+                <span style="flex: 1; padding-right: 16px; padding-right: 1rem; line-height: 1.3">{{
+                  activeCategoryInfo ? activeCategoryInfo.name : ''
+                }}</span>
                 <div class="category-menu-btn" @click="showCategoryDrawer">
                   <span>{{ $t('questionPage["questionCategoryMenuBtn"]') }}</span>
                   <img src="@/public/images/icons/menu_icon.png" alt="" />
                 </div>
               </h2>
 
-              <div
-                v-for="question in questionList"
-                :key="question.id"
-                class="question-item"
-                @click="showQuestionDetail(question)"
-              >
-                <div class="question-title">
-                  {{ question.title }}
-                  <div v-if="question.isHot" class="question-tag hot-tag">
-                    {{ $t('questionPage["questionTagHot"]') }}
-                  </div>
-                  <div v-if="question.isNew" class="question-tag new-tag">
-                    {{ $t('questionPage["questionTagNew"]') }}
+              <template v-if="activeCategoryInfo">
+                <div
+                  v-for="(question, index) in activeCategoryInfo.lists"
+                  :key="index"
+                  class="question-item"
+                  @click="showQuestionDetail(question)"
+                >
+                  <div class="question-title">
+                    {{ question.title }}
+                    <!-- <div v-if="question.isHot" class="question-tag hot-tag">
+                      {{ $t('questionPage["questionTagHot"]') }}
+                    </div>
+                    <div v-if="question.isNew" class="question-tag new-tag">
+                      {{ $t('questionPage["questionTagNew"]') }}
+                    </div> -->
                   </div>
+                  <div class="question-preview" v-html="question.content"></div>
                 </div>
-                <div class="question-preview">{{ question.preview }}</div>
-              </div>
+              </template>
             </div>
           </template>
 
@@ -118,21 +122,15 @@
 
             <div class="detail-header">
               <h2>{{ currentQuestion.title }}</h2>
-              <div class="meta">
-                <span>{{
-                  $t('system["updateTimeTxt"]', {
-                    updateTime: formatDate(currentQuestion.updateTime, locale),
-                  })
-                }}</span>
-                <span>{{
-                  $t('questionPage["questionReadCount"]', { count: currentQuestion.views })
-                }}</span>
-              </div>
             </div>
 
             <div class="detail-content" v-html="currentQuestion.content"></div>
           </div>
         </div>
+
+        <div class="loading-box" v-show="isLoading">
+          <a-spin :spinning="isLoading" size="large" />
+        </div>
       </div>
 
       <!-- 分类导航侧边抽屉 -->
@@ -151,12 +149,12 @@
             v-for="category in categoryList"
             :key="category.id"
             class="category-item"
-            :class="{ active: activeCategory === category.id }"
-            @click="changeCategory(category.id)"
+            :class="{ active: activeCategoryId === category.id }"
+            @click="changeCategory(category)"
           >
-            {{ category.name }}
+            <label class="category-item__name">{{ category.name }}</label>
             <span class="count">{{
-              $t('questionPage["questionCategoryCountTxt"]', { count: category.count })
+              $t('questionPage["questionCategoryCountTxt"]', { count: category.lists.length })
             }}</span>
           </div>
         </div>
@@ -167,22 +165,32 @@
     <div class="question-main__container question-main-normal width-1200">
       <!-- 左侧分类导航 -->
       <div class="question-sidebar">
-        <div
-          v-for="category in categoryList"
-          :key="category.id"
-          class="category-item"
-          :class="{ active: activeCategory === category.id }"
-          @click="changeCategory(category.id)"
-        >
-          {{ category.name }}
-          <span class="count">{{
-            $t('questionPage["questionCategoryCountTxt"]', { count: category.count })
-          }}</span>
-        </div>
+        <a-skeleton-button
+          :active="true"
+          size="large"
+          shape="default"
+          block="true"
+          v-if="isLoading"
+        />
+        <template v-else>
+          <div
+            v-for="category in categoryList"
+            :key="category.id"
+            class="category-item"
+            :class="{ active: activeCategoryId === category.id }"
+            @click="changeCategory(category)"
+          >
+            <label class="category-item__name">{{ category.name }}</label>
+            <span class="count">{{
+              $t('questionPage["questionCategoryCountTxt"]', { count: category.lists.length })
+            }}</span>
+          </div>
+        </template>
       </div>
 
       <!-- 右侧内容 -->
       <div class="question-content">
+        <!-- 问题列表 -->
         <template v-if="!currentQuestion">
           <!-- 搜索结果列表 -->
           <div v-if="searchResults.length > 0" class="search-results">
@@ -199,25 +207,27 @@
 
           <!-- 问题列表 -->
           <div v-else class="question-list">
-            <h2>{{ getCategoryName(activeCategory) }}</h2>
+            <h2>{{ activeCategoryInfo ? activeCategoryInfo.name : '' }}</h2>
 
-            <div
-              v-for="question in questionList"
-              :key="question.id"
-              class="question-item"
-              @click="showQuestionDetail(question)"
-            >
-              <div class="question-title">
-                {{ question.title }}
-                <div v-if="question.isHot" class="question-tag hot-tag">
-                  {{ $t('questionPage["questionTagHot"]') }}
-                </div>
-                <div v-if="question.isNew" class="question-tag new-tag">
-                  {{ $t('questionPage["questionTagNew"]') }}
+            <template v-if="activeCategoryInfo">
+              <div
+                v-for="question in activeCategoryInfo.lists"
+                :key="question.id"
+                class="question-item"
+                @click="showQuestionDetail(question)"
+              >
+                <div class="question-title">
+                  {{ question.title }}
+                  <!-- <div v-if="question.isHot" class="question-tag hot-tag">
+                    {{ $t('questionPage["questionTagHot"]') }}
+                  </div>
+                  <div v-if="question.isNew" class="question-tag new-tag">
+                    {{ $t('questionPage["questionTagNew"]') }}
+                  </div> -->
                 </div>
+                <div class="question-preview" v-html="question.content"></div>
               </div>
-              <div class="question-preview">{{ question.preview }}</div>
-            </div>
+            </template>
           </div>
         </template>
 
@@ -225,16 +235,6 @@
         <div v-else class="question-detail">
           <div class="detail-header">
             <h2>{{ currentQuestion.title }}</h2>
-            <div class="meta">
-              <span>{{
-                $t('system["updateTimeTxt"]', {
-                  updateTime: formatDate(currentQuestion.updateTime, locale),
-                })
-              }}</span>
-              <span>{{
-                $t('questionPage["questionReadCount"]', { count: currentQuestion.views })
-              }}</span>
-            </div>
           </div>
 
           <div class="detail-content" v-html="currentQuestion.content"></div>
@@ -263,6 +263,10 @@
             </div> -->
           </div>
         </div>
+
+        <div class="loading-box" v-show="isLoading">
+          <a-spin :spinning="isLoading" size="large" />
+        </div>
       </div>
     </div>
 
@@ -285,7 +289,7 @@
           </svg>
           <div>
             <strong>{{ $t('questionPage["contactForPhoneTitle"]') }}</strong>
-            <p>400-888-9999</p>
+            <p>+852-30623063</p>
             <p>{{ $t('questionPage["contactForPhoneWorkTime"]') }}</p>
           </div>
         </div>
@@ -304,7 +308,7 @@
           </svg>
           <div>
             <strong>{{ $t('questionPage["contactForEmailTitle"]') }}</strong>
-            <p>support@example.com</p>
+            <p>vavabuy@163.com</p>
             <p>{{ $t('questionPage["contactForEmailWorkTime"]') }}</p>
           </div>
         </div>
@@ -315,8 +319,8 @@
 
 <script setup lang="ts">
 import { useI18n } from 'vue-i18n'
-import { storeToRefs } from 'pinia'
 import { ref } from 'vue'
+import { storeToRefs } from 'pinia'
 import useSystemStore from '@/stores/useSystemStore'
 import useData from './hooks/useData'
 
@@ -326,13 +330,12 @@ const { locale } = storeToRefs(systemStore)
 
 const {
   categoryList,
-  activeCategory,
-  questionList,
+  isLoading,
+  activeCategoryId,
+  activeCategoryInfo,
   currentQuestion,
-  questions,
   searchQuery,
   searchResults,
-  getCategoryName,
   changeCategory,
   showQuestionDetail,
   searchQuestions,
@@ -367,6 +370,13 @@ const formatDate = (dateString: string, lang: string) => {
   margin: 0 auto 3.125rem;
 }
 
+.loading-box {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+}
+
 .width-1200 {
   padding: 0 15px 60px;
   padding: 0 0.9375rem 3.75rem;
@@ -439,13 +449,14 @@ const formatDate = (dateString: string, lang: string) => {
   gap: 1.875rem;
 
   .question-sidebar {
-    width: 250px;
-    width: 15.625rem;
-    width: 250px;
-    width: 15.625rem;
+    min-width: 250px;
+    min-width: 15.625rem;
+    max-width: 300px;
+    max-width: 18.75rem;
     flex-shrink: 0;
 
     .category-item {
+      flex: 1;
       padding: 12px 15px;
       padding: 0.75rem 0.9375rem;
       border-left: 4px solid transparent;
@@ -466,10 +477,21 @@ const formatDate = (dateString: string, lang: string) => {
     .category-item.active {
       border-left-color: var(--color-active);
       background: #e3f2fd;
-      font-weight: bold;
+      // font-weight: bold;
+    }
+
+    .category-item__name {
+      flex: 1;
+      padding-right: 12px;
+      padding-right: 0.75rem;
+      /*  white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis; */
     }
 
     .count {
+      min-width: 64px;
+      min-width: 4rem;
       color: #757575;
       font-size: 0.9rem;
     }
@@ -533,8 +555,13 @@ const formatDate = (dateString: string, lang: string) => {
   }
 
   .question-preview {
+    max-height: 24px;
+    max-height: 1.5rem;
     color: #666;
     font-size: 0.9375rem;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
   }
 
   .question-tag {
@@ -570,16 +597,6 @@ const formatDate = (dateString: string, lang: string) => {
         margin-bottom: 0.625rem;
         text-align: center;
       }
-
-      .meta {
-        display: flex;
-        justify-content: center;
-        gap: 15px;
-        gap: 0.9375rem;
-        color: #757575;
-        font-size: 15px;
-        font-size: 0.9375rem;
-      }
     }
 
     .detail-footer {
@@ -640,9 +657,16 @@ const formatDate = (dateString: string, lang: string) => {
   }
 }
 
+.question-main-normal {
+  position: relative;
+  min-height: 370px;
+}
+
 .question-main-768 {
+  position: relative;
   display: none;
   padding-bottom: 0;
+  min-height: 320px;
 }
 
 .question-main__container-768 {
@@ -667,6 +691,7 @@ const formatDate = (dateString: string, lang: string) => {
       display: flex;
       justify-content: space-between;
       align-items: center;
+      align-items: flex-start;
       margin-bottom: 8px;
       margin-bottom: 0.5rem;
       color: var(--color-active);
@@ -675,6 +700,8 @@ const formatDate = (dateString: string, lang: string) => {
     }
 
     .category-menu-btn {
+      min-width: 60px;
+      min-width: 3.75rem;
       display: flex;
       align-items: center;
       color: #333;
@@ -798,6 +825,11 @@ const formatDate = (dateString: string, lang: string) => {
 </style>
 
 <style>
+.question-sidebar .ant-skeleton.ant-skeleton-element,
+.question-sidebar .ant-skeleton.ant-skeleton-element .ant-skeleton-button {
+  height: 100%;
+}
+
 /* 富文本内容样式 */
 .detail-content h3,
 .detail-content h4 {
@@ -832,6 +864,11 @@ const formatDate = (dateString: string, lang: string) => {
   margin: 0.5rem 0;
 }
 
+.question-content img,
+.detail-content img {
+  width: 100%;
+}
+
 .payment-method {
   margin: 20px 0;
   margin: 1.25rem 0;

+ 3 - 6
src/views/Home/modules/Banners/index.vue

@@ -6,13 +6,9 @@
     <!-- content -->
     <div class="banners__content">
       <div class="banners__content-title">
-        <span>{{ $t('paragraph["国际运转·一站解决"]') }}</span>
-      </div>
-      <div class="banners__content-desc">
-        {{
-          $t('paragraph["连接全球优质商品,一键下单无忧,实时查看订单进程,享受便捷购物体验。"]')
-        }}
+        <span>{{ $t('paragraph["bannerTitle"]') }}</span>
       </div>
+      <div class="banners__content-desc" v-html="$t(`paragraph['bannerDesc']`)"></div>
       <div class="banners__content-btn" @click="goRegister">
         <span>{{ $t('system["立即免费注册"]') }}</span>
       </div>
@@ -71,6 +67,7 @@ const goRegister = function () {
     font-size: 18px;
     font-size: 1.125rem;
     color: rgba(255, 255, 255, 0.8);
+    line-height: 2;
     transition: all 0.3s;
   }
 

+ 25 - 9
src/views/Home/modules/WrapFive/index.vue

@@ -14,8 +14,13 @@
             </div>
             <div class="swiper-slide__card-body">
               <div class="swiper-slide__card-avatar">
-                <img src="@/public/images/avatar01.png" alt="" />
-                <img src="@/public/images/avatar02.png" alt="" />
+                <img src="@/public/images/avatar_1.jpg" alt="" v-if="i == 0" />
+                <img src="@/public/images/avatar_3.jpg" alt="" v-if="i == 1" />
+                <img src="@/public/images/avatar_4.jpg" alt="" v-if="i == 2" />
+                <img src="../../../../public/images/logo_icon.png" />
+                <!-- <img src="../../../../public/images/icons/xinxilan_logo_icon.png" v-if="i == 0" />
+                <img src="../../../../public/images/icons/aodaliya_logo_icon.png" v-if="i == 1" />
+                <img src="../../../../public/images/icons/yidali_logo_icon.png" v-if="i == 2" /> -->
               </div>
               <div>
                 <div class="swiper-slide__card-name">{{ $t(`system["${item.name}"]`) }}</div>
@@ -52,21 +57,22 @@ onMounted(() => {
 
 const dataList = [
   {
-    name: '囧二',
+    name: 'Lawrence',
     address: '新西兰',
-    avatar: [],
+    avatar: ['../../../../public/images/icons/xinxilan_logo_icon.png'],
     content: '如果你想买其他东西,没有比 Vava buy 更好的了',
+    locale: '',
   },
   {
-    name: '囧二',
+    name: 'John',
     address: '澳大利亚',
-    avatar: [],
-    content: '如果你想买其他东西,没有比 Vava buy 更好的了',
+    avatar: ['../../../../public/images/icons/aodaliya_logo_icon.png'],
+    content: '太棒了!一切都很顺利,接收订单并快速发货的速度让人印象深刻',
   },
   {
-    name: '囧二',
+    name: 'Anna',
     address: '意大利',
-    avatar: [],
+    avatar: ['../../../../public/images/icons/yidali_logo_icon.png'],
     content: '非常易于使用,包装很精美,会收获很多惊喜',
   },
 ]
@@ -172,10 +178,20 @@ const dataList = [
     }
 
     .swiper-slide__card-avatar img {
+      width: 56px;
+      width: 3.5rem;
+      height: 56px;
+      height: 3.5rem;
+      border-radius: 50%;
       position: relative;
       z-index: 1;
     }
 
+    .swiper-slide__card-avatar img:nth-child(1) {
+      box-sizing: content-box;
+      border: 3px solid #fff;
+    }
+
     .swiper-slide__card-avatar img:nth-child(2) {
       position: absolute;
       left: 30px;

+ 20 - 2
src/views/Home/modules/WrapTwo/index.vue

@@ -8,7 +8,12 @@
       </div>
       <div class="wrap__two-content">
         <div class="wrap__two-steps">
-          <div v-for="(step, i) in stepList" :key="i" class="wrap__two-step-item">
+          <div
+            v-for="(step, i) in stepList"
+            :key="i"
+            class="wrap__two-step-item"
+            :class="[locale == 'en' ? 'wrap__two-step-item__en' : '']"
+          >
             <img
               v-if="i == 0"
               src="@/public/images/icons/address_icon.png"
@@ -43,12 +48,17 @@
 
 <script setup lang="ts">
 import { reactive } from 'vue'
+import { storeToRefs } from 'pinia'
+import useSystemStore from '@/stores/useSystemStore'
 
 interface IStep {
   name: string
   desc: string
 }
 
+const systemStore = useSystemStore()
+const { locale } = storeToRefs(systemStore)
+
 const stepList = reactive<IStep[]>([
   {
     name: '注册账户',
@@ -117,11 +127,19 @@ const goRegister = function () {
     flex-direction: column;
     align-items: center;
     padding: 0 15px;
-    padding: 0 1.875rem;
+    padding: 0 1rem;
     color: #fff;
     text-align: center;
   }
 
+  .wrap__two-step-item__en {
+    flex: 3;
+  }
+
+  .wrap__two-step-item__en:nth-child(2) {
+    flex: 4;
+  }
+
   .wrap__two-step-icon {
     height: 58px;
     height: 3.625rem;

+ 2 - 0
src/views/Layout/index.vue

@@ -42,6 +42,8 @@ import Footer from './modules/Footer/index.vue'
 
   .layout__main {
     flex: 1;
+    display: flex;
+    flex-direction: column;
     width: 100%;
     height: auto;
   }

+ 0 - 102
src/views/Layout/modules/Footer/hooks/useLink.ts

@@ -1,102 +0,0 @@
-import { reactive, ref, watch } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-
-
-export interface ILink {
-  text: string;
-  path?: string;
-  id?: string | number;
-  url?: string;
-}
-
-
-function useLink() {
-
-
-  const linkList1 = reactive<ILink[]>([
-    { path: '/article/article_details', id: '123', text: 'VAVA BUY 是如何工作的' },
-    { path: '/article/article_details', id: '456', text: '为什么选择 VAVA BUY' },
-    { path: '/article/article_details', id: '3', text: '您所在国家/地区的运费和价格' },
-    { path: '/article/article_details', id: '4', text: '报名' },
-    { path: '/article/article_details', id: '5', text: '汇率' },
-    { path: '/article/article_details', id: '6', text: '不能寄送的物品' },
-    { path: '/faq', id: '', text: '常见问题' },
-    { path: '/article/article_details', id: '8', text: '日志' },
-    { path: '/article/article_details', id: '9', text: '您的隐私权利' },
-  ])
-
-  const linkList2 = reactive<ILink[]>([
-    { path: '/article/article_details', id: '10', text: '关于 VAVA BUY' },
-    { path: '/article/article_details', id: '11', text: 'VAVA BUY 评论' },
-    { path: '/article/article_details', id: '12', text: '新闻与动态' },
-    { path: '/article/article_details', id: '13', text: 'VAVA BUY.com 的招聘信息' },
-    { path: '/article/article_details', id: 'abcdefg', text: '与 VAVA BUY 合作' },
-    { path: '/article/article_details', id: '15', text: '联系我们' },
-    { path: '/article/article_details', id: '16', text: '网站地图' },
-    { path: '/article/article_details', id: '17', text: '奖学金' },
-    { path: '/article/article_details', id: '18', text: '顶级商店' },
-  ])
-
-  const linkList3 = reactive<ILink[]>([
-    { path: '/article/article_details', id: '19', text: 'Facebook' },
-    { path: '/article/article_details', id: '20', text: '社交平台' },
-    { path: '/article/article_details', id: '21', text: 'X(原 Twitter)' },
-    { url: 'https://www.baidu.com', text: '获取 VAVA BUY 应用程序' },
-  ])
-
-
-  const route = useRoute()
-  const router = useRouter()
-  const currentPath = ref<string>('')
-  const currentId = ref<string | number | null>(null)
-
-
-  // 获取最新路由信息(路径&id)
-  watch(
-    () => route.fullPath,
-    (newVal, oldVal) => {
-      console.log('route', route)
-      currentPath.value = route.path
-      currentId.value = !!route.query.id ? (route.query.id as (string | number)) : null
-    },
-    { immediate: true }
-  )
-
-
-  // 跳转回调
-  const handleTextLinkClick = function (linkInfo: ILink, id?: string | number) {
-
-    if (!linkInfo || (!linkInfo.path && !linkInfo.url)) return
-
-    if (linkInfo.url) {
-      window.open(linkInfo.url, '_blank')
-    }
-    else if (linkInfo.id) {
-      router.push({
-        path: linkInfo.path,
-        query: {
-          id: linkInfo.id ? linkInfo.id : ''
-        }
-      })
-    }
-    else {
-      router.push({ path: linkInfo.path })
-    }
-  }
-
-  return {
-    linkList1,
-    linkList2,
-    linkList3,
-    route,
-    router,
-    currentPath,
-    currentId,
-    handleTextLinkClick
-  }
-}
-
-
-
-
-export default useLink

+ 112 - 82
src/views/Layout/modules/Footer/index.vue

@@ -6,69 +6,53 @@
           <img src="@/public/images/logo.png" class="footer-friendlink__logo" />
           <div class="footer-friendlink__baseinfo">
             <div
-              v-for="(item, i) in baseInfoList"
-              :key="i"
               class="footer-friendlink__baseinfo-item"
+              style="font-size: 18px; font-size: 1.125rem; margin-bottom: 10px"
             >
-              <span class="footer-friendlink__baseinfo-label">{{ item.label }}</span>
-              <span class="footer-friendlink__baseinfo-content">{{ item.content }}</span>
+              <span class="footer-friendlink__baseinfo-label">{{ $t('system.companyName') }} </span>
+            </div>
+            <div class="footer-friendlink__baseinfo-item">
+              <span class="footer-friendlink__baseinfo-label">{{ $t('system.emailLabel') }}: </span>
+              <span class="footer-friendlink__baseinfo-content">vavabuy@163.com</span>
+            </div>
+            <div class="footer-friendlink__baseinfo-item">
+              <span class="footer-friendlink__baseinfo-label"
+                >{{ $t('system.hongkongEmailLabel') }}:
+              </span>
+              <span class="footer-friendlink__baseinfo-content">296635202@qq.com </span>
+            </div>
+            <div class="footer-friendlink__baseinfo-item">
+              <span class="footer-friendlink__baseinfo-label"
+                >{{ $t('system.addressLabel') }}:
+              </span>
+              <span class="footer-friendlink__baseinfo-content"
+                >{{ $t('system.companyAddress') }}
+              </span>
+            </div>
+            <div class="footer-friendlink__baseinfo-item">
+              <span class="footer-friendlink__baseinfo-label"
+                >{{ $t('system.hongkongAddressLabel') }}:
+              </span>
+              <span class="footer-friendlink__baseinfo-content"
+                >{{ $t('system.hongkongCompanyAddress') }}
+              </span>
             </div>
           </div>
         </div>
-        <div class="footer-friendlink__box">
-          <div
-            v-for="(link, i) in linkList1"
-            :key="i"
-            class="footer-friendlink__item"
-            @click="handleTextLinkClick(link, link.id ? link.id : '')"
-          >
-            <span
-              class="text_hover_underline"
-              :class="[
-                (currentId && currentId == link.id && currentPath == link.path) ||
-                (!currentId && currentPath == link.path)
-                  ? 'text_hover_underline--active'
-                  : '',
-              ]"
-              >{{ $t(`textLink["${link.text}"]`) }}</span
-            >
-          </div>
-        </div>
-        <div class="footer-friendlink__box">
-          <div
-            v-for="(link, i) in linkList2"
-            :key="i"
-            class="footer-friendlink__item"
-            @click="handleTextLinkClick(link, link.id ? link.id : '')"
-          >
-            <span
-              class="text_hover_underline"
-              :class="[
-                (currentId && currentId == link.id && currentPath == link.path) ||
-                (!currentId && currentPath == link.path)
-                  ? 'text_hover_underline--active'
-                  : '',
-              ]"
-              >{{ $t(`textLink["${link.text}"]`) }}</span
-            >
-          </div>
-        </div>
-        <div class="footer-friendlink__box">
+        <div class="footer-friendlink__box" v-for="(itemInfo, i) in [...linkList]" :key="i">
+          <div class="footer-friendlink__box-head">{{ itemInfo.name }}</div>
           <div
-            v-for="(link, i) in linkList3"
-            :key="i"
+            v-for="(info, index) in itemInfo.lists"
+            :key="index"
             class="footer-friendlink__item"
-            @click="handleTextLinkClick(link, link.id ? link.id : '')"
+            @click="updateActiveInfo(itemInfo, info, goArticleDetailPage)"
           >
             <span
               class="text_hover_underline"
               :class="[
-                (currentId && currentId == link.id && currentPath == link.path) ||
-                (!currentId && currentPath == link.path)
-                  ? 'text_hover_underline--active'
-                  : '',
+                activeLinkId && activeLinkId == info.id ? 'text_hover_underline--active' : '',
               ]"
-              >{{ $t(`textLink["${link.text}"]`) }}</span
+              >{{ info.title }}</span
             >
           </div>
         </div>
@@ -95,27 +79,55 @@
 
 
 <script setup lang="ts">
-import useLink from './hooks/useLink'
-
-const {
-  linkList1,
-  linkList2,
-  linkList3,
-  route,
-  router,
-  currentPath,
-  currentId,
-  handleTextLinkClick,
-} = useLink()
-
-const baseInfoList = [
-  { label: '', content: '4299 Express Lane' },
-  { label: '', content: 'Sarasota,FL 34249 USA' },
-  { label: 'WhatsApp Message: ', content: '1.941.225.7374' },
-  { label: 'Phone: ', content: '1.941.227.4444' },
-  { label: 'Fax: ', content: '1.941.827.2985' },
-  { label: 'Local Time: ', content: '04:19' },
-]
+import { watch, computed } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { storeToRefs } from 'pinia'
+import useSystemStore from '@/stores/useSystemStore'
+import useFooterStore from '@/stores/useFooterStore'
+
+// system store
+const systemStore = useSystemStore()
+const { locale, screenWidth } = storeToRefs(systemStore)
+// footer store
+const footerStore = useFooterStore()
+const { linkList, activeLinkId, activeLinkInfo, activeLinkCateInfo } = storeToRefs(footerStore)
+const { fetchHelpDatas, updateActiveInfo } = footerStore
+// 路由对象
+const route = useRoute()
+const router = useRouter()
+
+const linkListCount = computed<number>(() => {
+  let count = 4
+  if (screenWidth.value >= 1200) {
+    count = 4
+  } else if (screenWidth.value > 824) {
+    count = 3
+  } else if (screenWidth.value > 524) {
+    count = 2
+  } else {
+    count = 1
+  }
+  return count
+})
+
+// 获取数据列表
+fetchHelpDatas(locale.value)
+
+// 跳转文章详情页
+const goArticleDetailPage = function () {
+  if (!activeLinkInfo.value && !activeLinkId.value) return
+  router.push({
+    path: '/article/article_details',
+    query: {
+      id: activeLinkId.value
+        ? activeLinkId.value
+        : activeLinkInfo.value
+        ? activeLinkInfo.value.id
+        : '',
+      cateId: activeLinkCateInfo.value ? activeLinkCateInfo.value.id : '',
+    },
+  })
+}
 </script>
 
 
@@ -136,7 +148,9 @@ const baseInfoList = [
   }
 
   .footer-friendlink__box {
-    width: auto;
+    // width: 25%;
+    max-width: 360px;
+    max-width: 22.5rem;
     padding: 6px 10px 10px;
     padding: 0.375rem 0.625rem 0.625rem;
   }
@@ -148,13 +162,27 @@ const baseInfoList = [
   .footer-friendlink__box .footer-friendlink__logo {
     width: 150px;
     width: 9.375rem;
-    margin-bottom: 36px;
-    margin-bottom: 2.25rem;
+    margin-bottom: 20px;
+    margin-bottom: 1.25rem;
+  }
+
+  .footer-friendlink__box .footer-friendlink__box-head {
+    margin-bottom: 24px;
+    margin-bottom: 1.5rem;
+    font-size: 18px;
+    font-size: 1.125rem;
+    font-weight: 500;
+    color: #222;
+  }
+
+  .footer-friendlink__baseinfo {
+    max-width: 360px;
+    max-width: 22.5rem;
   }
 
   .footer-friendlink__baseinfo-item {
-    margin-bottom: 4px;
-    margin-bottom: 0.25rem;
+    margin-bottom: 6px;
+    margin-bottom: 0.375rem;
     font-family: Poppins;
     font-size: 14px;
     font-size: 0.875rem;
@@ -162,10 +190,12 @@ const baseInfoList = [
     color: #3d3d3d;
   }
 
-  .footer-friendlink__baseinfo-item:last-child {
-    margin-bottom: 0;
-    margin-top: 20px;
-    margin-top: 1.25rem;
+  .footer-friendlink__baseinfo-label {
+    color: #000;
+  }
+
+  .footer-friendlink__baseinfo-content {
+    color: #555;
   }
 
   .footer-friendlink__item {
@@ -206,4 +236,4 @@ const baseInfoList = [
 @import url(./css/footer@1024.less);
 @import url(./css/footer@768.less);
 @import url(./css/footer@512.less);
-</style>
+</style>./hooks/useData

+ 7 - 6
src/views/Layout/modules/Header/hooks/useLinkList.ts

@@ -6,8 +6,8 @@ import { useRoute, useRouter } from 'vue-router'
 
 export interface ITextLink {
   text: string;
-  path?: string;
   id?: string | number;
+  path?: string;
   url?: string;
 }
 
@@ -18,7 +18,7 @@ function useLinkList() {
     {
       text: '与 VAVA BUY 合作',
       path: '/article/article_details',
-      id: 'abcdefg',
+      id: 'connect',
     },
   ])
 
@@ -26,17 +26,17 @@ function useLinkList() {
     {
       text: '如何运作',
       path: '/article/article_details',
-      id: '123',
+      id: 'works',
     },
     {
       text: '为什么选择 VAVA BUY',
       path: '/article/article_details',
-      id: '456',
+      id: 'about',
     },
     {
       text: '优势',
       path: '/article/article_details',
-      id: '789',
+      id: 'superiority',
     },
     {
       text: '在 VAVA BUY 购物',
@@ -73,7 +73,8 @@ function useLinkList() {
       router.push({
         path: linkInfo.path,
         query: {
-          id: linkInfo.id ? linkInfo.id : ''
+          id: linkInfo.id ? linkInfo.id : '',
+          type: 'header'
         }
       })
     }

+ 8 - 0
src/views/Layout/modules/Header/index.vue

@@ -160,6 +160,14 @@ const { textLinkList, textLinkList2, route, router, currentPath, currentId, hand
 const onLocaleChange = function (info: ILocale) {
   systemStore.toggleLocale(info)
   i18n.locale.value = info.value
+  const historyLength = window.history.length
+  if (historyLength > 1) {
+    window.history.go(-historyLength + 2)
+  }
+  router.replace('/')
+  setTimeout(() => {
+    window.location.reload()
+  })
 }
 
 // 跳转首页回调

+ 4 - 0
src/views/Layout/modules/Main/index.vue

@@ -11,4 +11,8 @@ import { RouterView } from 'vue-router'
 
 
 <style lang="less" scoped>
+#main__container {
+  position: relative;
+  flex: 1;
+}
 </style>

+ 4 - 0
vite.config.ts

@@ -15,4 +15,8 @@ export default defineConfig({
       '@': fileURLToPath(new URL('./src', import.meta.url))
     },
   },
+  server: {
+    port: 4000,
+    host: '0.0.0.0'
+  }
 })