13 次代碼提交 50e54f2e86 ... 6c3d34386d

作者 SHA1 備註 提交日期
  liangan 6c3d34386d Merge branch 'uat' 3 周之前
  liangan 3f8d86bc38 Merge branch 'dev' into uat 3 周之前
  liangan 9d7f242a4e fix: 帮助中心页面样式调整 3 周之前
  liangan 51b295d770 Merge branch 'dev' into uat 3 周之前
  liangan 5a077f1610 fix: 修改图标 3 周之前
  liangan 455d506ea6 Merge branch 'dev' into uat 3 周之前
  liangan 5c92e65886 fix: 修改图标 3 周之前
  liangan 61d69e18dd fix: pages.json修改 3 周之前
  liangan 23c4d6513d fix: pages.json修改 3 周之前
  liangan 19896f9ff5 Merge branch 'dev' into uat 3 周之前
  liangan 285ab7a1fa fix: 修改悬浮按钮样式 3 周之前
  liangan 8e70a0c206 fix: 修改悬浮按钮样式 3 周之前
  liangan 44c2651b2f feat: 帮助中心页面开发联调 3 周之前

+ 18 - 0
pages.config.ts

@@ -125,6 +125,24 @@ export default defineUniPages({
         navigationBarBackgroundColor: '#fff',
       },
     },
