3 Commits 4d21fa138e ... 37f853e646

Autor SHA1 Mensagem Data
  yuanmingze 37f853e646 Merge branch 'feature/caitianxia/261010' há 1 semana atrás
  yuanmingze 7790f3f145 完成咨询页面未登录状态游客接口 há 1 semana atrás
  yuanmingze db7f808a6a 完成多次 财天下签约税地 há 2 semanas atrás

+ 68 - 18
src/pages-auth/settlement-channel-sign/index.vue

@@ -105,7 +105,7 @@
 <script setup lang="ts">
 import { computed, ref } from 'vue'
 
-import { onLoad } from '@dcloudio/uni-app'
+import { onLoad, onShow } from '@dcloudio/uni-app'
 
 import {
   batchChannelCertApi,
@@ -200,7 +200,11 @@ const showSubmit = computed(() => {
 })
 
 const canSubmit = computed(() => {
-  return Boolean(selectedChannel.value?.selectable) && !isSubmitting.value
+  return (
+    Boolean(selectedChannel.value?.selectable) &&
+    !isSubmitting.value &&
+    !settlementSignStore.processingSignTraceId
+  )
 })
 
 const showToast = (title: string): void => {
@@ -239,7 +243,10 @@ const findCertItem = (subjectLocation: string): CertItem | undefined => {
 const isSelectableChannel = (certItem?: CertItem): boolean => {
   if (!certItem) return true
 
-  return SELECTABLE_CERT_STATUS.has(certItem.commonCertStatus ?? '')
+  return (
+    SELECTABLE_CERT_STATUS.has(certItem.commonCertStatus ?? '') ||
+    (certItem.commonCertStatus === 'SIGN' && certItem.needContinue === true)
+  )
 }
 
 const getCertStatusText = (certItem?: CertItem): string => {
@@ -438,24 +445,35 @@ const certFn = async (): Promise<void> => {
     return
   }
 
+  const userInfo = await userStore.getUserInfoByCode()
+  if (!userInfo?.userId || !userInfo.certToken) {
+    showToast('认证信息获取失败,请重试')
+    return
+  }
+
   const accountInfo = wx.getAccountInfoSync()
   const appId = accountInfo.miniProgram.appId
+  const context = settlementSignStore.createContext(subjectLocation)
 
   const signRedirectUrl = `/pages-auth/settlement-channel-sign/index?subjectLocationByUrl=${encodeURIComponent(
     subjectLocation
-  )}`
+  )}&signTraceId=${encodeURIComponent(context.signTraceId)}`
 
   const data = {
-    userId: currentUserInfo.value?.userId || '',
+    userId: userInfo.userId,
     subjectLocation,
-    token: currentUserInfo.value?.certToken || '',
+    token: userInfo.certToken,
     signRedirectUrl,
     wxMiniAppId: appId,
   }
 
-  const res = await batchChannelCertApi(data)
+  const res = await batchChannelCertApi(data).catch((error) => {
+    settlementSignStore.clearContext(context.signTraceId)
+    throw error
+  })
 
   if (res.code === 0 && res.data?.errMsg) {
+    settlementSignStore.clearContext(context.signTraceId)
     authErrorMessage.value = res.data.errMsg
     authErrorDialogVisible.value = true
     isSubmitting.value = false
@@ -463,6 +481,7 @@ const certFn = async (): Promise<void> => {
   }
 
   if (res.data.commonCertStatus === 'FAIL_SIGN') {
+    settlementSignStore.clearContext(context.signTraceId)
     showToast(res.msg || res.data.errMsg || '签约失败')
     return
   }
@@ -470,14 +489,16 @@ const certFn = async (): Promise<void> => {
   const h5SignUrl = res.data?.h5SignUrl
 
   if (h5SignUrl) {
-    const context = settlementSignStore.createContext(subjectLocation)
-
     uni.navigateTo({
       url: `/pages-auth/web-view/index?url=${encodeURIComponent(
         h5SignUrl
       )}&subjectLocation=${encodeURIComponent(
         subjectLocation
       )}&signTraceId=${encodeURIComponent(context.signTraceId)}`,
+      fail: () => {
+        settlementSignStore.clearContext(context.signTraceId)
+        showToast('签约页面打开失败,请重试')
+      },
     })
 
     return
@@ -486,37 +507,66 @@ const certFn = async (): Promise<void> => {
   await handleCertSuccess()
 }
 
-const signRedirect = async (subjectLocation: string): Promise<boolean> => {
+const signRedirect = async (subjectLocation: string, signTraceId = ''): Promise<boolean> => {
   if (!subjectLocation) {
     showToast('参数异常')
     return false
   }
+
+  if (!settlementSignStore.beginSignRedirect(subjectLocation, signTraceId)) return false
+
+  isSubmitting.value = true
+
   try {
-    await signRedirectApi({
+    await userStore.getUserInfoByCode()
+
+    const res = await signRedirectApi({
       subjectLocation,
     })
-    settlementSignStore.clearContext()
-    uni.showToast({
-      title: '签约成功',
-      icon: 'success',
-    })
+
+    if (res.data?.commonCertStatus === 'SIGN' && res.data.needContinue === true) {
+      selectedSubjectLocation.value = subjectLocation
+      await certFn()
+      return true
+    }
+
+    if (SELECTABLE_CERT_STATUS.has(res.data?.commonCertStatus ?? '')) {
+      settlementSignStore.clearContext()
+      await userStore.getUserInfoByCode()
+      showToast(res.data.commonCertStatus === 'FAIL_SIGN' ? '签约失败,请重试' : '签约尚未完成')
+      return false
+    }
+
+    await handleCertSuccess()
     return true
   } catch (error) {
     console.error('signRedirect error:', error)
     return false
+  } finally {
+    settlementSignStore.finishSignRedirect()
+    isSubmitting.value = false
   }
 }
 
 onLoad(async (query) => {
   const subjectLocation = query?.subjectLocationByUrl?.trim() || ''
+  const signTraceId = query?.signTraceId?.trim() || ''
 
   if (subjectLocation) {
-    await signRedirect(subjectLocation)
+    await signRedirect(subjectLocation, signTraceId)
+  } else {
+    await userStore.getUserInfoByCode()
   }
 
-  await userStore.getUserInfoByCode()
   await getSubjectLocationDict()
 })
+
+onShow(() => {
+  const context = settlementSignStore.returnedContext
+  if (!context) return
+
+  void signRedirect(context.subjectLocation, context.signTraceId)
+})
 </script>
 
 <style lang="scss" scoped>

+ 17 - 3
src/pages-auth/web-view/index.vue

@@ -7,7 +7,7 @@
 <script setup lang="ts">
 import { ref } from 'vue'
 
-import { onLoad } from '@dcloudio/uni-app'
+import { onLoad, onUnload } from '@dcloudio/uni-app'
 
 import { useSettlementSignStore } from '@/stores/modules/settlementSign'
 
@@ -36,6 +36,8 @@ const settlementSignStore = useSettlementSignStore()
 const url = ref('')
 const routeSubjectLocation = ref('')
 const routeSignTraceId = ref('')
+let isUnloaded = false
+let isRedirecting = false
 
 const showToast = (title: string) => {
   uni.showToast({
@@ -96,6 +98,8 @@ const resolveSignTraceId = (message: WebViewMessage): string => {
 }
 
 const redirectToSettlementSignPage = (subjectLocation: string, signTraceId: string) => {
+  if (isUnloaded || isRedirecting) return
+
   if (!subjectLocation) {
     showToast('参数异常')
     return
@@ -107,8 +111,12 @@ const redirectToSettlementSignPage = (subjectLocation: string, signTraceId: stri
     query.push(`signTraceId=${encodeURIComponent(signTraceId)}`)
   }
 
+  isRedirecting = true
   uni.redirectTo({
     url: `/pages-auth/settlement-channel-sign/index?${query.join('&')}`,
+    fail: () => {
+      isRedirecting = false
+    },
   })
 }
 
@@ -135,8 +143,14 @@ onLoad((query?: WebViewPageQuery) => {
   }
 
   url.value = safeDecodeURIComponent(query.url)
-  routeSubjectLocation.value = query.subjectLocation || ''
-  routeSignTraceId.value = query.signTraceId || ''
+  routeSubjectLocation.value = safeDecodeURIComponent(query.subjectLocation || '').trim()
+  routeSignTraceId.value = safeDecodeURIComponent(query.signTraceId || '').trim()
+})
+
+onUnload(() => {
+  isUnloaded = true
+  // 这里只记录返回,等签约页 onShow 后再请求,避免在页面退出过程中继续跳转。
+  settlementSignStore.markReturned(routeSubjectLocation.value, routeSignTraceId.value)
 })
 </script>
 

+ 2 - 0
src/pages/information/components/academic/AcademicArticleItem.vue

@@ -18,6 +18,7 @@
       </view>
 
       <button
+        v-if="shareEnabled"
         class="share-button"
         hover-class="share-button--hover"
         @tap.stop="emit('share', item)"
@@ -43,6 +44,7 @@ import type { ArticleItem } from '@/services/modules/information/type'
 
 const props = defineProps<{
   item: ArticleItem
+  shareEnabled: boolean
 }>()
 
 const emit = defineEmits<{

+ 15 - 3
src/pages/information/components/academic/AcademicInfoPanel.vue

@@ -2,7 +2,7 @@
   <view class="academic-panel">
     <InformationBannerSwiper />
 
-    <view class="search-section">
+    <view v-if="isLoggedIn" class="search-section">
       <InformationSearchBar @search="handleSearch" />
     </view>
 
@@ -11,6 +11,7 @@
         v-for="item in articleList"
         :key="item.id"
         :item="item"
+        :share-enabled="isLoggedIn"
         @select="handleArticleClick"
         @share="emit('share', $event)"
       />
@@ -29,9 +30,9 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref } from 'vue'
+import { computed, onMounted, ref } from 'vue'
 
-import { getArticlePageApi } from '@/services/modules/information'
+import { getArticlePageApi, getGuestArticlePageApi } from '@/services/modules/information'
 import type { ArticleItem, ArticlePageRequest } from '@/services/modules/information/type'
 
 import { useUserStore } from '@/stores/modules/user'
@@ -47,6 +48,7 @@ const emit = defineEmits<{
 }>()
 
 const userStore = useUserStore()
+const isLoggedIn = computed(() => userStore.isLoggedIn)
 
 const articleList = ref<ArticleItem[]>([])
 const loading = ref(false)
@@ -79,6 +81,14 @@ const getArticleList = async (current = 1, append = false): Promise<void> => {
   loading.value = true
 
   try {
+    if (!isLoggedIn.value) {
+      const { data } = await getGuestArticlePageApi()
+      articleList.value = Array.isArray(data) ? data : data?.records || []
+      currentPage.value = 1
+      finished.value = true
+      return
+    }
+
     const { data } = await getArticlePageApi(buildRequestParams(current))
 
     const records = Array.isArray(data?.records) ? data.records : []
@@ -116,6 +126,8 @@ const handleSearch = (params: { region: string; keyword: string }): void => {
 }
 
 const handleArticleClick = (item: ArticleItem): void => {
+  if (!isLoggedIn.value) return
+
   uni.navigateTo({
     url: `/pages/information/detail/index?type=academic&id=${item.id}`,
   })

+ 2 - 0
src/pages/information/components/album/ProductAlbumItem.vue

@@ -28,6 +28,7 @@
         </text>
 
         <button
+          v-if="shareEnabled"
           class="share-button"
           hover-class="share-button--hover"
           @tap.stop="emit('share', item)"
@@ -54,6 +55,7 @@ import type { AlbumPageNewItem } from '@/services/modules/information/type'
 
 const props = defineProps<{
   item: AlbumPageNewItem
+  shareEnabled: boolean
 }>()
 
 const emit = defineEmits<{

+ 15 - 3
src/pages/information/components/album/ProductAlbumPanel.vue

@@ -1,6 +1,6 @@
 <template>
   <view class="album-panel">
-    <view class="search-section">
+    <view v-if="isLoggedIn" class="search-section">
       <view class="search-card">
         <input
           v-model="keyword"
@@ -24,6 +24,7 @@
         v-for="item in albumList"
         :key="item.id"
         :item="item"
+        :share-enabled="isLoggedIn"
         @select="handleAlbumClick"
         @share="emit('share', $event)"
       />
@@ -42,11 +43,11 @@
 </template>
 
 <script setup lang="ts">
-import { onBeforeUnmount, onMounted, ref } from 'vue'
+import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
 
 import { debounce } from '@/plugins/debounce'
 
-import { getAlbumPageApi } from '@/services/modules/information'
+import { getAlbumPageApi, getGuestAlbumPageApi } from '@/services/modules/information'
 import type { AlbumPageNewItem } from '@/services/modules/information/type'
 
 import { useUserStore } from '@/stores/modules/user'
@@ -60,6 +61,7 @@ const emit = defineEmits<{
 }>()
 
 const userStore = useUserStore()
+const isLoggedIn = computed(() => userStore.isLoggedIn)
 
 const albumList = ref<AlbumPageNewItem[]>([])
 const loading = ref(false)
@@ -89,6 +91,14 @@ const getAlbumList = async (current = 1, append = false) => {
   loading.value = true
 
   try {
+    if (!isLoggedIn.value) {
+      const { data } = await getGuestAlbumPageApi()
+      albumList.value = Array.isArray(data) ? data : data?.records || []
+      currentPage.value = 1
+      finished.value = true
+      return
+    }
+
     const { data } = await getAlbumPageApi(buildRequestParams(current))
 
     const records = Array.isArray(data?.records) ? data.records : []
@@ -156,6 +166,8 @@ const clearKeyword = async () => {
 }
 
 const handleAlbumClick = (item: AlbumPageNewItem) => {
+  if (!isLoggedIn.value) return
+
   if (!item.id) {
     uni.showToast({
       title: '相册信息异常',

+ 57 - 23
src/pages/information/index.vue

@@ -2,23 +2,24 @@
   <page-meta :page-style="sharePopupVisible ? 'overflow:hidden;' : 'overflow:visible;'" />
 
   <view class="page">
-    <view v-if="!isLoggedIn" class="empty-state">
-      <wd-empty icon="no-content" tip="暂无数据" />
-    </view>
-
-    <template v-else>
-      <InformationTabs v-model="activeTab" :tabs="tabs" />
-
-      <AcademicInfoPanel
-        v-if="activeTab === 'academic'"
-        ref="academicPanelRef"
-        @share="openArticleShare"
-      />
-
-      <ProductAlbumPanel v-else ref="albumPanelRef" @share="openAlbumShare" />
-    </template>
+    <InformationTabs v-model="activeTab" :tabs="tabs" />
+
+    <AcademicInfoPanel
+      v-if="activeTab === 'academic'"
+      :key="`academic:${panelKey}`"
+      ref="academicPanelRef"
+      @share="openArticleShare"
+    />
+
+    <ProductAlbumPanel
+      v-else
+      :key="`album:${panelKey}`"
+      ref="albumPanelRef"
+      @share="openAlbumShare"
+    />
   </view>
   <wd-popup
+    v-if="isLoggedIn"
     v-model="sharePopupVisible"
     position="bottom"
     root-portal
@@ -99,9 +100,9 @@
 </template>
 
 <script setup lang="ts">
-import { computed, ref } from 'vue'
+import { computed, ref, watch } from 'vue'
 
-import { onPullDownRefresh, onReachBottom, onShareAppMessage } from '@dcloudio/uni-app'
+import { onPullDownRefresh, onReachBottom, onShareAppMessage, onShow } from '@dcloudio/uni-app'
 
 import {
   getPackageListByTaskTypeAndAlbumIdApi,
@@ -157,6 +158,10 @@ const isLoggedIn = computed<boolean>(() => {
   return Boolean(userStore.isLoggedIn)
 })
 
+const panelKey = computed(() => {
+  return `${userStore.access_token || 'guest'}:${userStore.currentUserInfo?.userId || ''}`
+})
+
 const tabs: TabItem[] = [
   {
     label: '学术信息',
@@ -186,7 +191,10 @@ const selectedPackage = computed<SharePackageItem | undefined>(() => {
 
 const canSubmitShare = computed<boolean>(() => {
   return Boolean(
-    shareTarget.value?.id && selectedPackage.value?.value && !sharePickerScrolling.value
+    isLoggedIn.value &&
+    shareTarget.value?.id &&
+    selectedPackage.value?.value &&
+    !sharePickerScrolling.value
   )
 })
 
@@ -226,8 +234,9 @@ const openAlbumShare = async (item: AlbumPageNewItem): Promise<void> => {
 }
 
 const openShare = async (target: ShareTarget): Promise<void> => {
-  if (loadingSharePackages.value || showBlacklistWarning()) return
+  if (!isLoggedIn.value || loadingSharePackages.value || showBlacklistWarning()) return
 
+  const requestPanelKey = panelKey.value
   loadingSharePackages.value = true
   shareTarget.value = target
   shareRemark.value = ''
@@ -240,6 +249,8 @@ const openShare = async (target: ShareTarget): Promise<void> => {
       target.type === 'academic'
         ? await getPackageListByTaskTypeApi(ARTICLE_TASK_TYPE)
         : await getPackageListByTaskTypeAndAlbumIdApi(ALBUM_TASK_TYPE, target.id)
+    if (!isLoggedIn.value || requestPanelKey !== panelKey.value) return
+
     const packageList = Array.isArray(res.data) ? res.data : []
 
     if (packageList.length === 0) {
@@ -275,6 +286,8 @@ const reportShare = async (params: {
   packageId: string
   remark: string
 }): Promise<void> => {
+  if (!isLoggedIn.value) return
+
   const userId = userStore.currentUserInfo?.userId || ''
 
   if (!userId) return
@@ -311,16 +324,30 @@ const reportShare = async (params: {
   }
 }
 
-onReachBottom(() => {
-  if (!isLoggedIn.value) return
+const updateShareMenu = (): void => {
+  // #ifdef MP-WEIXIN
+  if (isLoggedIn.value) {
+    wx.showShareMenu({ menus: ['shareAppMessage'] })
+  } else {
+    wx.hideShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] })
+  }
+  // #endif
+}
+
+watch(panelKey, () => {
+  sharePopupVisible.value = false
+  resetShareForm()
+  updateShareMenu()
+})
+
+onShow(updateShareMenu)
 
+onReachBottom(() => {
   void currentPanel.value?.loadMore()
 })
 
 onPullDownRefresh(async () => {
   try {
-    if (!isLoggedIn.value) return
-
     await currentPanel.value?.refresh()
   } finally {
     uni.stopPullDownRefresh()
@@ -328,6 +355,13 @@ onPullDownRefresh(async () => {
 })
 
 onShareAppMessage((res) => {
+  if (!isLoggedIn.value) {
+    return {
+      title: '要易小助手',
+      path: '/pages/information/index',
+    }
+  }
+
   const dataset =
     res.from === 'button' ? (res.target?.dataset as ShareDataset | undefined) : undefined
 

+ 2 - 1
src/services/modules/auth/index.ts

@@ -17,6 +17,7 @@ import type {
   SignAgreementRequest,
   SignCertRequest,
   SignRedirectRequest,
+  SignRedirectResponse,
 } from './type'
 
 export const getAuthInfoApi = () => {
@@ -73,7 +74,7 @@ export const signAgreementApi = (data: SignAgreementRequest) => {
 }
 
 export const signRedirectApi = (data: SignRedirectRequest) => {
-  return http.get(`/admin/user-sign-cert/sign/redirect`, data, {
+  return http.get<SignRedirectResponse>(`/admin/user-sign-cert/sign/redirect`, data, {
     loading: true,
     loadingText: '签约中...',
   })

+ 5 - 0
src/services/modules/auth/type.d.ts

@@ -145,3 +145,8 @@ export interface SignAgreementRequest {
 export interface SignRedirectRequest {
   subjectLocation: string
 }
+
+export interface SignRedirectResponse {
+  commonCertStatus: string | null
+  needContinue: boolean | null
+}

+ 22 - 0
src/services/modules/information/index.ts

@@ -18,6 +18,17 @@ export const getArticlePageApi = (data: ArticlePageRequest) => {
   })
 }
 
+export const getGuestArticlePageApi = () => {
+  return http.get<ArticlePageResponse | ArticleItem[]>(
+    `/admin/api/getGuestArticlePage`,
+    undefined,
+    {
+      loading: true,
+      loadingText: '加载中...',
+    }
+  )
+}
+
 export const getAlbumPageApi = (data: AlbumPageNewRequest) => {
   return http.get<AlbumPageNewResponse>(`/admin/api/getAlbumPageNew`, data, {
     loading: true,
@@ -25,6 +36,17 @@ export const getAlbumPageApi = (data: AlbumPageNewRequest) => {
   })
 }
 
+export const getGuestAlbumPageApi = () => {
+  return http.get<AlbumPageNewResponse | AlbumPageNewItem[]>(
+    `/admin/api/getGuestAlbumPage`,
+    undefined,
+    {
+      loading: true,
+      loadingText: '加载中...',
+    }
+  )
+}
+
 export const getArticleByIdApi = (id: string) => {
   return http.get<ArticleItem>(`/admin/api/getArticleById?id=${id}`)
 }

+ 1 - 0
src/services/modules/login/userInfo.d.ts

@@ -98,6 +98,7 @@ export interface CertItem {
   createTime: string
   updateTime: string
   createUser: number
+  needContinue: boolean | null
   updateUser: number
 }
 

+ 51 - 1
src/stores/modules/settlementSign.ts

@@ -10,6 +10,8 @@ export interface SettlementSignContext {
 
 interface SettlementSignState {
   context?: SettlementSignContext
+  returnedSignTraceId: string
+  processingSignTraceId: string
 }
 
 const SIGN_CONTEXT_EXPIRE_TIME = 30 * 60 * 1000
@@ -21,6 +23,8 @@ const createSignTraceId = (subjectLocation: string): string => {
 export const useSettlementSignStore = defineStore('settlementSign', {
   state: (): SettlementSignState => ({
     context: undefined,
+    returnedSignTraceId: '',
+    processingSignTraceId: '',
   }),
 
   getters: {
@@ -37,6 +41,11 @@ export const useSettlementSignStore = defineStore('settlementSign', {
     pendingSubjectLocation(): string {
       return this.validContext?.subjectLocation || ''
     },
+
+    returnedContext(): SettlementSignContext | undefined {
+      const context = this.validContext
+      return context?.signTraceId === this.returnedSignTraceId ? context : undefined
+    },
   },
 
   actions: {
@@ -48,12 +57,53 @@ export const useSettlementSignStore = defineStore('settlementSign', {
       }
 
       this.context = context
+      this.returnedSignTraceId = ''
 
       return context
     },
 
-    clearContext(): void {
+    markReturned(subjectLocation: string, signTraceId: string): void {
+      const context = this.validContext
+      if (
+        !context ||
+        context.subjectLocation !== subjectLocation ||
+        context.signTraceId !== signTraceId ||
+        this.processingSignTraceId === signTraceId
+      ) {
+        return
+      }
+
+      this.returnedSignTraceId = signTraceId
+    },
+
+    // query 回跳与 WebView 返回共用同一把锁,避免两个页面实例重复处理。
+    beginSignRedirect(subjectLocation: string, signTraceId: string): boolean {
+      if (this.processingSignTraceId) return false
+
+      const context = this.validContext
+      if (
+        signTraceId &&
+        (context?.signTraceId !== signTraceId || context.subjectLocation !== subjectLocation)
+      ) {
+        return false
+      }
+
+      this.processingSignTraceId =
+        signTraceId ||
+        (context?.subjectLocation === subjectLocation ? context.signTraceId : subjectLocation)
+      this.returnedSignTraceId = ''
+      return true
+    },
+
+    finishSignRedirect(): void {
+      this.processingSignTraceId = ''
+    },
+
+    clearContext(signTraceId?: string): void {
+      if (signTraceId && this.context?.signTraceId !== signTraceId) return
+
       this.context = undefined
+      this.returnedSignTraceId = ''
     },
   },