shmily1213 1 неделя назад
Родитель
Сommit
57e814c829

+ 3 - 12
src/api/modules/login.ts

@@ -1,4 +1,4 @@
-import { BindCardInfo, LoginInfo, LoginRequestDTO, MobileLoginResponseDTO, RegisterInfo, UserInfo } from "@/types/user";
+import { LoginRequestDTO, MobileLoginResponseDTO, RegisterInfo, UserInfo } from "@/types/user";
 import { ApiResponse } from "@/types";
 import flyio from "../flyio";
 
@@ -17,7 +17,7 @@ export function login(params: LoginRequestDTO) {
  * @param params 注册参数
  * @returns 注册结果
  */
-export function registry(params: BindCardInfo) {
+export function registry(params: RegisterInfo) {
   return flyio.post('/registry', params) as Promise<ApiResponse<any>>;
 }
 
@@ -26,16 +26,7 @@ export function registry(params: BindCardInfo) {
  * @param params 注册参数
  * @returns 注册结果
  */
-export function improve(params: BindCardInfo) {
-  return flyio.post('/improve', params) as Promise<ApiResponse<any>>;
-}
-
-/**
- * 完善信息
- * @param params 注册参数
- * @returns 注册结果
- */
-export function improveWithToken(params: BindCardInfo, token: string) {
+export function improveWithToken(params: RegisterInfo, token: string) {
   return flyio.post('/improve', params, {
     headers: {
       'TempToken': `Bearer ${token}`

+ 5 - 1
src/api/modules/pay.ts

@@ -8,5 +8,9 @@ export function getEcardPrices(params: any) {
 }
 
 export function createOrder(params: any) {
-  return flyio.post('/front/ecard/createOrder?' + qs.stringify(params)) as Promise<ApiResponse<any>>;
+  return flyio.post('/front/ecard/createOrder?' + qs.stringify(params)) as Promise<ApiResponse<Pay.CreateOrderResult>>;
+}
+
+export function queryOrder(orderId: string) {
+  return flyio.get('/front/ecard/getOrderPayStatus', { orderId }) as Promise<ApiResponse<Pay.OrderPayStatus>>;
 }

+ 9 - 1
src/api/modules/user.ts

@@ -1,5 +1,5 @@
 import flyio from "../flyio";
-import { ApiCaptchaResponse, ApiResponse, DictItem, ConfigItem, ApiResponseList } from "@/types";
+import { ApiCaptchaResponse, ApiResponse, DictItem, ConfigItem, ApiResponseList, User } from "@/types";
 import { ClassItem, ClassListQueryDTO, SchoolItem, SchoolListQueryDTO, SmsRequestDTO, CardInfo } from "@/types/user";
 
 /**
@@ -32,4 +32,12 @@ export function getSchoolList(params: SchoolListQueryDTO) {
  */
 export function getClassList(params: ClassListQueryDTO) {
   return flyio.get('/front/user/getClassList', params) as Promise<ApiResponse<ClassItem[]>>;
+}
+
+/**
+ * 
+ * @returns 获取用户绑定的卡信息
+ */
+export function getUserBindCard() {
+  return flyio.get('/front/user/getUserBindCard', {}) as Promise<ApiResponse<User.CardInfo>>;
 }

+ 15 - 0
src/common/enum.ts

@@ -338,4 +338,19 @@ export enum EnumPaperBuildType {
    * 全量手动
    */
   FULL_HAND = 'FullHand'
+}
+
+export enum EnumUserBindCardStatus {
+  /**
+   * 未绑定
+   */
+  UNBOUND = 0,
+  /**
+   * 绑定中
+   */
+  BINDING = 1,
+  /**
+   * 已绑定
+   */
+  BOUND = 1
 }

+ 4 - 1
src/hooks/useCacheStore.js

@@ -49,6 +49,7 @@ export const useCacheStore = createGlobalState(() => {
     }
     const executeAction = async (action, payload) => {
         let result = action.handler(payload)
+        console.log(result)
         if (result instanceof Promise) result = await result
         return result
     }
@@ -62,6 +63,7 @@ export const useCacheStore = createGlobalState(() => {
     * @returns {Object} 缓存结果
     * */
     const dispatchCache = async (action, payload = undefined, options = undefined) => {
+      console.log(111, action)
         // TODO:并发锁, 并发机会不高且没有致命性影响,以后再优化
         const formatAction = checkActionDefine(action)
         let typeNode = container.get(formatAction.type)
@@ -79,9 +81,10 @@ export const useCacheStore = createGlobalState(() => {
             await sleep(0)
             return cacheData.data
         }
-
         // cache miss, execute action and cache result
+        console.log(222)
         const result = await executeAction(formatAction, payload)
+        console.log(333, result)
         const cacheOptions = _.merge(defaultOptions, options)
         cacheData = new CacheResult(result, cacheOptions)
         if (!(empty(result) && cacheOptions.emptyIgnore)) {

+ 17 - 0
src/hooks/useEnv.ts

@@ -0,0 +1,17 @@
+import { useAppStore } from "@/store/appStore";
+export const useEnv = () => {
+  const { systemInfo } = useAppStore();
+  const platform = ref(window.platform || "");
+  const isH5 = computed(() => platform.value === "h5");
+  const isWap2App = computed(() => platform.value === "wap2app");
+  const isAndroid = computed(() => systemInfo.platform.toLowerCase() == 'android')
+  const isIOS = computed(() => systemInfo.platform.toLowerCase() == 'ios')
+
+  return {
+    platform,
+    isH5,
+    isWap2App,
+    isAndroid,
+    isIOS
+  }
+}

+ 193 - 11
src/hooks/usePay.ts

@@ -1,8 +1,20 @@
-import { createOrder, getEcardPrices } from "@/api/modules/pay";
+import { createOrder, getEcardPrices, queryOrder } from "@/api/modules/pay";
+import { useEnv } from "./useEnv";
+import { usePayStore } from "@/store/payStore";
+import config from "@/config";
+import { Pay } from "@/types";
 
 export const usePay = () => {
+  const payStore = usePayStore();
+  const { price } = storeToRefs(payStore);
   const ready = ref(false);
-  const price = ref(468000);
+  const loading = ref(false);
+  const outTradeNo = ref('');
+  const h5url = ref('');
+  const queryTimes = ref(10);
+  const callback = ref<Pay.OrderPayCallback>({});
+  let webview: PlusWebviewWebviewObject | HTMLIFrameElement | undefined = undefined;
+
   const formatPrice = computed(() => {
     return price.value / 100;
   });
@@ -10,23 +22,193 @@ export const usePay = () => {
     const res = await getEcardPrices({});
     const card = res.data?.at(0);
     if (card) {
-      price.value = card.price;
+      payStore.setPrice(card.price)
       ready.value = true;
     }
   }
-  const pay = () => {
-    createOrder({
-      totalFee: price.value,
-      type: undefined
-    }).then(res => {
-      console.log(res)
-    })
+  const pay = async () => {
+    loading.value = true;
+    uni.$ie.showLoading('支付中...');
+    try {
+      const { data } = await createOrder({
+        totalFee: price.value,
+        type: undefined
+      });
+      uni.$ie.hideLoading();
+      outTradeNo.value = data.outTradeNo;
+      h5url.value = data.h5url;
+      if (outTradeNo && h5url) {
+        clearResultFrame()
+        webview = await createResultFrame(h5url.value);
+      } else {
+        throw new Error('支付地址缺失');
+      }
+    } catch (error) {
+      loading.value = false;
+      uni.$ie.showToast('下单失败,请稍后再试');
+    }
   }
+  const checkOrderStatus = async () => {
+    if (!outTradeNo.value) {
+      return;
+    }
+
+    const no = outTradeNo.value.split('_')[1];
+    if (!no) {
+      console.error('checkOrderStatus: 订单号格式错误');
+      callback.value.onFailed?.();
+      return;
+    }
+
+    let queryCount = 0; // 当前查询次数
+    const queryInterval = 3000; // 查询间隔(毫秒)
+    let timerId: ReturnType<typeof setTimeout> | null = null;
+
+    /**
+     * 停止查询
+     */
+    const stopQuery = () => {
+      if (timerId) {
+        clearTimeout(timerId);
+        timerId = null;
+        clearResultFrame();
+      }
+      loading.value = false;
+      uni.$ie.hideLoading();
+    };
+
+    /**
+     * 执行查询
+     */
+    const query = async () => {
+      try {
+        queryCount++;
+        const { data } = await queryOrder(no);
+        console.log(`checkOrderStatus: 第 ${queryCount} 次查询订单状态`);
+        console.log(JSON.stringify(data))
+        if (!data) {
+          console.error('checkOrderStatus: 查询结果为空');
+          stopQuery();
+          callback.value.onFailed?.();
+          return;
+        }
+
+        // 支付成功
+        if (data.isPaySuccess) {
+          console.log('checkOrderStatus: 支付成功');
+          stopQuery();
+          callback.value.onSuccess?.();
+          return;
+        }
+
+        // 支付失败
+        if (data.isPayFailed) {
+          console.log('checkOrderStatus: 支付失败');
+          stopQuery();
+          callback.value.onFailed?.();
+          return;
+        }
+
+        // 未支付状态
+        if (data.isUnPaid) {
+          // 如果已达到最大查询次数,回调失败
+          if (queryCount >= queryTimes.value) {
+            console.log(`checkOrderStatus: 已查询 ${queryTimes.value} 次,仍未支付,回调失败`);
+            stopQuery();
+            callback.value.onFailed?.();
+            return;
+          }
+
+          // 继续查询
+          timerId = setTimeout(() => {
+            query();
+          }, queryInterval);
+        } else {
+          // 未知状态,回调失败
+          console.error('checkOrderStatus: 未知的支付状态', data);
+          stopQuery();
+          callback.value.onFailed?.();
+        }
+      } catch (error) {
+        // 查询过程中发生错误,回调失败
+        console.error('checkOrderStatus: 查询订单状态失败', error);
+        stopQuery();
+        callback.value.onFailed?.();
+      }
+    };
+
+    // 开始第一次查询
+    uni.$ie.showLoading('支付查询中...')
+    query();
+  }
+
+  const createResultFrame = async (url: string) => {
+    const { isH5, isWap2App } = useEnv();
+    if (isH5.value) {
+      await sleep()
+      const iframe = document.createElement('iframe');
+      const style = `width:0px;height:0px;position: absolute;opacity: 0;z-index: -1;outline:none;border:none;`;
+      iframe.setAttribute('style', style);
+      iframe.setAttribute('sandbox', 'allow-scripts allow-top-navigation allow-same-origin');
+      iframe.style.position = 'absolute';
+      iframe.src = url;
+      iframe.onload = () => checkOrderStatus()
+      iframe.onerror = () => console.error('Payment: iframe加载失败')
+      document.body.appendChild(iframe);
+      return iframe
+    } else if (isWap2App.value) {
+      const style: PlusWebviewWebviewStyles = {
+        webviewBGTransparent: true,
+        opacity: 0,
+        render: 'always',
+        zindex: -1,
+        width: '1px',
+        height: '1px',
+        top: '0px',
+        left: '0px',
+        additionalHttpHeaders: {
+          Referer: config.paySiteUrl,
+        }
+      };
+      const wv = plus.webview.create(url, 'wechatPayWebview', style);
+      wv.addEventListener('loaded', (e) => {
+        checkOrderStatus();
+      }, false);
+      wv.show();
+      return wv;
+    }
+  }
+
+  const clearResultFrame = () => {
+    const { isH5, isWap2App } = useEnv();
+    if (webview) {
+      if (isH5.value) {
+        document.body.removeChild(webview as HTMLIFrameElement)
+      } else if (isWap2App.value) {
+        (webview as PlusWebviewWebviewObject).close();
+      }
+      webview = undefined;
+    }
+  }
+
+  const sleep = (time: number = 300) => {
+    return new Promise(resolve => {
+      setTimeout(resolve, time);
+    });
+  }
+
+  const setCallback = (cb: Pay.OrderPayCallback) => {
+    callback.value = cb;
+  }
+
   getPrice();
   return {
+    getPrice,
     price,
     formatPrice,
     ready,
-    pay
+    pay,
+    loading,
+    setCallback
   }
 }

+ 1 - 1
src/hooks/useUserStore.js

@@ -23,7 +23,7 @@ export const useUserStore = createGlobalState(() => {
     const isScoreLocked = computed(() => data.value.scoreLock == mxConst.enum.scoreLock.locked)
     const isScoreUnlocked = computed(() => data.value.scoreLock == mxConst.enum.scoreLock.unlock)
     const isScoreLocking = computed(() => data.value.scoreLock == mxConst.enum.scoreLock.locking)
-    const isCultural = computed(() => data.value.examType == mxConst.keyCulturalExamType)
+    const isCultural = computed(() => data.value.examType == 'VHS')
     const enableAuthModify = computed(() => data.value.enableAuthModify)
     const period = computed(() => data.value.period) // 只是为了妆容旧结构,理论上这个值已经没有用了
     // 同意协议需要严格判定,必须和当前用户挂钩

+ 5 - 0
src/main.ts

@@ -91,6 +91,11 @@ export function createApp() {
         disableHover: {
           default: false
         }
+      },
+      button: {
+        loadingColor: {
+          default: 'rgb(200, 200, 200)'
+        }
       }
     }
   })

+ 6 - 1
src/pagesMain/pages/index/components/index-banner.vue

@@ -88,7 +88,12 @@ const validMenus = computed(() => {
     //   name: '专升本',
     //   icon: '/menu/menu-upgrade.png',
     //   pageUrl: '/pages/index/index',
-    // }
+    // },
+    {
+      name: '查位次',
+      icon: '/menu/menu-upgrade.png',
+      pageUrl: '/pagesOther/pages/career/query-segment/query-segment',
+    }
   ]
   return menus.filter(item => item.visible !== false);
 });

+ 6 - 6
src/pagesMain/pages/index/index.vue

@@ -11,8 +11,7 @@
       <template #headerRight="{ isTransparent }">
         <view v-if="userStore.getLocation" class="ml-10 flex items-center gap-x-4" @click="handleChangeLocation">
           <uv-icon name="map" size="14" :color="isTransparent ? '#333' : 'var(--primary-color)'" />
-          <text class="text-26 transition-colors duration-300"
-            :class="[isTransparent ? 'text-fore-title' : 'text-primary']">
+          <text class="text-26" :class="[isTransparent ? 'text-fore-title' : 'text-primary']">
             {{ userStore.getLocation }}
           </text>
         </view>
@@ -74,8 +73,9 @@ const checkProvinceInfo = () => {
     popupRef.value.open();
   }
 }
-const checkTeacherInfo = async () => {
-  await userStore.checkInfoComplete();
+const checkInfo = async () => {
+  await userStore.checkInfoTeacherComplete();
+  await userStore.checkInfoStudentComplete();
 }
 const reloadUserInfo = async () => {
   if (userStore.isLogin) {
@@ -105,9 +105,9 @@ onShow(() => {
   }, 0);
   setTimeout(() => {
     checkProvinceInfo();
-    checkTeacherInfo();
+    checkInfo();
     reloadUserInfo();
-  }, 500);
+  }, 600);
   isHide.value = false;
 });
 </script>

+ 3 - 2
src/pagesMain/pages/me/components/me-info.vue

@@ -80,8 +80,9 @@ const handleHeaderClick = async () => {
   const isLogin = await userStore.checkLogin({ askToLogin: false });
   if (isLogin) {
     setTimeout(async () => {
-      await userStore.checkInfoComplete();
-    }, 500)
+      await userStore.checkInfoTeacherComplete();
+      await userStore.checkInfoStudentComplete();
+    }, 600)
   }
 }
 const handleVip = () => {

+ 6 - 0
src/pagesMain/pages/me/me.vue

@@ -10,8 +10,14 @@
 </template>
 
 <script lang="ts" setup>
+import { usePay } from '@/hooks/usePay';
 import meInfo from './components/me-info.vue';
 import meMenu from './components/me-menu.vue';
+
+const { getPrice } = usePay();
+onLoad(() => {
+  getPrice();
+});
 </script>
 
 <style></style>

+ 104 - 106
src/pagesOther/pages/career/query-segment/query-segment.vue

@@ -1,146 +1,144 @@
 <template>
-    <z-paging ref="paging" v-model="list" :auto="false" :default-page-size="20"
-              @on-refresh="handleRefresh" @query="handleQuery">
-        <template #top>
-            <mx-nav-bar title="一分一段"/>
-            <uv-gap height="15"/>
-            <uv-form ref="form" :model="queryParams" :rules="rules" error-type="toast">
-                <view class="px-30 bg-white fx-row items-center gap-30">
-                    <uv-tags icon="lock" shape="circle" plain plain-fill :text="currentUser.examMajorName"/>
-                    <mx-condition/>
-                </view>
-            </uv-form>
-        </template>
-        <view class="px-30 py-10 bg-white">
-            <uv-search v-model="score" placeholder="输入分数查等效分" @search="handleScoreSearch(false)"
-                       @custom="handleScoreSearch(false)"/>
-            <view class="mt-30 grid grid-cols-7 items-center">
-                <view class="col-span-2 fx-col fx-cen-cen gap-10">
-                    <text class="text-2xs text-tips">分数</text>
-                    <text class="text-sm text-main font-bold">{{ match.score || '-' }}</text>
-                </view>
-                <view class="col-span-2 fx-col fx-cen-cen gap-10">
-                    <text class="text-2xs text-tips">同分人数</text>
-                    <text class="text-sm text-main font-bold">{{ match.num || '-' }}</text>
-                </view>
-                <view class="col-span-3 fx-col fx-cen-cen gap-10">
-                    <text class="text-2xs text-tips">位次区间</text>
-                    <text class="text-sm text-main font-bold">{{ match.highestRank + '~' + match.lowestRank }}</text>
-                </view>
-            </view>
+  <z-paging ref="paging" v-model="list" :auto="false" :default-page-size="20" @on-refresh="handleRefresh"
+    @query="handleQuery">
+    <template #top>
+      <mx-nav-bar title="一分一段" />
+      <uv-gap height="15" />
+      <uv-form ref="form" :model="queryParams" :rules="rules" error-type="toast">
+        <view class="px-30 bg-white fx-row items-center gap-30">
+          <uv-tags icon="lock" shape="circle" plain plain-fill :text="currentUser.examMajorName" />
+          <mx-condition />
         </view>
-        <template v-if="eqList.length">
-            <view class="px-30 bg-white py-10 text-main font-bold">历年等效分</view>
-            <segment-score-table :cols="eqTable.cols" :rows="eqTable.rows" class="p-30 bg-white"/>
-            <view class="px-30 bg-white">
-                <uv-text prefix-icon="error-circle" type="info" size="12"
-                         :icon-style="{color: 'var(--error-color)', marginRight: '3px', fontSize: '14px'}"
-                         text="因高考人数及招生计划每年都存在一定的增减,分数和位次也会有相应浮动,我们根据历年省控线变化幅度等比计算出历年等效位次和等效分。"/>
-            </view>
-        </template>
-        <view class="py-20 bg-white">
-            <uv-gap height="15" class="!bg-bg"/>
+      </uv-form>
+    </template>
+    <view class="px-30 py-10 bg-white">
+      <uv-search v-model="score" placeholder="输入分数查等效分" @search="handleScoreSearch(false)"
+        @custom="handleScoreSearch(false)" />
+      <view class="mt-30 grid grid-cols-7 items-center">
+        <view class="col-span-2 fx-col fx-cen-cen gap-10">
+          <text class="text-2xs text-tips">分数</text>
+          <text class="text-sm text-main font-bold">{{ match.score || '-' }}</text>
         </view>
-        <uv-sticky :custom-nav-height="-1">
-            <view class="bg-white px-30 py-10 text-main font-bold">一分一段表</view>
-        </uv-sticky>
-        <segment-score-table :cols="segCols" :rows="list" class="p-30 bg-white"/>
-    </z-paging>
+        <view class="col-span-2 fx-col fx-cen-cen gap-10">
+          <text class="text-2xs text-tips">同分人数</text>
+          <text class="text-sm text-main font-bold">{{ match.num || '-' }}</text>
+        </view>
+        <view class="col-span-3 fx-col fx-cen-cen gap-10">
+          <text class="text-2xs text-tips">位次区间</text>
+          <text class="text-sm text-main font-bold">{{ match.highestRank + '~' + match.lowestRank }}</text>
+        </view>
+      </view>
+    </view>
+    <template v-if="eqList.length">
+      <view class="px-30 bg-white py-10 text-main font-bold">历年等效分</view>
+      <segment-score-table :cols="eqTable.cols" :rows="eqTable.rows" class="p-30 bg-white" />
+      <view class="px-30 bg-white">
+        <uv-text prefix-icon="error-circle" type="info" size="12"
+          :icon-style="{ color: 'var(--error-color)', marginRight: '3px', fontSize: '14px' }"
+          text="因高考人数及招生计划每年都存在一定的增减,分数和位次也会有相应浮动,我们根据历年省控线变化幅度等比计算出历年等效位次和等效分。" />
+      </view>
+    </template>
+    <view class="py-20 bg-white">
+      <uv-gap height="15" class="!bg-bg" />
+    </view>
+    <uv-sticky :custom-nav-height="-1">
+      <view class="bg-white px-30 py-10 text-main font-bold">一分一段表</view>
+    </uv-sticky>
+    <segment-score-table :cols="segCols" :rows="list" class="p-30 bg-white" />
+  </z-paging>
 </template>
 
 <script setup>
-import {ref, watch, computed} from 'vue'
-import {useUserStore} from "@/hooks/useUserStore";
-import {useProvideSearchModel} from "@/components/mx-condition/useSearchModelInjection";
-import {useConditionSegmentLocation} from "@/components/mx-condition/modules/useConditionSegmentLocation";
-import {useConditionSegmentYear} from "@/components/mx-condition/modules/useConditionSegmentYear";
-import {useConditionSegmentMode} from "@/components/mx-condition/modules/useConditionSegmentMode";
-import {useProvideVoluntaryData} from "@/hooks/useVoluntaryDataInjection";
-import {useCacheStore} from "@/hooks/useCacheStore";
-import {cacheActions} from "@/hooks/defineCacheActions";
+import { ref, watch, computed } from 'vue'
+import { useUserStore } from "@/hooks/useUserStore";
+import { useProvideSearchModel } from "@/components/mx-condition/useSearchModelInjection";
+import { useConditionSegmentLocation } from "@/components/mx-condition/modules/useConditionSegmentLocation";
+import { useConditionSegmentYear } from "@/components/mx-condition/modules/useConditionSegmentYear";
+import { useConditionSegmentMode } from "@/components/mx-condition/modules/useConditionSegmentMode";
+import { useProvideVoluntaryData } from "@/hooks/useVoluntaryDataInjection";
+import { useCacheStore } from "@/hooks/useCacheStore";
+import { cacheActions } from "@/hooks/defineCacheActions";
 import SegmentScoreTable from "@/pagesOther/pages/career/query-segment/components/segment-score-table.vue";
 
 const paging = ref(null)
 const form = ref(null)
 const list = ref([])
 const eqList = ref([])
-const {currentUser, isCultural} = useUserStore()
-const {provinceName: location, examMajor: mode} = currentUser.value
-const queryParams = ref({location, year: '', mode})
+const { currentUser, isCultural } = useUserStore()
+const { provinceName: location, examMajor: mode } = currentUser.value
+const queryParams = ref({ location, year: '', mode })
 const score = ref(currentUser.value.score)
 const defaultMatch = {
-    highestRank: '',
-    lowestRank: '',
-    num: 0,
-    numTotal: 0,
-    score: 0
+  highestRank: '',
+  lowestRank: '',
+  num: 0,
+  numTotal: 0,
+  score: 0
 }
 const match = ref(defaultMatch)
 
 const segCols = [
-    {label: '分数', prop: 'score'},
-    {label: '位次区间', prop: 'rank'},
-    {label: '同分人数', prop: 'num'},
+  { label: '分数', prop: 'score' },
+  { label: '位次区间', prop: 'rank' },
+  { label: '同分人数', prop: 'num' },
 ]
 
 const eqTable = computed(() => {
-    const cols = [], rows = []
-    if (!eqList.value.length) return {cols, rows}
-    cols.push({label: '类型/年份', prop: 'type'})
-    const eqSeat = {type: '等效位'}
-    const eqScore = {type: '等效分'}
-    eqList.value.forEach(r => {
-        cols.push({label: r.year, prop: r.year})
-        eqSeat[r.year] = r.seat || '-'
-        eqScore[r.year] = r.score || '-'
-    })
-    rows.push(eqSeat)
-    rows.push(eqScore)
-    return {cols, rows}
+  const cols = [], rows = []
+  if (!eqList.value.length) return { cols, rows }
+  cols.push({ label: '类型/年份', prop: 'type' })
+  const eqSeat = { type: '等效位' }
+  const eqScore = { type: '等效分' }
+  eqList.value.forEach(r => {
+    cols.push({ label: r.year, prop: r.year })
+    eqSeat[r.year] = r.seat || '-'
+    eqScore[r.year] = r.score || '-'
+  })
+  rows.push(eqSeat)
+  rows.push(eqScore)
+  return { cols, rows }
 })
 
-const {dispatchCache, removeCache} = useCacheStore()
-const {onSearch, rules} = useProvideSearchModel([
-    // useConditionSegmentLocation(),
-    // useConditionSegmentYear(),
-    // useConditionSegmentMode()
-    // 目前只需要响应年份
-    useConditionSegmentYear()
+const { dispatchCache, removeCache } = useCacheStore()
+const { onSearch, rules } = useProvideSearchModel([
+  useConditionSegmentLocation(),
+  // useConditionSegmentYear(),
+  // useConditionSegmentMode()
+  // 目前只需要响应年份
+  useConditionSegmentYear()
 ], queryParams, form)
-const {voluntaryData, validate: validateScore} = useProvideVoluntaryData(() => queryParams.value.year)
+const { voluntaryData, validate: validateScore } = useProvideVoluntaryData(() => queryParams.value.year)
 
 onSearch(() => {
-    handleScoreSearch()
-    paging.value.reload()
+  handleScoreSearch()
+  paging.value.reload()
 })
 watch(voluntaryData, () => handleScoreSearch())
 
 const handleRefresh = () => {
-    removeCache(cacheActions.getSectionList)
-    removeCache(cacheActions.getEquivalentScore)
+  removeCache(cacheActions.getSectionList)
+  removeCache(cacheActions.getEquivalentScore)
 
-    handleScoreSearch()
+  handleScoreSearch()
 }
 
 const handleQuery = (pageNum, pageSize) => {
-    const payload = {...queryParams.value, pageNum, pageSize}
-    dispatchCache(cacheActions.getSectionList, payload)
-        .then(res => {
-            res.rows.forEach(r => r.rank = r.highestRank + '~' + r.lowestRank)
-            paging.value.completeByTotal(res.rows, res.total)
-        })
-        .catch(e => paging.value.complete(false))
+  const payload = { ...queryParams.value, pageNum, pageSize }
+  dispatchCache(cacheActions.getSectionList, payload)
+    .then(res => {
+      res.rows.forEach(r => r.rank = r.highestRank + '~' + r.lowestRank)
+      paging.value.completeByTotal(res.rows, res.total)
+    })
+    .catch(e => paging.value.complete(false))
 }
 
 const handleScoreSearch = async (silence = true) => {
-    await validateScore(score.value, silence)
-    const payload = {...queryParams.value, score: score.value}
-    const res = await dispatchCache(cacheActions.getEquivalentScore, payload)
-    match.value = res.match || defaultMatch
-    eqList.value = res.scores || []
+  await validateScore(score.value, silence)
+  const payload = { ...queryParams.value, score: score.value }
+  const res = await dispatchCache(cacheActions.getEquivalentScore, payload)
+  match.value = res.match || defaultMatch
+  eqList.value = res.scores || []
 }
 </script>
 
-<style lang="scss">
-
-</style>
+<style lang="scss"></style>

+ 1 - 1
src/pagesOther/pages/voluntary/index/components/cart-step.vue

@@ -8,7 +8,7 @@
         <voluntary-search @search="handleSearch"/>
         <view class="fx-col p-20 gap-20">
             <voluntary-item v-for="item in list" :item="item" @major="openMajorPopup(item)" @notify="showNotify"/>
-            <vip-guide-more v-if="isNotVip"/>
+            <!-- <vip-guide-more v-if="isNotVip"/> -->
         </view>
         <template #bottom>
             <voluntary-bottom @modify="$refs.modifyPopup.open()" @cart="$refs.cartPopup.open()"/>

+ 12 - 13
src/pagesOther/pages/voluntary/index/components/course-selector.vue

@@ -1,20 +1,19 @@
 <template>
-    <view class="fx-col">
-        <view class="pl-20">
-            专业类别
-        </view>
-        <view class="py-15 fx-row gap-20">
-            <uv-tags size="large" type="primary" shape="circle" icon="lock" plain plain-fill
-                     :text="currentUser.examMajorName"/>
-        </view>
+  <view class="fx-col">
+    <view class="pl-20">
+      专业类别
     </view>
+    <view class="py-15 fx-row gap-20">
+      <uv-tags size="large" type="primary" shape="circle" icon="lock" plain plain-fill
+        :text="userInfo.examMajorName" />
+    </view>
+  </view>
 </template>
 
-<script setup>
-import {useUserStore} from "@/hooks/useUserStore";
+<script lang="ts" setup>
+import { useUserStore } from "@/store/userStore";
 
-const {currentUser} = useUserStore()
+const { userInfo } = useUserStore()
 </script>
 
-<style lang="scss" scoped>
-</style>
+<style lang="scss" scoped></style>

+ 29 - 13
src/pagesOther/pages/voluntary/index/index.vue

@@ -1,16 +1,25 @@
 <template>
-  <view ref="container" class="h-full">
-    <swiper :current="currentStep" disable-touch class="h-full">
-      <swiper-item class="h-full">
-        <score-step />
-      </swiper-item>
-      <swiper-item>
-        <batch-step />
-      </swiper-item>
-      <swiper-item>
-        <cart-step />
-      </swiper-item>
-    </swiper>
+  <view ref="container" class="h-full flex flex-col">
+    <ie-navbar title="填志愿" custom-back>
+      <template #headerLeft>
+        <view>
+          <uv-icon v-if="currentStep > 0" name="arrow-left" size="20px" @click="handlePrev"></uv-icon>
+        </view>
+      </template>
+    </ie-navbar>
+    <view class="flex-1 min-h-1">
+      <swiper :current="currentStep" disable-touch class="h-full">
+        <swiper-item class="h-full">
+          <score-step />
+        </swiper-item>
+        <swiper-item>
+          <batch-step />
+        </swiper-item>
+        <swiper-item>
+          <cart-step />
+        </swiper-item>
+      </swiper>
+    </view>
   </view>
 </template>
 
@@ -46,8 +55,15 @@ const container = ref(null) // swiper必须指定明确的高度,所以多包
 const assistantSvc = useProvideVoluntaryAssistant(stepSvc, dataSvc, formSvc, cartSvc, highlightSvc)
 const { navBinding, onComplete, resetAll } = assistantSvc
 
+const handlePrev = () => {
+  currentStep.value -= 1
+  if (currentStep.value < 0) {
+    currentStep.value = 0
+  }
+}
+
 onComplete((id) => {
-  transferTo('/pages/voluntary/detail/detail', { id })
+  transferTo('/pagesOther/pages/voluntary/detail/detail', { id })
   currentStep.value = 0
   resetAll()
 })

+ 54 - 55
src/pagesOther/pages/voluntary/list/list.vue

@@ -1,68 +1,67 @@
 <template>
-    <z-paging ref="paging" v-model="list" @query="handleQuery">
-        <template #top>
-            <mx-nav-bar title="我的志愿表"/>
-        </template>
-        <view class="p-30 fx-col gap-30">
-            <view v-for="item in list" class="bg-white mx-card p-30 fx-row fx-bet-cen" @click="goDetails(item)">
-                <view class="fx-col gap-10">
-                    <text class="font-bold text-main"> {{ item.name }}</text>
-                    <text class="text-tips text-sm">
-                        {{ `${item.score}分 ${item.batchName || ''} ${item.userSnapshot.examMajorName}` }}
-                    </text>
-                </view>
-                <view class="fx-row">
-                    <view class="w-80 h-80 fx-row fx-cen-cen" @click.stop="handleDelete(item)">
-                        <uv-icon name="trash" size="20"/>
-                    </view>
-                    <uv-icon name="arrow-right"></uv-icon>
-                </view>
-            </view>
+  <z-paging ref="paging" v-model="list" @query="handleQuery">
+    <template #top>
+      <mx-nav-bar title="我的志愿表" />
+    </template>
+    <view class="p-30 fx-col gap-30">
+      <view v-for="item in list" class="bg-white mx-card p-30 fx-row fx-bet-cen" @click="goDetails(item)">
+        <view class="fx-col gap-10">
+          <text class="font-bold text-main"> {{ item.name }}</text>
+          <text class="text-tips text-sm">
+            {{ `${item.score}分 ${item.batchName || ''} ${item.userSnapshot.examMajorName}` }}
+          </text>
         </view>
-    </z-paging>
+        <view class="fx-row">
+          <view class="w-80 h-80 fx-row fx-cen-cen" @click.stop="handleDelete(item)">
+            <uv-icon name="trash" size="20" />
+          </view>
+          <uv-icon name="arrow-right"></uv-icon>
+        </view>
+      </view>
+    </view>
+  </z-paging>
 </template>
 
 <script>
-import {delZytbRecord, selectZytbRecord} from '@/api/webApi/volunteer'
-import {useProvideTransfer} from "@/hooks/useTransfer";
-import {confirmAsync} from "@/utils/uni-helper";
-import {toast} from "@/uni_modules/uv-ui-tools/libs/function";
+import { delZytbRecord, selectZytbRecord } from '@/api/webApi/volunteer'
+import { useProvideTransfer } from "@/hooks/useTransfer";
+import { confirmAsync } from "@/utils/uni-helper";
+import { toast } from "@/uni_modules/uv-ui-tools/libs/function";
 
 export default {
-    data() {
-        return {
-            list: []
-        }
+  data() {
+    return {
+      list: []
+    }
+  },
+  setup() {
+    const { transferTo } = useProvideTransfer()
+    return {
+      transferTo
+    }
+  },
+  methods: {
+    goDetails(data) {
+      this.transferTo('/pagesOther/pages/voluntary/detail/detail', data, null, true)
     },
-    setup() {
-        const {transferTo} = useProvideTransfer()
-        return {
-            transferTo
-        }
+    handleQuery(pageNum, pageSize) {
+      selectZytbRecord({ pageNum, pageSize }).then(res => {
+        res.rows.forEach(r => {
+          if (typeof r.userSnapshot === 'string')
+            r.userSnapshot = JSON.parse(r.userSnapshot)
+          if (!r.userSnapshot) r.userSnapshot = { examMajorName: '' }
+        })
+        this.$refs.paging.completeByTotal(res.rows, res.total)
+      }).catch(e => this.$refs.paging.complete(false))
     },
-    methods: {
-        goDetails(data) {
-            this.transferTo('/pages/voluntary/detail/detail', data, null, true)
-        },
-        handleQuery(pageNum, pageSize) {
-            selectZytbRecord({pageNum, pageSize}).then(res => {
-                res.rows.forEach(r => {
-                    if (typeof r.userSnapshot === 'string')
-                        r.userSnapshot = JSON.parse(r.userSnapshot)
-                    if (!r.userSnapshot) r.userSnapshot = {examMajorName: ''}
-                })
-                this.$refs.paging.completeByTotal(res.rows, res.total)
-            }).catch(e => this.$refs.paging.complete(false))
-        },
-        async handleDelete(item) {
-            await confirmAsync(`确认删除'${item.name}'`)
-            await delZytbRecord({id: item.id})
-            toast('删除成功')
-            this.$refs.paging.reload()
-        }
+    async handleDelete(item) {
+      await confirmAsync(`确认删除'${item.name}'`)
+      await delZytbRecord({ id: item.id })
+      toast('删除成功')
+      this.$refs.paging.reload()
     }
+  }
 }
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>

+ 20 - 9
src/pagesSystem/pages/bind-profile/bind-profile.vue

@@ -1,6 +1,6 @@
 <template>
   <ie-page bg-color="#F6F8FA" :safeAreaInsetBottom="false">
-    <ie-navbar title="完善信息"></ie-navbar>
+    <ie-navbar title="完善信息" custom-back @left-click="handleBack" />
     <uv-form labelPosition="left" :model="form" labelWidth="70px" ref="formRef">
       <content-card title="个人信息">
         <uv-form-item label="姓名" prop="name" borderBottom required>
@@ -108,11 +108,11 @@
 <script lang="ts" setup>
 import ContentCard from '../../components/content-card.vue';
 import { useUserStore } from '@/store/userStore';
-import { registry, improve, improveWithToken } from '@/api/modules/login';
+import { registry, improveWithToken } from '@/api/modules/login';
 import { useTransferPage } from '@/hooks/useTransferPage';
 import { useExamType } from '@/composables/useExamType';
 import { useAppStore } from '@/store/appStore';
-import { BindCardInfo, CardInfo, ClassItem, LoginInfo, RegisterInfo, SchoolItem, Scores, UserInfo } from '@/types/user';
+import { CardInfo, ClassItem, LoginInfo, RegisterInfo, SchoolItem, Scores, UserInfo } from '@/types/user';
 
 import { getClassList } from '@/api/modules/user';
 import { EnumBindScene, EnumExamRecordType, EnumExamType, EnumUserType } from '@/common/enum';
@@ -128,7 +128,7 @@ const { form: examTypeForm, examTypeList, examMajorList, provinceList, endYearLi
 const userStore = useUserStore();
 const { prevData, transferTo, transferBack } = useTransferPage();
 
-const form = ref<Partial<BindCardInfo>>({});
+const form = ref<Partial<RegisterInfo>>({});
 const scoresForm = ref<Scores>({})
 const formRef = ref();
 const customStyle = {
@@ -361,12 +361,12 @@ const handleSubmit = async () => {
     try {
       console.log('初步提交信息:', prevData.value.scene, params);
       if (prevData.value.scene === EnumBindScene.REGISTER) {
-        startRegister(params as BindCardInfo);
+        startRegister(params as RegisterInfo);
       } else {
         if (prevData.value.scene === EnumBindScene.LOGIN_BIND) {
-          startLoginBind(params as BindCardInfo);
+          startLoginBind(params as RegisterInfo);
         } else {
-          startRegister(params as BindCardInfo);
+          startRegister(params as RegisterInfo);
         }
       }
     } catch (error) {
@@ -375,7 +375,7 @@ const handleSubmit = async () => {
   }
 }
 
-const startLoginBind = async (params: BindCardInfo) => {
+const startLoginBind = async (params: RegisterInfo) => {
   uni.$ie.showLoading();
   const token = prevData.value.token;
   try {
@@ -394,7 +394,7 @@ const startLoginBind = async (params: BindCardInfo) => {
   }
 }
 
-const startRegister = async (params: BindCardInfo) => {
+const startRegister = async (params: RegisterInfo) => {
   uni.$ie.showLoading();
   const { token } = await registry(params);
   if (token) {
@@ -407,6 +407,17 @@ const startRegister = async (params: BindCardInfo) => {
   }
 }
 
+const handleBack = () => {
+  const pages = getCurrentPages();
+  const page = pages[pages.length - 2];
+  // 如果是登录页,允许直接返回
+  if (page?.route && ['pagesMain/pages/index/index', 'pagesSystem/pages/pay/pay'].includes(page.route)) {
+    uni.$ie.showToast('请先完善信息');
+  } else {
+    transferBack();
+  }
+};
+
 const goHome = () => {
   setTimeout(() => {
     transferTo('/pagesMain/pages/index/index', {

+ 1 - 1
src/pagesSystem/pages/edit-profile/edit-profile.vue

@@ -156,7 +156,7 @@ import ContentCard from '@/pagesSystem/components/content-card.vue';
 import { updateUserInfo } from '@/api/modules/login';
 import { useUserStore } from '@/store/userStore';
 import { useSchool } from '@/composables/useSchool';
-import { BindCardInfo, UserInfo } from '@/types/user';
+import { UserInfo } from '@/types/user';
 import { EnumDictName, EnumSmsApiType, EnumExamType } from '@/common/enum';
 import { validatePhone } from '@/hooks/useValidation';
 import { validateSms } from '@/api/modules/system';

+ 18 - 2
src/pagesSystem/pages/pay/pay.vue

@@ -34,17 +34,33 @@
         </uv-radio-group>
       </view>
       <view class="btn-wrap">
-        <uv-button type="primary" @click="handlePay">确认支付 ¥{{ formatPrice }}</uv-button>
+        <uv-button type="primary" :loading="loading" loading-color="#FFFFFF" :text="`确认支付 ¥${formatPrice}`"
+          @click="handlePay"></uv-button>
       </view>
     </view>
   </ie-page>
 </template>
 <script lang="ts" setup>
 import { usePay } from '@/hooks/usePay';
-const { ready, price, formatPrice, pay } = usePay();
+import { useUserStore } from '@/store/userStore';
+
+const userStore = useUserStore();
+const { ready, formatPrice, pay, setCallback, loading } = usePay();
 const isIOS = ref(false);
 const payType = ref('wechat');
+
 const handlePay = () => {
+  setCallback({
+    onSuccess: () => {
+      uni.$ie.showSuccess('支付成功');
+      setTimeout(() => {
+        userStore.checkInfoStudentComplete();
+      }, 500);
+    },
+    onFailed: () => {
+      uni.$ie.showError('支付失败');
+    }
+  });
   pay();
 }
 </script>

+ 1 - 1
src/store/dictStore.ts

@@ -35,7 +35,7 @@ export const useDictStore = defineStore('ie-dict', {
       }
       return await this.loadDicts([dictName]);
     },
-    getDictLabel(dictName: string, value: string | number) {
+    getDictLabel(dictName: string, value?: string | number) {
       if (!value) {
         return value;
       }

+ 16 - 0
src/store/payStore.ts

@@ -0,0 +1,16 @@
+export const usePayStore = defineStore('ie-pay', {
+  state: () => ({
+    price: 468000,
+  }),
+  actions: {
+    setPrice(price: number) {
+      this.price = price;
+    }
+  },
+  persist: {
+    storage: {
+      getItem: uni.getStorageSync,
+      setItem: uni.setStorageSync,
+    }
+  }
+})

+ 39 - 3
src/store/userStore.ts

@@ -1,6 +1,6 @@
 import { defineStore } from 'pinia';
 import { useTransferPage } from '@/hooks/useTransferPage';
-import { ref, computed } from 'vue';
+import { getUserBindCard } from '@/api/modules/user';
 import { getUserInfo } from '@/api/modules/login';
 import config from '@/config';
 
@@ -11,7 +11,7 @@ import defaultAvatar from '@/static/personal/avatar_default.png'
 
 // @ts-ignore
 import { useUserStore as useOldUserStore } from '@/hooks/useUserStore';
-import { CardType, EnumExamType, EnumReviewMode, EnumUserType } from '@/common/enum';
+import { CardType, EnumBindScene, EnumExamType, EnumReviewMode, EnumUserBindCardStatus, EnumUserType } from '@/common/enum';
 import { OPEN_VIP_POPUP } from '@/types/injectionSymbols';
 import { getDirectedSchool, saveDirectedSchool } from '@/api/modules/study';
 const oldUserStore = useOldUserStore()
@@ -183,7 +183,11 @@ export const useUserStore = defineStore('ie-user', {
         resolve(true);
       });
     },
-    checkInfoComplete() {
+    /**
+     * 检查老师是否完善信息
+     * @returns 
+     */
+    checkInfoTeacherComplete(): Promise<boolean> {
       return new Promise((resolve, reject) => {
         if (this.needCompleteInfo && (!this.userInfo.location || !this.userInfo.examType || !this.userInfo.endYear)) {
           const { transferTo } = useTransferPage();
@@ -197,6 +201,38 @@ export const useUserStore = defineStore('ie-user', {
         }
       });
     },
+    /**
+     * 检查学生是否需要完善信息
+     * @returns 
+     */
+    checkInfoStudentComplete(): Promise<boolean> {
+      return new Promise(async (resolve, reject) => {
+        if (this.user?.bindStatus === EnumUserBindCardStatus.BINDING) {
+          const { data } = await getUserBindCard();
+          if (data && data.cardNo && data.password) {
+            const submitInfo = {
+              token: this.accessToken,
+              scene: EnumBindScene.LOGIN_BIND,
+              userInfo: this.userInfo,
+              cardInfo: data,
+              registerInfo: {
+                username: data.cardNo,
+                password: data.password,
+              }
+            };
+            const { transferTo } = useTransferPage();
+            transferTo('/pagesSystem/pages/bind-profile/bind-profile', {
+              data: submitInfo
+            }).then(res => {
+              resolve(res as boolean);
+            }).catch(() => {
+              resolve(false);
+            });
+          }
+        }
+        return resolve(true);
+      });
+    },
     async getUserInfo() {
       const res = await getUserInfo();
       const { data, isDefaultModifyPwd, isPasswordExpired, card, org } = res;

+ 17 - 0
src/types/pay.ts

@@ -6,4 +6,21 @@ export interface EcardPrice {
   examType: string;
   createdAt: string;
   updatedAt: string;
+}
+
+export interface CreateOrderResult {
+  h5url: string;
+  outTradeNo: string;
+}
+
+export interface OrderPayStatus {
+  isUnPaid: boolean;
+  isPayFailed: boolean;
+  isPaySuccess: boolean;
+}
+
+export interface OrderPayCallback {
+  onSuccess?: () => void;
+  onFailed?: () => void;
+  onUnpaid?: () => void;
 }

+ 12 - 12
src/types/user.ts

@@ -1,4 +1,4 @@
-import { CardType, EnumExamType, EnumSmsType, EnumUserType } from "@/common/enum";
+import { CardType, EnumExamType, EnumSmsType, EnumUserBindCardStatus, EnumUserType } from "@/common/enum";
 
 export interface LoginInfo {
   accessToken: string;
@@ -67,7 +67,8 @@ export interface CardInfo {
   className?: string;
   endYear?: number;
   password: string;
-
+  schoolId?: number;
+  schoolName?: string;
 }
 
 export interface RegisterInfo {
@@ -88,12 +89,6 @@ export interface RegisterInfo {
   uuid: string;
 }
 
-export interface BindCardInfo extends RegisterInfo {
-  cardNo?: string;
-  // 临时信息
-  schoolClassName?: string;
-}
-
 export interface Scores {
   biology?: number;
   chemistry?: number;
@@ -142,6 +137,7 @@ export interface UserInfo {
   endYear: number;
   examType: EnumExamType;
   examMajor?: string;
+  examMajorName?: string;
   inviteCode?: string;
   location: string;
   nickName: string;
@@ -159,6 +155,7 @@ export interface UserInfo {
   campusClassName?: string;
   campusName?: string;
   classSelect: number; // 0: 不可修改班级 1: 可修改班级
+  bindStatus?: EnumUserBindCardStatus
 }
 
 export interface VipCardInfo {
@@ -175,7 +172,10 @@ export interface VipCardInfo {
 
 export type UserRole = 'vip' | 'normal' | 'guest' | 'teacher' | 'agent' | 'auditor'
 
-// export interface BindCardInfo {
-//   cardNo: string;
-//   password: string;
-// }
+/**
+ * 线上支付时用来判断是否需要完善信息,跟BindCardInfo用途不一样
+ */
+export interface UserBindCardInfo {
+  cardNo: string;
+  password: string;
+}

+ 3 - 0
src/uni_modules/uv-button/components/uv-button/uv-button.vue

@@ -229,6 +229,9 @@ export default {
 				}
 			},
 			loadingColor() {
+        if (this.loadingColor) {
+          return this.loadingColor;
+        }
 				if (this.plain) {
 					// 如果有设置color值,则用color值,否则使用type主题颜色
 					return this.color ? this.color : '#3c9cff';