+    {
+      path: 'pages/mine/helpCenter',
+      type: 'page',
+      layout: 'default',
+      style: {
+        navigationBarTitleText: '%helpCenter.title%',
+        navigationBarBackgroundColor: '#fff',
+      },
+    },
+    {
+      path: 'pages/mine/helpCenterDetail',
+      type: 'page',
+      layout: 'default',
+      style: {
+        navigationBarTitleText: '%helpCenter.detail.title%',
+        navigationBarBackgroundColor: '#fff',
+      },
+    },
     {
       path: 'pages/mine/mine',
       type: 'page',

+ 15 - 0
src/api/qa.ts

@@ -0,0 +1,15 @@
+import { http } from '@/utils/http'
+
+const pre = import.meta.env.VITE_API_SECONDARY_URL_PREFIX
+
+export function qaGroupList() {
+  return http.get<any>(`${pre}/api/qa/groupList`)
+}
+
+export function qaList(data: any) {
+  return http.get<any>(`${pre}/api/qa/qaList`, data)
+}
+
+export function qaInfo(id: number) {
+  return http.get<any>(`${pre}/api/qa/qaInfo/${id}`)
+}

+ 43 - 120
src/components/CustomerServiceFab.vue

@@ -1,6 +1,6 @@
 <script lang="ts" setup>
-import { onShow } from '@dcloudio/uni-app'
 import { getConfigByCode } from '@/api/common'
+import { toPage } from '@/utils/page'
 import { openH5WhatsApp } from '@/utils/social'
 import { toast } from '@/utils/toast'
 
@@ -8,10 +8,13 @@ const props = defineProps<{
   bottomOffset?: number
 }>()
 
-const STORAGE_KEY = 'customer_service_fab_position_v1'
+const active = ref<boolean>(false)
 
-const x = ref<number>(0)
-const y = ref<number>(0)
+const TABBAR_PAGES = [
+  'pages/index/index',
+  'pages/income/income',
+  'pages/mine/mine',
+]
 
 function rpxToPx(rpx: number) {
   const sys = uni.getSystemInfoSync()
@@ -19,89 +22,33 @@ function rpxToPx(rpx: number) {
   return (rpx * windowWidth) / 750
 }
 
-function getBottomLimitPx() {
-  const sys = uni.getSystemInfoSync() as any
-  const bottomOffsetPx = rpxToPx(props.bottomOffset ?? 0)
-  const safeBottomPx = Number(sys?.safeAreaInsets?.bottom) || 0
-  return bottomOffsetPx + safeBottomPx
-}
-
-function initDefaultPosition() {
-  const sys = uni.getSystemInfoSync()
-  const windowWidth = sys.windowWidth || 375
-  const windowHeight = sys.windowHeight || 667
-  const sizePx = rpxToPx(80)
-  const rightInsetPx = rpxToPx(24)
-  const bottomInsetPx = rpxToPx(-86)
-  const bottomOffsetPx = getBottomLimitPx()
-  x.value = Math.max(0, windowWidth - sizePx - rightInsetPx)
-  y.value = Math.max(0, windowHeight - sizePx - bottomOffsetPx - bottomInsetPx)
-}
-
-function clampPosition() {
-  const sys = uni.getSystemInfoSync()
-  const windowWidth = sys.windowWidth || 375
-  const windowHeight = sys.windowHeight || 667
-  const sizePx = rpxToPx(80)
-  const bottomOffsetPx = getBottomLimitPx()
-  const maxX = Math.max(0, windowWidth - sizePx)
-  const maxY = Math.max(0, windowHeight - sizePx - bottomOffsetPx)
-  x.value = Math.min(Math.max(0, x.value), maxX)
-  y.value = Math.min(Math.max(0, y.value), maxY)
-}
-
-function restorePosition() {
-  try {
-    const raw = uni.getStorageSync(STORAGE_KEY)
-    if (!raw) {
-      initDefaultPosition()
-      return
-    }
-    const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw
-    const nx = Number(parsed?.x)
-    const ny = Number(parsed?.y)
-    if (Number.isFinite(nx) && Number.isFinite(ny)) {
-      x.value = nx
-      y.value = ny
-      clampPosition()
-      return
-    }
-    initDefaultPosition()
-  }
-  catch {
-    initDefaultPosition()
-  }
+function getCurrentRoutePath() {
+  const pages = getCurrentPages() as any[]
+  const last = pages?.[pages.length - 1]
+  const route = String(last?.route || '')
+  return route.replace(/^\//, '')
 }
 
-function persistPosition() {
-  try {
-    uni.setStorageSync(STORAGE_KEY, JSON.stringify({ x: x.value, y: y.value }))
-  }
-  catch {
-  }
+function isTabbarPage() {
+  const route = getCurrentRoutePath()
+  return TABBAR_PAGES.includes(route)
 }
 
-function persistPositionClamped() {
-  clampPosition()
-  persistPosition()
+function getBottomOffsetPx() {
+  const sys = uni.getSystemInfoSync() as any
+  const extraOffsetPx = rpxToPx(props.bottomOffset ?? 0)
+  const safeBottomPx = Number(sys?.safeAreaInsets?.bottom) || 0
+  const tabbarHeightPx = isTabbarPage() ? 50 : 0
+  return extraOffsetPx + safeBottomPx + tabbarHeightPx
 }
 
-onShow(() => {
-  restorePosition()
+const fabCustomStyle = computed(() => {
+  const bottomPx = getBottomOffsetPx()
+  return `bottom: ${bottomPx}px;`
 })
 
-function handleMoveChange(e: any) {
-  const detail = e?.detail
-  const nx = Number(detail?.x)
-  const ny = Number(detail?.y)
-  if (Number.isFinite(nx))
-    x.value = nx
-  if (Number.isFinite(ny))
-    y.value = ny
-  clampPosition()
-}
-
 async function openCustomerService() {
+  active.value = false
   try {
     const res = await getConfigByCode({ code: 'live_chat' })
     const value = res?.data?.valueInfo
@@ -115,54 +62,30 @@ async function openCustomerService() {
     toast.info('客服暂不可用')
   }
 }
+
+function openHelpCenter() {
+  active.value = false
+  toPage({ url: '/pages/mine/helpCenter' })
+}
 </script>
 
 <template>
-  <movable-area class="customer-fab-area">
-    <movable-view
-      :x="x"
-      :y="y"
-      direction="all"
-      :inertia="false"
-      :animation="false"
-      :out-of-bounds="false"
-      class="customer-fab-movable"
-      @change="handleMoveChange"
-      @touchend="persistPositionClamped"
-      @touchcancel="persistPositionClamped"
-    >
-      <image
-        src="/static/icons/whatsapp.png"
-        class="customer-fab-trigger__icon"
-        mode="heightFix"
-        @click="openCustomerService"
-      />
-    </movable-view>
-  </movable-area>
+  <wd-fab v-model:active="active" position="right-bottom" :z-index="999" direction="top" :custom-style="fabCustomStyle" :expandable="false" :draggable="true">
+    <template #trigger>
+      <view class="w-68rpx">
+        <image src="/static/icons/whatsapp-fb.png" class="customer-fab-action__icon mb-12rpx" mode="heightFix" @click="openCustomerService" />
+        <image src="/static/icons/help-fb.png" class="customer-fab-action__icon" mode="heightFix" @click="openHelpCenter" />
+      </view>
+    </template>
+  </wd-fab>
 </template>
 
 <style lang="scss" scoped>
-.customer-fab-area {
-  position: fixed;
-  left: 0;
-  top: 0;
-  width: 100vw;
-  height: 100vh;
-  pointer-events: none;
-  z-index: 999;
-}
-
-.customer-fab-movable {
-  pointer-events: auto;
-  transition: none;
-  width: 80rpx;
-  height: 80rpx;
-}
-
-.customer-fab-trigger__icon {
-  box-shadow: 0 12rpx 30rpx rgba(0, 0, 0, 0.16);
-  height: 80rpx;
-  width: 80rpx;
+.customer-fab-action__icon {
+  width: 68rpx;
+  height: 68rpx;
+  box-sizing: border-box;
   border-radius: 50%;
+  background-color: #fff;
 }
 </style>

+ 1 - 1
src/layouts/tabbar.vue

@@ -15,7 +15,7 @@ const themeVars: ConfigProviderThemeVars = {
   <wd-config-provider :theme-vars="themeVars">
     <slot />
     <!-- #ifdef H5 -->
-    <CustomerServiceFab :bottom-offset="180" />
+    <CustomerServiceFab :bottom-offset="120" />
     <!-- #endif -->
     <FgTabbar />
     <wd-toast />

+ 3 - 0
src/locale/bn.json

@@ -128,8 +128,11 @@
   "mine.menu.address": "ঠিকানা বই",
   "mine.menu.share": "শেয়ার",
   "mine.menu.favorite": "আমার পছন্দ",
+  "mine.menu.helpCenter": "সাহায্য কেন্দ্র",
   "mine.menu.chat": "লাইভ চ্যাট",
   "mine.menu.activity": "কার্যকলাপ গ্রুপ",
+  "helpCenter.title": "সাহায্য কেন্দ্র",
+  "helpCenter.detail.title": "সাহায্য কেন্দ্র",
   "home.priceTab.allPrice": "সব দাম",
   "home.priceTab.300spot": "৩০০স্পট",
   "home.priceTab.500spot": "৫০০স্পট",

+ 3 - 0
src/locale/en.json

@@ -140,8 +140,11 @@
   "mine.menu.address": "Address Book",
   "mine.menu.share": "Share",
   "mine.menu.favorite": "My Favorite",
+  "mine.menu.helpCenter": "Help Center",
   "mine.menu.chat": "Live Chat",
   "mine.menu.activity": "Activity Group",
+  "helpCenter.title": "Help Center",
+  "helpCenter.detail.title": "Help Center",
   "income.totalEarnings": "Total Earnings",
   "income.accountBalance": "Revenue Account Balance",
   "income.settledAmount": "Settled Amount",

+ 3 - 0
src/locale/zh-Hans.json

@@ -128,8 +128,11 @@
   "mine.menu.address": "地址簿",
   "mine.menu.share": "分享",
   "mine.menu.favorite": "我的收藏",
+  "mine.menu.helpCenter": "帮助中心",
   "mine.menu.chat": "在线客服",
   "mine.menu.activity": "活动群组",
+  "helpCenter.title": "帮助中心",
+  "helpCenter.detail.title": "帮助中心",
   "home.priceTab.allPrice": "全部价格",
   "home.priceTab.300spot": "300积分",
   "home.priceTab.500spot": "500积分",

+ 18 - 0
src/pages.json

@@ -113,6 +113,24 @@
         "navigationBarBackgroundColor": "#fff"
       }
     },
+    {
+      "path": "pages/mine/helpCenter",
+      "type": "page",
+      "layout": "default",
+      "style": {
+        "navigationBarTitleText": "%helpCenter.title%",
+        "navigationBarBackgroundColor": "#fff"
+      }
+    },
+    {
+      "path": "pages/mine/helpCenterDetail",
+      "type": "page",
+      "layout": "default",
+      "style": {
+        "navigationBarTitleText": "%helpCenter.detail.title%",
+        "navigationBarBackgroundColor": "#fff"
+      }
+    },
     {
       "path": "pages/mine/mine",
       "type": "page",

+ 126 - 0
src/pages/mine/helpCenter.vue

@@ -0,0 +1,126 @@
+<route lang="json5" type="page">
+{
+  layout: 'default',
+  style: {
+    navigationBarTitleText: '%helpCenter.title%',
+    navigationBarBackgroundColor: '#fff',
+  },
+}
+</route>
+
+<script lang="ts" setup>
+// eslint-disable-next-line unused-imports/no-unused-imports
+import { onPageScroll } from '@dcloudio/uni-app'
+import useZPaging from 'z-paging/components/z-paging/js/hooks/useZPaging.js'
+
+import { qaGroupList, qaList } from '@/api/qa'
+import i18n from '@/locale/index'
+import { toPage } from '@/utils/page'
+
+defineOptions({
+  name: 'HelpCenter',
+})
+
+const systemInfo = uni.getSystemInfoSync()
+const safeAreaInsets = systemInfo.safeAreaInsets
+
+const paging = ref<any>(null)
+useZPaging(paging)
+
+const dataList = ref<any[]>([])
+
+const groupList = ref<any[]>([])
+const tab = ref<string>('')
+
+const locale = computed(() => i18n.global.locale)
+
+const shouldUseEn = computed(() => locale.value !== 'bn')
+
+watch(locale, () => {
+  paging.value?.reload()
+})
+
+function getGroupName(item: any) {
+  return shouldUseEn.value ? (item.enName || '') : (item.bnName || '')
+}
+
+function getQaTitle(item: any) {
+  return shouldUseEn.value ? (item.enTitle || '') : (item.bnTitle || '')
+}
+
+async function loadGroups() {
+  const res = await qaGroupList()
+  if (res.code === '200') {
+    const list = (res.data || []) as any[]
+    groupList.value = list
+    if (!tab.value && list.length)
+      tab.value = String(list[0].id)
+  }
+}
+
+async function queryList(pageNo: number, pageSize: number) {
+  if (!tab.value) {
+    paging.value?.complete([])
+    return
+  }
+  try {
+    const res = await qaList({
+      page: pageNo,
+      size: pageSize,
+      groupId: Number(tab.value),
+    })
+    if (res.code === '200') {
+      paging.value.complete(res.data?.list || [])
+    }
+    else {
+      paging.value.complete(false)
+    }
+  }
+  catch {
+    paging.value.complete(false)
+  }
+}
+function handleTabClick() {
+  paging.value?.reload()
+}
+
+function handleItemClick(item: any) {
+  toPage({ url: '/pages/mine/helpCenterDetail', params: { id: item.id } })
+}
+
+onShow(async () => {
+  if (!groupList.value.length) {
+    await loadGroups()
+  }
+  else {
+    paging.value?.reload()
+  }
+})
+</script>
+
+<template>
+  <z-paging v-if="tab" ref="paging" v-model="dataList" :show-loading-more-no-more-view="false" use-page-scroll @query="queryList">
+    <template #top>
+      <view class="bg-white" :style="{ paddingTop: `${safeAreaInsets?.top}px` }">
+        <wd-tabs v-if="groupList.length && tab" v-model="tab" :auto-line-width="true" custom-class="bg-transparent!" slidable="always" @click="handleTabClick">
+          <wd-tab v-for="g in groupList" :key="g.id" :name="String(g.id)" :title="getGroupName(g)" />
+        </wd-tabs>
+      </view>
+    </template>
+
+    <view class="pt-20rpx">
+      <view v-for="item in dataList" :key="item.id" class="mb-20rpx bg-white px-22rpx py-20rpx" @click="handleItemClick(item)">
+        <view class="flex items-center justify-between">
+          <view class="flex-1 pr-16rpx text-28rpx line-height-[2]">
+            {{ getQaTitle(item) }}
+          </view>
+          <wd-icon name="chevron-right" class="opacity-50" size="28rpx" />
+        </view>
+      </view>
+    </view>
+  </z-paging>
+</template>
+
+<style lang="scss" scoped>
+
+</style>

+ 89 - 0
src/pages/mine/helpCenterDetail.vue

@@ -0,0 +1,89 @@
+<route lang="json5" type="page">
+{
+  layout: 'default',
+  style: {
+    navigationBarTitleText: '%helpCenter.detail.title%',
+    navigationBarBackgroundColor: '#fff',
+  },
+}
+</route>
+
+<script lang="ts" setup>
+import { onLoad } from '@dcloudio/uni-app'
+
+import { qaInfo } from '@/api/qa'
+import i18n from '@/locale/index'
+
+defineOptions({
+  name: 'HelpCenterDetail',
+})
+
+interface QaItem {
+  id: number
+  bnTitle?: string
+  enTitle?: string
+  bnContent?: string
+  enContent?: string
+  groupId: number
+}
+
+const locale = computed(() => i18n.global.locale)
+const shouldUseEn = computed(() => locale.value === 'en' || locale.value === 'zh-Hans')
+
+const detail = ref<QaItem | null>(null)
+const contentHtml = ref<string>('')
+
+function formatTextToHtml(text: string) {
+  if (!text)
+    return ''
+  return text.replace(/\n/g, '<br>')
+}
+
+function getQaTitle(item: QaItem | null) {
+  if (!item)
+    return ''
+  return shouldUseEn.value ? (item.enTitle || item.bnTitle || '') : (item.bnTitle || item.enTitle || '')
+}
+
+async function loadDetail(id: number) {
+  const res = await qaInfo(id)
+  if (res.code === '200') {
+    detail.value = res.data as QaItem
+    const content = shouldUseEn.value
+      ? (detail.value.enContent || detail.value.bnContent || '')
+      : (detail.value.bnContent || detail.value.enContent || '')
+    contentHtml.value = formatTextToHtml(content)
+  }
+}
+
+watch(locale, () => {
+  if (!detail.value)
+    return
+  const content = shouldUseEn.value
+    ? (detail.value.enContent || detail.value.bnContent || '')
+    : (detail.value.bnContent || detail.value.enContent || '')
+  contentHtml.value = formatTextToHtml(content)
+})
+
+onLoad(async (options: any) => {
+  const id = Number(options?.id)
+  if (!Number.isFinite(id) || id <= 0)
+    return
+  await loadDetail(id)
+})
+</script>
+
+<template>
+  <view class="rounded-12rpx bg-white px-22rpx py-40rpx">
+    <view class="mb-18rpx text-30rpx font-bold">
+      {{ getQaTitle(detail) }}
+    </view>
+    <rich-text :nodes="contentHtml" />
+  </view>
+</template>
+
+<style lang="scss" scoped>
+page {
+  background-color: #fff;
+}
+</style>

+ 1 - 0
src/pages/mine/mine.vue

@@ -103,6 +103,7 @@ const menuList = computed(() => {
     { name: t('mine.menu.share'), url: '/pages/mine/share', icon: '/static/icons/share.png' },
     { name: t('mine.menu.favorite'), url: '/pages/mine/myFavorite', icon: '/static/icons/my-favorite.png' },
     { name: t('mine.menu.chat'), config: 'live_chat', icon: '/static/icons/live-chat.png' },
+    { name: t('mine.menu.helpCenter'), url: '/pages/mine/helpCenter', icon: '/static/icons/set-help.png' },
     // { name: t('mine.menu.activity'), config: 'activity_group', icon: '/static/icons/activity-group.png' },
   ]
 })

二進制
src/static/icons/help-fb.png


二進制
src/static/icons/set-help.png


二進制
src/static/icons/whatsapp-fb.png