Selaa lähdekoodia

merge conflict

abpcoder 1 kuukausi sitten
vanhempi
commit
2e67917855
39 muutettua tiedostoa jossa 780 lisäystä ja 853 poistoa
  1. 0 1
      .eslintrc-auto-import.json
  2. 1 0
      .gitignore
  3. 4 0
      src/App.vue
  4. 9 1
      src/common/routes.ts
  5. 15 27
      src/components/ie-page/components/vip-popup.vue
  6. 1 5
      src/components/ie-page/ie-page.vue
  7. 1 1
      src/components/ie-popup/ie-popup.vue
  8. 73 75
      src/pagesMain/pages/index/components/index-map.vue
  9. 5 0
      src/pagesMain/pages/index/index.vue
  10. 1 1
      src/pagesMain/pages/splash/splash.vue
  11. 8 6
      src/pagesOther/pages/career/detail/components/related-jobs.vue
  12. 85 80
      src/pagesOther/pages/skill/index/index.vue
  13. 25 27
      src/pagesOther/pages/skill/result/result.vue
  14. 11 0
      src/pagesOther/pages/university/detail/components/plan-enroll-list.vue
  15. 3 1
      src/pagesOther/pages/university/detail/detail.vue
  16. 2 6
      src/pagesOther/pages/university/index/components/college-list.vue
  17. 23 23
      src/pagesOther/pages/university/index/components/college-rank.vue
  18. 49 51
      src/pagesOther/pages/university/index/components/plus/college-item.vue
  19. 16 3
      src/pagesOther/pages/university/index/index.vue
  20. 1 1
      src/pagesOther/pages/university/picker/picker.vue
  21. 109 112
      src/pagesOther/pages/voluntary/index/components/voluntary-form-core.vue
  22. 0 2
      src/pagesOther/pages/voluntary/index/components/voluntary-form-simulate.vue
  23. 7 0
      src/pagesOther/pages/voluntary/index/index.vue
  24. 29 28
      src/pagesOther/pages/voluntary/list/components/voluntary-item.vue
  25. 58 0
      src/pagesOther/pages/voluntary/list/components/voluntary-majors-draggable-list.vue
  26. 0 214
      src/pagesOther/pages/voluntary/list/components/voluntary-majors-draggable.vue
  27. 145 134
      src/pagesOther/pages/voluntary/list/list.vue
  28. 29 32
      src/pagesOther/pages/voluntary/result/components/voluntary-result-compare.vue
  29. 2 2
      src/pagesStudy/pages/index/compoentns/index-practice-entry.vue
  30. 0 7
      src/pagesStudy/pages/index/index.vue
  31. 7 3
      src/pagesStudy/pages/knowledge-practice/knowledge-practice.vue
  32. 4 1
      src/pagesSystem/pages/login/login.vue
  33. 14 7
      src/pagesSystem/pages/webview/webview.vue
  34. 1 1
      src/types/injectionSymbols.ts
  35. 1 0
      src/types/study.ts
  36. 7 0
      src/types/transfer.ts
  37. 6 0
      src/uni_modules/uv-icon/components/uv-icon/uv-icon.vue
  38. 27 0
      src/utils/update.ts
  39. 1 1
      vite.config.js

+ 0 - 1
.eslintrc-auto-import.json

@@ -19,7 +19,6 @@
     "WritableComputedRef": true,
     "acceptHMRUpdate": true,
     "computed": true,
-    "createApp": true,
     "createPinia": true,
     "customRef": true,
     "defineAsyncComponent": true,

+ 1 - 0
.gitignore

@@ -68,3 +68,4 @@ package-lock.json
 *bak
 .vscode
 dist
+.hbuilderx

+ 4 - 0
src/App.vue

@@ -1,10 +1,14 @@
 <script>
 import { useAppStore } from '@/store/appStore';
+import { checkUpdate } from "@/utils/update.ts";
 export default {
   onLaunch: function () {
     console.log('App Launch')
     const appStore = useAppStore();
     appStore.init();
+    // #ifdef MP-WEIXIN
+    checkUpdate();
+    // #endif
   },
   onShow: function () {
     console.log('App Show')

+ 9 - 1
src/common/routes.ts

@@ -131,7 +131,15 @@ export const routes = {
   /**
    * mbti报告
    */
-  pageMbti: '/pagesOther/pages/test-center/mbti/mbti'
+  pageMbti: '/pagesOther/pages/test-center/mbti/mbti',
+  /**
+   * 登录
+   */
+  pageLogin: '/pagesSystem/pages/login/login',
+  /**
+   * 首页
+   */
+  pageIndex: '/pagesMain/pages/index/index',
 
 } as const;
 

+ 15 - 27
src/components/ie-page/components/vip-popup.vue

@@ -1,32 +1,20 @@
 <template>
-  <!-- #ifdef H5 -->
-  <teleport to="body">
-    <!-- #endif -->
-    <!-- #ifdef MP-WEIXIN -->
-    <root-portal externalClass="theme-ie">
-      <!-- #endif -->
-      <uv-popup ref="popupRef" mode="center" :close-on-click-overlay="true" :closeable="false" :round="16">
-        <view class="theme-ie w-[88vw] box-border px-54 pt-60 pb-80 bg-white text-center relative overflow-hidden">
-          <view class="relative z-1">
-            <view class="text-40 text-fore-title font-bold">
-              <text>当前无</text>
-              <text class="text-primary">VIP</text>
-              <text>权限</text>
-            </view>
-            <view class="mt-10 text-32 text-fore-light">开通会员,立即畅享专属权益与服务</view>
-            <!-- <ie-button custom-class="mt-60" @click="handleBuy">升级VIP权限</ie-button> -->
-            <ie-button type="info" custom-class="mt-40" @click="handleActivate">已线下购买,去激活</ie-button>
-          </view>
-          <ie-image :is-oss="true" src="/study-bg14.png" custom-class="absolute bottom-0 left-0 w-full h-full z-0"
-            mode="aspectFill" />
+  <ie-popup ref="popupRef" mode="center">
+    <view class="w-[88vw] box-border px-54 pt-60 pb-80 bg-white text-center relative overflow-hidden">
+      <view class="relative z-1">
+        <view class="text-40 text-fore-title font-bold">
+          <text>当前无</text>
+          <text class="text-primary">VIP</text>
+          <text>权限</text>
         </view>
-      </uv-popup>
-      <!-- #ifdef MP-WEIXIN -->
-    </root-portal>
-    <!-- #endif -->
-    <!-- #ifdef H5 -->
-  </teleport>
-  <!-- #endif -->
+        <view class="mt-10 text-32 text-fore-light">开通会员,立即畅享专属权益与服务</view>
+        <!-- <ie-button custom-class="mt-60" @click="handleBuy">升级VIP权限</ie-button> -->
+        <ie-button type="info" custom-class="mt-40" @click="handleActivate">已线下购买,去激活</ie-button>
+      </view>
+      <ie-image :is-oss="true" src="/study-bg14.png" custom-class="absolute bottom-0 left-0 w-full h-full z-0"
+        mode="aspectFill" />
+    </view>
+  </ie-popup>
 </template>
 <script lang="ts" setup>
 import { useTransferPage } from '@/hooks/useTransferPage';

+ 1 - 5
src/components/ie-page/ie-page.vue

@@ -81,13 +81,9 @@ const addListener = () => {
 const removeListener = () => {
   uni.$off(EnumEvent.OPEN_VIP_POPUP);
 }
-onMounted(() => {
+onShow(() => {
   addListener();
 });
-onBeforeUnmount(() => {
-  removeListener();
-});
-
 onHide(() => {
   removeListener();
 });

+ 1 - 1
src/components/ie-popup/ie-popup.vue

@@ -7,7 +7,7 @@
       <!-- #endif -->
       <uv-popup ref="popupRef" :mode="mode" :round="round" popup-class="theme-ie"
         :close-on-click-overlay="closeOnClickOverlay" :safeAreaInsetBottom="mode === 'bottom'" @close="handleClose">
-        <template v-if="showToolbar">
+        <template v-if="mode === 'bottom' && showToolbar">
           <ie-popup-toolbar :title="title" :cancelText="cancelText" :confirmText="confirmText" :showCancel="showCancel"
             :showConfirm="showConfirm" @cancel="handleCancel" @confirm="handleConfirm" />
         </template>

+ 73 - 75
src/pagesMain/pages/index/components/index-map.vue

@@ -1,110 +1,108 @@
 <template>
-    <view class="mx-30 mt-32 bg-white shadow-card overflow-hidden rounded-15 relative">
-        <view class="w-full h-150 bg-[#F7FEE7] absolute"/>
-        <view class="flex justify-between z-1 relative">
-            <view class="py-14 pl-40 pr-80 bg-white"
-                  style="clip-path: polygon(0 0, calc(100% - 40rpx) 0, 100% 80rpx, 100% 100%, 0 100%);">
-                <ie-image src="/map-title.png" is-oss custom-class="w-182 h-57"/>
-            </view>
-            <view class="flex-1 flex justify-center items-center text-24 text-fore-title">
-                分步拆解,指引升学每一步
-            </view>
+  <view class="mx-30 mt-32 bg-white shadow-card overflow-hidden rounded-15 relative">
+    <view class="w-full h-150 bg-[#F7FEE7] absolute" />
+    <view class="flex justify-between z-1 relative">
+      <view class="py-14 pl-40 pr-80 bg-white"
+        style="clip-path: polygon(0 0, calc(100% - 40rpx) 0, 100% 80rpx, 100% 100%, 0 100%);">
+        <ie-image src="/map-title.png" is-oss custom-class="w-182 h-57" />
+      </view>
+      <view class="flex-1 flex justify-center items-center text-24 text-fore-title">
+        分步拆解,指引升学每一步
+      </view>
+    </view>
+    <view class="bg-white rounded-tr-15 z-1 relative px-12 py-20 grid grid-cols-4 gap-x-12 gap-y-20">
+      <view v-for="(m, i) in maps" :key="i" class="flex flex-col items-center" @click="handleMap(m)">
+        <view class="px-24 leading-36 text-20 text-fore-title font-bold bg-[#CEF57B] rounded-full z-1"
+          style="margin-bottom: -18rpx">
+          第{{ i + 1 }}步
         </view>
-        <view class="bg-white rounded-tr-15 z-1 relative px-12 py-20 grid grid-cols-4 gap-x-12 gap-y-20">
-            <view v-for="(m,i) in maps" :key="i" class="flex flex-col items-center" @click="handleMap(m)">
-                <view class="px-24 leading-36 text-20 text-fore-title font-bold bg-[#CEF57B] rounded-full z-1"
-                      style="margin-bottom: -18rpx">
-                    第{{ i + 1 }}步
-                </view>
-                <view class="w-full bg-back-light rounded-10 flex flex-col items-center pt-36 pb-28"
-                      style="box-shadow: 1px 2px 0px 0px #DCF8BC;">
-                    <view>
-                        <view class="text-24 text-fore-title flex items-center gap-12">
-                            <view class="font-bold">{{ m.title }}</view>
-                            <view class="bg-black rounded-full p-4">
-                              <uv-icon name="arrow-right" color="white" size="6" />
-                            </view>
-                        </view>
-                        <view class="mt-3 text-22 text-fore-tip">{{ m.desc }}</view>
-                    </view>
-                </view>
+        <view class="w-full bg-back-light rounded-10 flex flex-col items-center pt-36 pb-28"
+          style="box-shadow: 1px 2px 0px 0px #DCF8BC;">
+          <view>
+            <view class="text-24 text-fore-title flex items-center gap-12">
+              <view class="font-bold">{{ m.title }}</view>
+              <view class="bg-black rounded-full p-4">
+                <uv-icon name="arrow-right" color="white" size="6" />
+              </view>
             </view>
+            <view class="mt-3 text-22 text-fore-tip">{{ m.desc }}</view>
+          </view>
         </view>
+      </view>
     </view>
+  </view>
 </template>
 
 <script setup lang="ts">
-import {routes} from "@/common/routes";
-import {useTransferPage} from "@/hooks/useTransferPage";
-import {useUserStore} from "@/store/userStore";
+import { routes } from "@/common/routes";
+import { useTransferPage } from "@/hooks/useTransferPage";
+import { useUserStore } from "@/store/userStore";
 
 interface SiteMap {
-    title: string;
-    desc: string;
-    pagePath: string;
-    handler?: () => void
+  title: string;
+  desc: string;
+  pagePath: string;
+  handler?: () => void
 }
 
 const userStore = useUserStore()
-const {transferTo} = useTransferPage()
+const { transferTo } = useTransferPage()
 
-const goSimulate = () => {
+const goSimulate = async () => {
+  const isLogin = await userStore.checkLogin();
+  if (isLogin) {
     const list = userStore.directedSchoolList || []
     const first = list[0] || {}
     if (!list.length || first.notice) {
-        transferTo(routes.studyIndex)
+      transferTo(routes.studyIndex)
     } else {
-        transferTo(routes.studySimulate, {
-            data: first
-        });
+      transferTo(routes.studySimulate, {
+        data: first
+      });
     }
+  }
 }
 
 const maps = computed<SiteMap[]>(() => [{
-    title: '本省规则',
-    desc: '填志愿不踩坑',
-    pagePath: routes.newsDetail + '?id=' + (userStore.isHN ? 1065 : 1078)
+  title: '本省规则',
+  desc: '填志愿不踩坑',
+  pagePath: routes.newsDetail + '?id=' + (userStore.isHN ? 1065 : 1078)
 }, {
-    title: '自我评价',
-    desc: '了解自身优势',
-    pagePath: ''
+  title: '自我评价',
+  desc: '了解自身优势',
+  pagePath: ''
 }, {
-    title: '职业规划',
-    desc: '锁定职业方向',
-    pagePath: routes.careerIndex
+  title: '职业规划',
+  desc: '锁定职业方向',
+  pagePath: routes.careerIndex
 }, {
-    title: '了解专业',
-    desc: '选择对口专业',
-    pagePath: routes.majorIndex
+  title: '了解专业',
+  desc: '选择对口专业',
+  pagePath: routes.majorIndex
 }, {
-    title: '锁定院校',
-    desc: '了解院校实力',
-    pagePath: routes.universityIndex
+  title: '锁定院校',
+  desc: '了解院校实力',
+  pagePath: routes.universityIndex
 }, {
-    title: '定向刷题',
-    desc: '根据考纲练习',
-    pagePath: routes.studyIndex
+  title: '定向刷题',
+  desc: '根据考纲练习',
+  pagePath: routes.studyIndex
 }, {
-    title: '模拟测试',
-    desc: '全真模拟',
-    pagePath: '',
-    handler: goSimulate
+  title: '模拟测试',
+  desc: '全真模拟',
+  pagePath: '',
+  handler: goSimulate
 }, {
-    title: '测录取率',
-    desc: '录取风险评估',
-    pagePath: routes.voluntaryIndex
+  title: '测录取率',
+  desc: '录取风险评估',
+  pagePath: routes.voluntaryIndex
 }])
 
 const handleMap = (m: SiteMap) => {
-    if (m.handler) return m.handler()
-    if (!m.pagePath) return
-    transferTo(m.pagePath)
+  if (m.handler) return m.handler()
+  if (!m.pagePath) return
+  transferTo(m.pagePath)
 }
-
-
-onMounted(() => userStore.getDirectedSchoolList())
 </script>
 
-<style scoped>
-
-</style>
+<style scoped></style>

+ 5 - 0
src/pagesMain/pages/index/index.vue

@@ -109,6 +109,11 @@ onShow(() => {
   }, 500);
   isHide.value = false;
 });
+onLoad(() => {
+  if (userStore.isLogin) {
+    userStore.getDirectedSchoolList();
+  }
+});
 </script>
 
 <style lang="scss" scoped></style>

+ 1 - 1
src/pagesMain/pages/splash/splash.vue

@@ -21,7 +21,7 @@ import { load } from '@/utils/loadFont';
 const splashTimeout = 1200;
 const appStore = useAppStore();
 const userStore = useUserStore();
-const { transferTo } = useTransferPage();
+const { transferTo, routes } = useTransferPage();
 // #ifdef H5
 uni.hideTabBar();
 // #endif

+ 8 - 6
src/pagesOther/pages/career/detail/components/related-jobs.vue

@@ -83,12 +83,14 @@ const customStyle = {
 watch(() => props.name, (newVal) => {
   if (newVal) {
     const index = list.value.findIndex(item => item.name === newVal);
-    getJobDetail(index);
-    setTimeout(() => {
-      nextTick(() => {
-        current.value = index;
-      });
-    }, 350);
+    if (index !== -1) {
+      getJobDetail(index);
+      setTimeout(() => {
+        nextTick(() => {
+          current.value = index;
+        });
+      }, 350);
+    }
   }
 });
 

+ 85 - 80
src/pagesOther/pages/skill/index/index.vue

@@ -1,99 +1,106 @@
 <template>
-    <ie-page bg-color="#F6F8FA">
-        <ie-navbar title="职业技能分测算" transparent bg-color="#FFFFFF" title-color="black" keep-title-color/>
-        <!-- #ifdef MP-WEIXIN -->
-        <uv-gap height="44"/>
-        <!-- #endif -->
-        <ie-image is-oss src="/volunteer/skill/index/bg.png" custom-class="w-full h-600"/>
-        <view class="-mt-420 z-1">
-            <view class="px-36 flex justify-between items-center">
-                <view class="text-3xl text-primary keep-all">
-                    <view class="mb-10 font-bold">测职业技能分</view>
-                    <view class="text-base">输入分数,测算所需职业技能分</view>
-                </view>
-                <ie-image is-oss src="/volunteer/skill/index/banner.png" custom-class="w-208 h-208"/>
-            </view>
-            <view class="mt-60 mx-30 bg-white rounded-xl p-35">
-                <view class="flex justify-between items-center">
-                    <view class="text-lg text-fore-title">报考院校专业</view>
-                    <view class="text-base text-fore-placeholder flex items-center" @click="handleSelect">
-                        <text>请选择</text>
-                        <uv-icon name="arrow-right" color="info"/>
-                    </view>
-                </view>
-                <ie-empty v-if="!rules.length" :image="emptyImg" text="请选择你的报考院校专业~"/>
-                <voluntary-form v-else ref="form" disable-simulate/>
-            </view>
+  <ie-page bg-color="#F6F8FA">
+    <ie-navbar title="职业技能分测算" transparent bg-color="#FFFFFF" title-color="black" keep-title-color />
+    <!-- #ifdef MP-WEIXIN -->
+    <uv-gap height="44" />
+    <!-- #endif -->
+    <ie-image is-oss src="/volunteer/skill/index/bg.png" custom-class="w-full h-600" />
+    <view class="-mt-420 z-1">
+      <view class="px-36 flex justify-between items-center">
+        <view class="text-3xl text-primary keep-all">
+          <view class="mb-10 font-bold">测职业技能分</view>
+          <view class="text-base">输入分数,测算所需职业技能分</view>
         </view>
-        <ie-safe-toolbar :height="84" :shadow="false">
-            <view class="px-30 py-16">
-                <ie-button @click="handleSubmit">开始计算</ie-button>
-            </view>
-        </ie-safe-toolbar>
-    </ie-page>
+        <ie-image is-oss src="/volunteer/skill/index/banner.png" custom-class="w-208 h-208" />
+      </view>
+      <view class="mt-60 mx-30 bg-white rounded-xl p-35">
+        <view class="flex justify-between items-center">
+          <view class="text-lg text-fore-title">报考院校专业</view>
+          <view class="text-base text-fore-placeholder flex items-center" @click="handleSelect">
+            <text>请选择</text>
+            <uv-icon name="arrow-right" color="info" />
+          </view>
+        </view>
+        <ie-empty v-if="!rules.length" :image="emptyImg" text="请选择你的报考院校专业~" />
+        <voluntary-form v-else ref="form" disable-simulate />
+      </view>
+    </view>
+    <ie-safe-toolbar :height="84" :shadow="false">
+      <view class="px-30 py-16">
+        <ie-button @click="handleSubmit">开始计算</ie-button>
+      </view>
+    </ie-safe-toolbar>
+  </ie-page>
 </template>
 
 <script setup lang="ts">
 import VoluntaryForm from "@/pagesOther/pages/voluntary/index/components/voluntary-form.vue";
-import {useTransferPage} from "@/hooks/useTransferPage";
-import {routes} from "@/common/routes";
-import {VOLUNTARY_TARGET, VOLUNTARY_RULES, VOLUNTARY_MODEL} from "@/types/injectionSymbols";
-import {EnrollRule, SelectedUniversityMajor, VoluntaryDto, VoluntaryModel, VoluntaryResult} from "@/types/voluntary";
+import { useTransferPage } from "@/hooks/useTransferPage";
+import { routes } from "@/common/routes";
+import { VOLUNTARY_TARGET, VOLUNTARY_RULES, VOLUNTARY_MODEL } from "@/types/injectionSymbols";
+import { EnrollRule, SelectedUniversityMajor, VoluntaryDto, VoluntaryModel, VoluntaryResult } from "@/types/voluntary";
 import config from "@/config";
-import {UniversityPickerPageOptions} from "@/types/transfer";
-import {getSkillRules, postSkillRules} from "@/api/modules/voluntary";
+import { UniversityPickerPageOptions } from "@/types/transfer";
+import { getSkillRules, postSkillRules } from "@/api/modules/voluntary";
+import { useAuth } from "@/hooks/useAuth";
+import { EnumUserRole } from "@/common/enum";
 
+const { hasPermission } = useAuth();
 const emptyImg = computed(() => config.ossUrl + '/volunteer/voluntary/index/empty_data.png')
 
 const form = ref<InstanceType<typeof VoluntaryForm>>()
 const target = ref<SelectedUniversityMajor>({} as SelectedUniversityMajor)
 const rules = ref<EnrollRule[]>([])
 const model = ref<VoluntaryModel>({})
-const {transferTo} = useTransferPage()
+const { transferTo } = useTransferPage()
 
 const handleSelect = async () => {
-    const option: UniversityPickerPageOptions = {
-        title: '选择你的报考院校专业',
-        fromVoluntary: true,
-        selectedUniversityId: target.value?.universityId,
-        selectedMajorId: target.value?.majorId
-    }
-    const picked = await transferTo(routes.targetPicker, {data: option})
-    if (!picked) return
-    target.value = picked as SelectedUniversityMajor
-    uni.$ie.showLoading()
-    try {
-        // reset
-        model.value = {}
-        rules.value = []
-        // request render rules
-        const res = await getSkillRules(target.value)
-        rules.value = res.data
-        // init model
-        rules.value.forEach((r) => {
-            r.details?.forEach((d) => {
-                if (d.options?.length) {
-                    model.value[d.fieldName] = d.defaultValue ? d.defaultValue : ''
-                    model.value[d.fieldName + 'Total'] = d.options ? d.options[0] : null
-                }
-            })
-        })
-    } catch (e) {
-        console.log('getRenderRules ex', e, target.value)
-        target.value = {} as SelectedUniversityMajor // clear for re-pick
-    } finally {
-        uni.$ie.hideLoading()
-    }
+  const option: UniversityPickerPageOptions = {
+    title: '选择你的报考院校专业',
+    fromVoluntary: true,
+    selectedUniversityId: target.value?.universityId,
+    selectedMajorId: target.value?.majorId
+  }
+  const picked = await transferTo(routes.targetPicker, { data: option })
+  if (!picked) return
+  target.value = picked as SelectedUniversityMajor
+  uni.$ie.showLoading()
+  try {
+    // reset
+    model.value = {}
+    rules.value = []
+    // request render rules
+    const res = await getSkillRules(target.value)
+    rules.value = res.data
+    // init model
+    rules.value.forEach((r) => {
+      r.details?.forEach((d) => {
+        if (d.options?.length) {
+          model.value[d.fieldName] = d.defaultValue ? d.defaultValue : ''
+          model.value[d.fieldName + 'Total'] = d.options ? d.options[0] : null
+        }
+      })
+    })
+  } catch (e) {
+    console.log('getRenderRules ex', e, target.value)
+    target.value = {} as SelectedUniversityMajor // clear for re-pick
+  } finally {
+    uni.$ie.hideLoading()
+  }
 }
 
 const handleSubmit = async () => {
-    if (!target.value.universityId||!target.value.majorId) return uni.$ie.showToast('请选择院校专业')
-    if (!rules.value.length) return uni.$ie.showToast('由于官方未公布历年录取分数或计划变更,暂时无法计算技能分')
-    await form.value?.validate()
-    // make request
-    const {data: result} = await postSkillRules(target.value, model.value)
-    const bigData: VoluntaryDto = {target: target.value, rules: rules.value, model: model.value, result}
-    transferTo(routes.skillResult, {bigData})
+  const hasAuth = hasPermission([EnumUserRole.VIP]);
+  if (!hasAuth) {
+    return;
+  }
+  if (!target.value.universityId || !target.value.majorId) return uni.$ie.showToast('请选择院校专业')
+  if (!rules.value.length) return uni.$ie.showToast('由于官方未公布历年录取分数或计划变更,暂时无法计算技能分')
+  await form.value?.validate()
+  // make request
+  const { data: result } = await postSkillRules(target.value, model.value)
+  const bigData: VoluntaryDto = { target: target.value, rules: rules.value, model: model.value, result }
+  transferTo(routes.skillResult, { bigData })
 }
 
 provide(VOLUNTARY_TARGET, target)
@@ -105,6 +112,4 @@ onPageScroll(() => {
 })
 </script>
 
-<style scoped>
-
-</style>
+<style scoped></style>

+ 25 - 27
src/pagesOther/pages/skill/result/result.vue

@@ -1,43 +1,43 @@
 <template>
-    <ie-page bg-color="#F6F8FA">
-        <ie-navbar title="测职业技能分"/>
-        <ie-image is-oss src="/volunteer/skill/index/bg.png" custom-class="w-full h-600 absolute"/>
-        <view class="p-28 z-2">
-            <view class="rounded-xl overflow-hidden">
-                <college-item :item="target.info" hidden-star reverse/>
-                <uv-line/>
-                <college-summary :college="target.info"/>
-            </view>
-            <skill-result />
-        </view>
-        <ie-safe-toolbar :height="84" :shadow="false">
-            <view class="px-30 py-16">
-                <ie-button @click="handleSubmit">加入志愿表</ie-button>
-            </view>
-        </ie-safe-toolbar>
-    </ie-page>
+  <ie-page bg-color="#F6F8FA">
+    <ie-navbar title="测职业技能分" />
+    <ie-image is-oss src="/volunteer/skill/index/bg.png" custom-class="w-full h-600 absolute" />
+    <view class="p-28 z-2">
+      <view v-if="target.info" class="rounded-xl overflow-hidden">
+        <college-item :item="target.info" hidden-star reverse />
+        <uv-line />
+        <college-summary :college="target.info" />
+      </view>
+      <skill-result />
+    </view>
+    <ie-safe-toolbar :height="84" :shadow="false">
+      <view class="px-30 py-16">
+        <ie-button @click="handleSubmit">加入志愿表</ie-button>
+      </view>
+    </ie-safe-toolbar>
+  </ie-page>
 </template>
 
 <script setup lang="ts">
 
-import {VOLUNTARY_TARGET, VOLUNTARY_RULES, VOLUNTARY_MODEL, VOLUNTARY_RESULT} from "@/types/injectionSymbols";
-import {useTransferPage} from "@/hooks/useTransferPage";
-import {VoluntaryDto} from "@/types/voluntary";
+import { VOLUNTARY_TARGET, VOLUNTARY_RULES, VOLUNTARY_MODEL, VOLUNTARY_RESULT } from "@/types/injectionSymbols";
+import { useTransferPage } from "@/hooks/useTransferPage";
+import { VoluntaryDto } from "@/types/voluntary";
 import VoluntaryFormMajor from "@/pagesOther/pages/voluntary/index/components/voluntary-form-major.vue";
 import CollegeItem from "@/pagesOther/pages/university/index/components/plus/college-item.vue";
 import CollegeSummary from "@/pagesOther/pages/university/index/components/plus/college-summary.vue";
 import SkillResult from "@/pagesOther/pages/skill/result/components/skill-result.vue";
-import {addVoluntary} from "@/api/modules/voluntary";
+import { addVoluntary } from "@/api/modules/voluntary";
 
-const {prevData} = useTransferPage<VoluntaryDto, any>()
+const { prevData } = useTransferPage<VoluntaryDto, any>()
 const target = computed(() => prevData.value.target || {})
 const rules = computed(() => prevData.value.rules || [])
 const model = computed(() => prevData.value.model || {})
 const result = computed(() => prevData.value.result || {})
 
 const handleSubmit = async () => {
-    await addVoluntary(target.value)
-    uni.$ie.showSuccess('保存成功')
+  await addVoluntary(target.value)
+  uni.$ie.showSuccess('保存成功')
 }
 
 provide(VOLUNTARY_TARGET, target)
@@ -49,6 +49,4 @@ onPageScroll(() => {
 })
 </script>
 
-<style lang="scss">
-
-</style>
+<style lang="scss"></style>

+ 11 - 0
src/pagesOther/pages/university/detail/components/plan-enroll-list.vue

@@ -67,7 +67,10 @@ import { addVoluntary } from "@/api/modules/voluntary";
 import { SelectedUniversityMajor } from "@/types/voluntary";
 import { useTransferPage } from "@/hooks/useTransferPage";
 import { routes } from "@/common/routes";
+import { useAuth } from "@/hooks/useAuth";
+import { EnumUserRole } from "@/common/enum";
 
+const { hasPermission } = useAuth();
 
 const props = withDefaults(defineProps<{
   mode: HistoryMode;
@@ -142,12 +145,20 @@ const handleRuleClick = (descriptor: IPlanEnrollDescriptor, history: IPlanEnroll
 }
 
 const handleAddVoluntary = async (item: IPlanEnrollHistory) => {
+  const hasAuth = hasPermission([EnumUserRole.VIP]);
+  if (!hasAuth) {
+    return;
+  }
   const { code } = baseInfo.value
   await addVoluntary({ universityId: code, majorId: item.id + '' })
   uni.$ie.showSuccess('保存成功')
 }
 
 const handleRateVoluntary = (item: IPlanEnrollHistory) => {
+  const hasAuth = hasPermission([EnumUserRole.VIP]);
+  if (!hasAuth) {
+    return;
+  }
   const selected: SelectedUniversityMajor = {
     universityId: baseInfo.value.code,
     universityLogo: baseInfo.value.logo,

+ 3 - 1
src/pagesOther/pages/university/detail/detail.vue

@@ -2,7 +2,9 @@
   <ie-page>
     <ie-navbar :title="prevData.name" transparent bg-color="#FFFFFF" title-color="black" keep-title-color />
     <uv-skeletons v-if="loading" :skeleton="skeleton" />
-    <ie-image v-else :src="baseInfo.bannerUrl || baseInfo.logo" custom-class="w-full h-[240px]" mode="aspectFill" />
+    <view v-else class="w-full h-[240px]">
+      <ie-image :src="baseInfo.bannerUrl || baseInfo.logo" custom-class="w-full h-full" mode="aspectFill" />
+    </view>
     <view class="-mt-60 z-1 rounded-t-3xl p-30 bg-white">
       <college-info :info="baseInfo" :loading="loading" />
     </view>

+ 2 - 6
src/pagesOther/pages/university/index/components/college-list.vue

@@ -30,7 +30,7 @@ const props = withDefaults(defineProps<{
   customItemClick: false,
   extraFilter: () => ({})
 })
-const emits = defineEmits(['item-click'])
+const emits = defineEmits(['click'])
 
 const { prevData, transferTo } = useTransferPage()
 const paging = ref<ZPagingInstance>()
@@ -63,11 +63,7 @@ const handleQuery = (pageNum: number, pageSize: number) => {
 }
 
 const handleDetail = (u: University) => {
-  if (props.customItemClick) {
-    return emits('item-click', u)
-  }
-  const { id, code, name } = u
-  transferTo(routes.universityDetail, { data: { id, code, name } })
+  emits('click', u)
 }
 
 provide(UNIVERSITY_FILTER, queryParams)

+ 23 - 23
src/pagesOther/pages/university/index/components/college-rank.vue

@@ -1,42 +1,42 @@
 <template>
-    <view class="h-full">
-        <z-paging ref="paging" v-model="list" @query="handleQuery">
-            <view class="p-20 flex flex-col gap-20">
-                <college-item v-for="i in list" :key="i.code" :item="i" class="mx-card" @click="handleDetail(i)"/>
-            </view>
-        </z-paging>
-    </view>
+  <view class="h-full">
+    <z-paging ref="paging" v-model="list" @query="handleQuery">
+      <view class="p-20 flex flex-col gap-20">
+        <college-item v-for="i in list" :key="i.code" :item="i" class="mx-card" @click="handleDetail(i)" />
+      </view>
+    </z-paging>
+  </view>
 </template>
 
 <script setup lang="ts">
-import {useTransferPage} from "@/hooks/useTransferPage";
-import {universityList} from "@/api/modules/university";
-import {University} from "@/types/university";
-import {useUserStore} from "@/store/userStore";
+import { useTransferPage } from "@/hooks/useTransferPage";
+import { universityList } from "@/api/modules/university";
+import { University } from "@/types/university";
+import { useUserStore } from "@/store/userStore";
 import CollegeItem from "@/pagesOther/pages/university/index/components/plus/college-item.vue";
-import {routes} from "@/common/routes";
+import { routes } from "@/common/routes";
 
-const {transferTo} = useTransferPage()
+const { transferTo } = useTransferPage()
 const paging = ref<ZPagingInstance>()
 const list = ref<University[]>([])
-const {getLocation} = useUserStore()
+const { getLocation } = useUserStore()
 
 const rankTips = computed(() => `${getLocation || '湖南'}省招生院校竞争力排名`)
 
 const handleQuery = (pageNum: number, pageSize: number) => {
-    const payload = {pageNum, pageSize, filterRank: true}
-    universityList(payload)
-        .then(res => paging.value?.completeByTotal(res.rows, res.total))
-        .catch(e => paging.value?.complete(false))
+  const payload = { pageNum, pageSize, filterRank: true }
+  universityList(payload)
+    .then(res => paging.value?.completeByTotal(res.rows, res.total))
+    .catch(e => paging.value?.complete(false))
 }
 
+const emits = defineEmits<{
+  (e: 'click', college: University): void
+}>()
 const handleDetail = (college: University) => {
-    const {id, code, name} = college
-    transferTo(routes.universityDetail, {data: {id, code, name}})
+  emits('click', college)
 }
 
 </script>
 
-<style scoped>
-
-</style>
+<style scoped></style>

+ 49 - 51
src/pagesOther/pages/university/index/components/plus/college-item.vue

@@ -1,39 +1,39 @@
 <template>
-    <view class="p-20 bg-white flex justify-between items-start gap-20" @click="emits('click')">
-        <ie-image v-if="!hiddenLogo&&!reverse" :src="item.logo" mode="aspectFit" custom-class="w-120 h-120"/>
-        <view class="flex-1 flex flex-col gap-10">
-            <view v-if="showName||showStar" class="flex justify-between items-center gap-20">
-                <uv-text v-if="showName" type="main" bold :text="item.name"/>
-                <uv-tags v-if="showStar" v-bind="starBinding" :text="item.star"/>
-            </view>
-            <view v-if="bxTags.length" class="flex flex-wrap gap-8">
-                <uv-tags v-for="t in bxTags" :text="t" v-bind="getHighlightBindings(t)" @click="handleTagClick(t)"/>
-            </view>
-            <slot v-if="!hiddenAddress" name="address">
-                <uv-text type="tips" prefix-icon="empty-address" :lines="addressLines" :icon-style="{color: '#999999'}"
-                         size="12" :text="item.address"/>
-            </slot>
-        </view>
-        <ie-image v-if="!hiddenLogo&&reverse" :src="item.logo" mode="aspectFit" custom-class="w-120 h-120"/>
-        <slot name="right"/>
+  <view class="p-20 bg-white flex justify-between items-start gap-20" @click="emits('click')">
+    <ie-image v-if="!hiddenLogo && !reverse" :src="item.logo" mode="aspectFit" custom-class="w-120 h-120" />
+    <view class="flex-1 flex flex-col gap-10">
+      <view v-if="showName || showStar" class="flex justify-between items-center gap-20">
+        <uv-text v-if="showName" type="main" bold :text="item.name" />
+        <uv-tags v-if="showStar" v-bind="starBinding" :text="item.star" />
+      </view>
+      <view v-if="bxTags.length" class="flex flex-wrap gap-8">
+        <uv-tags v-for="t in bxTags" :key="t" :text="t" v-bind="getHighlightBindings(t)" @click="handleTagClick(t)" />
+      </view>
+      <slot v-if="!hiddenAddress" name="address">
+        <uv-text type="tips" prefix-icon="empty-address" :lines="addressLines" :icon-style="{ color: '#999999' }"
+          size="12" :text="item.address" />
+      </slot>
     </view>
+    <ie-image v-if="!hiddenLogo && reverse" :src="item.logo" mode="aspectFit" custom-class="w-120 h-120" />
+    <slot name="right" />
+  </view>
 </template>
 
 <script setup lang="ts">
 import _ from 'lodash';
-import {University} from "@/types/university";
+import { University } from "@/types/university";
 
 const props = withDefaults(defineProps<{
-    item: University;
-    hiddenLogo?: boolean;
-    hiddenName?: boolean;
-    hiddenStar?: boolean;
-    hiddenAddress?: boolean;
-    addressLines?: number;
-    reverse?: boolean;
+  item: University;
+  hiddenLogo?: boolean;
+  hiddenName?: boolean;
+  hiddenStar?: boolean;
+  hiddenAddress?: boolean;
+  addressLines?: number;
+  reverse?: boolean;
 }>(), {
-    item: () => ({} as University),
-    addressLines: 1
+  item: () => ({} as University),
+  addressLines: 1
 })
 const emits = defineEmits(['tag', 'click'])
 
@@ -42,50 +42,48 @@ const showName = computed(() => !props.hiddenName)
 const showStar = computed(() => !props.hiddenStar && props.item.star)
 
 const tagAttrs = {
-    type: 'info',
-    plain: true,
-    size: 'tiny',
-    'class': 'pointer-events-none'
+  type: 'info',
+  plain: true,
+  size: 'tiny',
+  'class': 'pointer-events-none'
 }
 const highlights = ['双高']
 const starBinding = {
-    ...tagAttrs,
-    type: 'warning'
+  ...tagAttrs,
+  type: 'warning'
 }
 const tagHighlight = {
-    ...tagAttrs,
-    type: 'primary',
-    plainFill: true
+  ...tagAttrs,
+  type: 'primary',
+  plainFill: true
 }
 
 const bxTags = computed(() => {
-    const {bxLevel, bxType} = props.item
-    const tags = bxLevel ? bxLevel.split(',') : []
-    if (bxType) {
-        _.pull(tags, '双高')
-        tags.push(bxType)
-    }
-    return tags
+  const { bxLevel, bxType } = props.item
+  const tags = bxLevel ? bxLevel.split(',') : []
+  if (bxType) {
+    _.pull(tags, '双高')
+    tags.push(bxType)
+  }
+  return tags
 })
 
 const isSpecialTag = (tag: string) => {
-    return !isCultural.value && tag == props.item.bxType
+  return !isCultural.value && tag == props.item.bxType
 }
 
 const isHighlight = (tag: string) => {
-    return highlights.includes(tag) || isSpecialTag(tag)
+  return highlights.includes(tag) || isSpecialTag(tag)
 }
 
 const getHighlightBindings = (tag: string) => {
-    const attrs = isHighlight(tag) ? tagHighlight : tagAttrs
-    return isSpecialTag(tag) ? {...attrs, icon: 'question-circle', reverse: true, 'class': ''} : attrs
+  const attrs = isHighlight(tag) ? tagHighlight : tagAttrs
+  return isSpecialTag(tag) ? { ...attrs, icon: 'question-circle', reverse: true, 'class': '' } : attrs
 }
 
 const handleTagClick = (tag: string) => {
-    if (isSpecialTag(tag)) emits('tag')
+  if (isSpecialTag(tag)) emits('tag')
 }
 </script>
 
-<style scoped>
-
-</style>
+<style scoped></style>

+ 16 - 3
src/pagesOther/pages/university/index/index.vue

@@ -4,20 +4,25 @@
     <ie-auto-resizer>
       <ie-tabs-swiper v-model="current" :list="tabs" :scrollable="false">
         <template #list>
-          <college-list :absolute="true" />
+          <college-list :absolute="true" @click="handleDetail" />
         </template>
         <template #rank>
-          <college-rank />
+          <college-rank @click="handleDetail" />
         </template>
       </ie-tabs-swiper>
     </ie-auto-resizer>
   </ie-page>
 </template>
 <script lang="ts" setup>
-import { SwiperTabItem } from "@/types";
+import type { SwiperTabItem, University } from "@/types";
 import CollegeList from "@/pagesOther/pages/university/index/components/college-list.vue";
 import CollegeRank from "@/pagesOther/pages/university/index/components/college-rank.vue";
+import { useTransferPage } from "@/hooks/useTransferPage";
+import { useAuth } from "@/hooks/useAuth";
+import { EnumUserRole } from "@/common/enum";
 
+const { hasPermission } = useAuth();
+const { transferTo, routes } = useTransferPage();
 const current = ref(0);
 const tabs = ref<SwiperTabItem[]>([{
   name: '院校库',
@@ -30,5 +35,13 @@ const tabs = ref<SwiperTabItem[]>([{
 const handleChangeSwiper = function (e: any) {
   current.value = e.detail.current;
 }
+const handleDetail = (u: University.University) => {
+  const hasAuth = hasPermission([EnumUserRole.VIP]);
+  if (!hasAuth) {
+    return;
+  }
+  const { id, code, name } = u
+  transferTo(routes.universityDetail, { data: { id, code, name } })
+}
 </script>
 <style lang="scss" scoped></style>

+ 1 - 1
src/pagesOther/pages/university/picker/picker.vue

@@ -1,6 +1,6 @@
 <template>
     <ie-page>
-        <college-list custom-item-click @item-click="handleItemClick">
+        <college-list @click="handleItemClick">
             <template #top>
                 <ie-navbar :title="title"/>
             </template>

+ 109 - 112
src/pagesOther/pages/voluntary/index/components/voluntary-form-core.vue

@@ -1,25 +1,24 @@
 <template>
-    <view v-if="rulesInit" class="mt-40 flex flex-col gap-30">
-        <view v-for="(r,i) in renderRules.filter(i => i.details?.length)" :key="i" class="flex flex-col gap-30">
-            <view class="text-32 font-bold">{{ r.category }}</view>
-            <uv-form ref="form" :model="model" :rules="rules" label-position="top">
-                <uv-form-item v-for="d in r.details" :key="d.fieldName" :prop="d.fieldName">
-                    <view class="flex-1 flex items-center justify-between gap-40">
-                        <text class="text-30 text-fore-title" :style="{width: getLabelWidth(r)}">{{ d.label }}</text>
-                        <uv-input v-model="model[d.fieldName]" :disabled="d.readonly"
-                                  :placeholder="d.placeholder || '请输入'"
-                                  :suffix-icon="d.readonly ? 'lock': undefined"/>
-                        <text class="text-28 text-fore-placeholder">(总分 {{ d.options.toString() }})</text>
-                    </view>
-                </uv-form-item>
-            </uv-form>
-        </view>
+  <view v-if="rulesInit && renderRules" class="mt-40 flex flex-col gap-30">
+    <view v-for="(r, i) in renderRules.filter(i => i.details?.length)" :key="i" class="flex flex-col gap-30">
+      <view class="text-32 font-bold">{{ r.category }}</view>
+      <uv-form ref="form" :model="model" :rules="rules" label-position="top">
+        <uv-form-item v-for="d in r.details" :key="d.fieldName" :prop="d.fieldName">
+          <view class="flex-1 flex items-center justify-between gap-40">
+            <text class="text-30 text-fore-title" :style="{ width: getLabelWidth(r) }">{{ d.label }}</text>
+            <uv-input v-model="model[d.fieldName]" :disabled="d.readonly" :placeholder="d.placeholder || '请输入'"
+              :suffix-icon="d.readonly ? 'lock' : undefined" />
+            <text v-if="d.options" class="text-28 text-fore-placeholder">(总分 {{ d.options.toString() }})</text>
+          </view>
+        </uv-form-item>
+      </uv-form>
     </view>
+  </view>
 </template>
 
 <script setup lang="ts">
-import {VOLUNTARY_RULES, VOLUNTARY_MODEL} from "@/types/injectionSymbols";
-import {EnrollRule, EnrollRuleItem, VoluntaryModel} from "@/types/voluntary";
+import { VOLUNTARY_RULES, VOLUNTARY_MODEL } from "@/types/injectionSymbols";
+import { EnrollRule, EnrollRuleItem, VoluntaryModel } from "@/types/voluntary";
 import _ from "lodash";
 
 const renderRules = inject(VOLUNTARY_RULES) || ref<EnrollRule[]>()
@@ -29,112 +28,110 @@ const rules = ref({})
 const rulesInit = ref(false)
 
 const getLabelWidth = function (rule: EnrollRule) {
-    const maxLen = _.maxBy(rule.details, d => d.label.length)
-    return (maxLen?.label.length || 2) * 30 + 'rpx'
+  const maxLen = _.maxBy(rule.details, d => d.label.length)
+  return (maxLen?.label.length || 2) * 30 + 'rpx'
 }
 
 const validate = () => {
-    if (!form.value) return
-    const validates = form.value.map(f => f.validate())
-    return Promise.all(validates)
+  if (!form.value) return
+  const validates = form.value.map(f => f.validate())
+  return Promise.all(validates)
 }
 
 watch([renderRules, model], ([renderRules, model]) => {
-    if (!renderRules || !model) return
-    const autoRules: Record<string, any> = {}
-    renderRules.forEach((item: EnrollRule) => {
-        item.details?.forEach((r: EnrollRuleItem) => {
-            // TODO: 此规则逻辑从旧的uni-vueuse mx-base分支迁移过来,可能不完全适用于新版本。
-            const fieldRules = []
-            // 录取规则,自动添加非空校验
-            if (r.enumRuleCategory == 'Enroll') {
-                fieldRules.push({required: true, message: `请填写${r.label}分数`})
+  if (!renderRules || !model) return
+  const autoRules: Record<string, any> = {}
+  renderRules.forEach((item: EnrollRule) => {
+    item.details?.forEach((r: EnrollRuleItem) => {
+      // TODO: 此规则逻辑从旧的uni-vueuse mx-base分支迁移过来,可能不完全适用于新版本。
+      const fieldRules = []
+      // 录取规则,自动添加非空校验
+      if (r.enumRuleCategory == 'Enroll') {
+        fieldRules.push({ required: true, message: `请填写${r.label}分数` })
+      }
+      // 分制类型的输入,要同时校验分制与得分
+      if (r.enumInputType == 'Score') {
+        fieldRules.push({
+          validator: (_r: any, _v: any, cb: (arg0: string | undefined) => void) => {
+            const fieldTotal = r.fieldName + 'Total'
+            let score = Number(model[r.fieldName]) || 0
+            let total = Number(model[fieldTotal]) || 0
+            if (!total) {
+              cb(`请选择总分`)
+              return
             }
-            // 分制类型的输入,要同时校验分制与得分
-            if (r.enumInputType == 'Score') {
-                fieldRules.push({
-                    validator: (_r: any, _v: any, cb: (arg0: string | undefined) => void) => {
-                        const fieldTotal = r.fieldName + 'Total'
-                        let score = Number(model[r.fieldName]) || 0
-                        let total = Number(model[fieldTotal]) || 0
-                        if (!total) {
-                            cb(`请选择总分`)
-                            return
-                        }
-                        if (!score) {
-                            cb(`请填写${r.label}分数`)
-                            return
-                        }
-                        if (score < 0 || score > total) {
-                            cb(`分数不能超过总分${total}`)
-                            return
-                        }
-                        cb(undefined)
-                    }
-                })
+            if (!score) {
+              cb(`请填写${r.label}分数`)
+              return
             }
-            const hasVal = (val: any) => val !== null && val !== undefined
-            if (hasVal(r.min) || hasVal(r.max)) {
-                const createRangeMsg = () => {
-                    if (r.enumInputType == 'Text') {
-                        if (hasVal(r.min) && hasVal(r.max)) return `长度必须在${r.min}-${r.max}个字符之间`
-                        if (hasVal(r.min)) return `长度至少${r.min}个字符`
-                        if (hasVal(r.max)) return `长度不超过${r.max}个字符`
-                    } else if (r.enumInputType == 'Number') {
-                        if (hasVal(r.min) && hasVal(r.max)) return `数值必须在${r.min}-${r.max}之间`
-                        if (hasVal(r.min)) return `数值不能小于${r.min}`
-                        if (hasVal(r.max)) return `数值不能大于${r.max}`
-                    } else if (r.enumInputType == 'Checkbox') {
-                        if (hasVal(r.min) && hasVal(r.max)) return `必须选择${r.min}-${r.max}项`
-                        if (hasVal(r.min)) return `至少选择${r.min}项`
-                        if (hasVal(r.max)) return `至多选择${r.max}项`
-                    }
-                }
-                const createRangeType = () => {
-                    switch (r.enumInputType) {
-                        case 'Number':
-                        case 'Score':
-                            return 'number'
-                        case 'Checkbox':
-                            return 'array'
-                        default:
-                            return 'string'
-                    }
-                }
-                const rangeRule : {
-                    type: string,
-                    min?: number,
-                    max?: number,
-                    message: string | undefined
-                    transform?: (val: any) => any
-                } = {
-                    type: createRangeType(),
-                    min: r.min,
-                    max: r.max,
-                    message: createRangeMsg()
-                }
-                if (!hasVal(r.min)) delete rangeRule.min
-                if (!hasVal(r.max)) delete rangeRule.max
-                if (r.enumInputType == 'Number')
-                    rangeRule.transform = val => hasVal(val) ? val * 1 : val
-                fieldRules.push(rangeRule)
+            if (score < 0 || score > total) {
+              cb(`分数不能超过总分${total}`)
+              return
             }
-            if (r.regex) {
-                fieldRules.push({
-                    pattern: r.regex,
-                    message: `${r.label}格式不符合要求`
-                })
-            }
-            if (fieldRules.length) autoRules[r.fieldName] = fieldRules
+            cb(undefined)
+          }
+        })
+      }
+      const hasVal = (val: any) => val !== null && val !== undefined
+      if (hasVal(r.min) || hasVal(r.max)) {
+        const createRangeMsg = () => {
+          if (r.enumInputType == 'Text') {
+            if (hasVal(r.min) && hasVal(r.max)) return `长度必须在${r.min}-${r.max}个字符之间`
+            if (hasVal(r.min)) return `长度至少${r.min}个字符`
+            if (hasVal(r.max)) return `长度不超过${r.max}个字符`
+          } else if (r.enumInputType == 'Number') {
+            if (hasVal(r.min) && hasVal(r.max)) return `数值必须在${r.min}-${r.max}之间`
+            if (hasVal(r.min)) return `数值不能小于${r.min}`
+            if (hasVal(r.max)) return `数值不能大于${r.max}`
+          } else if (r.enumInputType == 'Checkbox') {
+            if (hasVal(r.min) && hasVal(r.max)) return `必须选择${r.min}-${r.max}项`
+            if (hasVal(r.min)) return `至少选择${r.min}项`
+            if (hasVal(r.max)) return `至多选择${r.max}项`
+          }
+        }
+        const createRangeType = () => {
+          switch (r.enumInputType) {
+            case 'Number':
+            case 'Score':
+              return 'number'
+            case 'Checkbox':
+              return 'array'
+            default:
+              return 'string'
+          }
+        }
+        const rangeRule: {
+          type: string,
+          min?: number,
+          max?: number,
+          message: string | undefined
+          transform?: (val: any) => any
+        } = {
+          type: createRangeType(),
+          min: r.min,
+          max: r.max,
+          message: createRangeMsg()
+        }
+        if (!hasVal(r.min)) delete rangeRule.min
+        if (!hasVal(r.max)) delete rangeRule.max
+        if (r.enumInputType == 'Number')
+          rangeRule.transform = val => hasVal(val) ? val * 1 : val
+        fieldRules.push(rangeRule)
+      }
+      if (r.regex) {
+        fieldRules.push({
+          pattern: r.regex,
+          message: `${r.label}格式不符合要求`
         })
+      }
+      if (fieldRules.length) autoRules[r.fieldName] = fieldRules
     })
-    rules.value = autoRules
-    rulesInit.value = true
-}, {immediate: true})
+  })
+  rules.value = autoRules
+  rulesInit.value = true
+}, { immediate: true })
 
-defineExpose({validate})
+defineExpose({ validate })
 </script>
 
-<style scoped>
-
-</style>
+<style scoped></style>

+ 0 - 2
src/pagesOther/pages/voluntary/index/components/voluntary-form-simulate.vue

@@ -28,8 +28,6 @@ const goSimulate = () => {
         });
     }
 }
-
-onMounted(() => userStore.getDirectedSchoolList())
 </script>
 
 <style scoped>

+ 7 - 0
src/pagesOther/pages/voluntary/index/index.vue

@@ -34,7 +34,10 @@ import {useTransferPage} from "@/hooks/useTransferPage";
 import {routes} from "@/common/routes";
 import {UniversityPickerPageOptions} from "@/types/transfer";
 import {getRenderRules, postRenderRules} from "@/api/modules/voluntary";
+import { useAuth } from "@/hooks/useAuth";
+import { EnumUserRole } from "@/common/enum";
 
+const { hasPermission } = useAuth();
 const emptyImg = computed(() => config.ossUrl + '/volunteer/voluntary/index/empty_data.png')
 
 const form = ref<InstanceType<typeof VoluntaryForm>>()
@@ -83,6 +86,10 @@ const processSelected = async (picked: SelectedUniversityMajor) => {
 }
 
 const handleSubmit = async () => {
+    const hasAuth = hasPermission([EnumUserRole.VIP]);
+    if (!hasAuth) {
+        return;
+    }
     if (!target.value.universityId || !target.value.majorId) return uni.$ie.showToast('请选择院校专业')
     if (!rules.value.length) return uni.$ie.showToast('由于官方未公布历年录取分数或计划变更,暂时无法计算录取概率')
     await form.value?.validate()

+ 29 - 28
src/pagesOther/pages/voluntary/list/components/voluntary-item.vue

@@ -1,53 +1,54 @@
 <template>
-    <view class="bg-white rounded-xl p-28 flex justify-between items-center gap-20 relative">
-        <view v-if="showIndex" class="absolute left-0 top-10 px-20 py-12 rounded-r-full text-24 font-bold"
-              :class="showIndex.clazz">
-            {{ showIndex.text }}
-        </view>
-        <view class="flex-1" :class="{'mt-50': showIndex}">
-            <view class="flex justify-between items-center gap-20">
-                <ie-image :src="data.universityLogo" mode="aspectFit" custom-class="w-48 h-48"/>
-                <view class="text-30 font-bold text-fore-title flex-1">{{ data.universityName }}</view>
-                <uv-icon name="more-dot-fill" size="20" @click="$emit('more')"/>
-            </view>
-            <voluntary-majors-draggable v-model:majors="data.majors" @change="handleSortUpdate" @delete="handleDelete" />
-        </view>
+  <view class="bg-white rounded-xl p-28 flex justify-between items-center gap-20 relative">
+    <view v-if="showIndex" class="absolute left-0 top-10 px-20 py-12 rounded-r-full text-24 font-bold"
+      :class="showIndex.clazz">
+      {{ showIndex.text }}
     </view>
+    <view class="flex-1" :class="{ 'mt-50': showIndex }">
+      <view class="flex justify-between items-center gap-20">
+        <ie-image :src="data.universityLogo" mode="aspectFit" custom-class="w-48 h-48" />
+        <view class="text-30 font-bold text-fore-title flex-1">{{ data.universityName }}</view>
+        <uv-icon name="more-dot-fill" size="20" @click="$emit('more')" />
+      </view>
+      <voluntary-majors-draggable-list v-model:majors="data.majors" @change="handleSortUpdate" @delete="handleDelete" />
+    </view>
+  </view>
 </template>
 
 <script lang="ts" setup>
-import {VoluntaryMajorItem, VoluntaryRecord} from "@/types/voluntary";
-import VoluntaryMajorsDraggable from "@/pagesOther/pages/voluntary/list/components/voluntary-majors-draggable.vue";
-import {removeVoluntaryByMajor, sortVoluntaryByMajor} from "@/api/modules/voluntary";
+import { VoluntaryMajorItem, VoluntaryRecord } from "@/types/voluntary";
+import VoluntaryMajorsDraggableList from "./voluntary-majors-draggable-list.vue";
+import { removeVoluntaryByMajor, sortVoluntaryByMajor } from "@/api/modules/voluntary";
 
 const props = defineProps<{
-    index: number;
-    data: VoluntaryRecord;
+  index: number;
+  data: VoluntaryRecord;
 }>()
 const emits = defineEmits(['more', 'error', 'deleted'])
 
 const indexOptions = [{
-    text: '第一志愿',
-    clazz: 'bg-warning-light text-warning-dark'
+  text: '第一志愿',
+  clazz: 'bg-warning-light text-warning-dark'
 }, {
-    text: '第二志愿',
-    clazz: 'bg-primary-100 text-primary'
+  text: '第二志愿',
+  clazz: 'bg-primary-100 text-primary'
 }]
 const showIndex = computed(() => indexOptions[props.index])
 
 const handleSortUpdate = () => {
-    sortVoluntaryByMajor(props.data.universityId, props.data.majors.map(m => m.majorId))
-        .catch((e: any) => emits('error', e))
+  sortVoluntaryByMajor(props.data.universityId, props.data.majors.map(m => m.majorId))
+    .catch((e: any) => emits('error', e))
 }
 
 const handleDelete = async (m: VoluntaryMajorItem) => {
-    await uni.$ie.showConfirm({title: '删除提醒', content: `确定删除该专业?`})
+  // 改为 showModal,避免 reject 错误上报
+  const confirm = await uni.$ie.showModal({ title: '删除提醒', content: `确定删除该专业?` })
+  if (confirm) {
     await removeVoluntaryByMajor(m.majorId)
     uni.$ie.showSuccess('删除成功')
     emits('deleted')
+  }
 }
 </script>
 
-<style scoped>
-
-</style>
+<style scoped></style>

+ 58 - 0
src/pagesOther/pages/voluntary/list/components/voluntary-majors-draggable-list.vue

@@ -0,0 +1,58 @@
+<template>
+  <view class="mt-20">
+    <l-drag ref="dragRef" :list="majors" :column="1" gridHeight="56px" :touchHandle="touchHandle" ghost handle
+      @change="changeSort">
+      <!-- // 每一项的插槽 grid 的 content 您传入的数据 -->
+      <template #grid="{ active, content, index }">
+        <!-- // grid.active 是否为当前拖拽项目 根据自己需要写样式 -->
+        <view
+          class="bg-back rounded-6 h-[44px] pl-30 pr-20 relative flex items-center gap-x-16 mb-[10px] border border-solid"
+          :class="[active ? 'border-primary' : 'border-transparent']">
+          <view class="text-32 text-fore-placeholder font-bold">{{ toFixedLen(index) }}</view>
+          <view class="flex-1 w-1 h-full leading-[44px] text-28 text-fore-title font-bold ellipsis-1">{{ content.majorName }}</view>
+          <uv-icon name="trash" size="18" color="error" @click="handleDelete" />
+          <view slot="handle" @touchstart="handleDragStart" @touchend="handleDragEnd">
+            <uv-icon name="list-dot" size="18" color="primary" />
+          </view>
+        </view>
+      </template>
+      <template #ghots></template>
+    </l-drag>
+  </view>
+</template>
+<script lang="ts" setup>
+import type { Voluntary } from '@/types';
+import { VOLUNTARY_REFRESHER_ENABLED } from "@/types/injectionSymbols";
+
+const props = defineProps<{
+  majors: Voluntary.VoluntaryMajorItem[];
+}>();
+const refresherEnabled = inject(VOLUNTARY_REFRESHER_ENABLED) || ref(false)
+const emits = defineEmits<{
+  (e: "delete", major: Voluntary.VoluntaryMajorItem): void;
+  (e: "update:majors", majors: Voluntary.VoluntaryMajorItem[]): void;
+  (e: "change", majors: Voluntary.VoluntaryMajorItem[]): void;
+}>();
+const touchHandle = ref(false)
+
+const handleDragStart = () => {
+  touchHandle.value = true;
+  refresherEnabled.value = false;
+}
+const handleDragEnd = () => {
+  touchHandle.value = false;
+  refresherEnabled.value = true;
+}
+const changeSort = (e: any) => {
+  const list = e.map((item: any) => item.content);
+  emits('update:majors', list);
+  emits('change', list);
+}
+const handleDelete = (major: Voluntary.VoluntaryMajorItem) => {
+  emits('delete', major);
+}
+const toFixedLen = (i: number, len: number = 2) => {
+  return String(i + 1).padStart(len, "0");
+}
+</script>
+<style lang="scss" scoped></style>

+ 0 - 214
src/pagesOther/pages/voluntary/list/components/voluntary-majors-draggable.vue

@@ -1,214 +0,0 @@
-<template>
-    <!-- H5:SortableJS -->
-    <!-- #ifdef H5 -->
-    <view ref="h5ListRef" class="mt-30 flex flex-col">
-        <view
-            v-for="(m, i) in innerMajors"
-            :key="getKey(m, i)"
-            class="bg-back-light rounded-lg p-20 flex justify-between items-center gap-20 mb-20 min-w-0"
-            :data-index="i"
-        >
-            <view class="text-32 text-fore-placeholder font-bold">{{ toFixedLen(i) }}</view>
-            <view class="flex-1 w-0 text-28 text-fore-title font-bold truncate">{{ m.majorName }}</view>
-
-            <uv-icon name="trash" size="18" color="error" @click="$emit('delete', m)"/>
-
-            <!-- 拖拽手柄:Sortable handle -->
-            <view class="drag-handle">
-                <uv-icon name="list-dot" size="18" color="primary"/>
-            </view>
-        </view>
-    </view>
-    <!-- #endif -->
-
-    <!-- #ifndef H5 -->
-    <movable-area
-        class="mt-30 w-full"
-        :style="{ height: areaHeight + 'px' }"
-        :catchtouchmove="dragging"
-        @touchmove.stop.prevent="noop"
-    >
-        <movable-view
-            v-for="(m, i) in innerMajors"
-            :key="getKey(m, i)"
-            class="w-full"
-            direction="vertical"
-            :y="getY(i)"
-            :disabled="activeIndex !== i"
-            :inertia="false"
-            :damping="80"
-            :animation="true"
-            @change="(e) => onDragChange(e, i)"
-            @touchend="onDragEnd"
-        >
-            <view class="bg-back-light rounded-lg p-20 flex justify-between items-center gap-20">
-                <!-- 主内容:阻断触摸,确保只能手柄拖 -->
-                <view class="flex-1 flex items-center gap-20" @touchmove.stop>
-                    <view class="text-32 text-fore-placeholder font-bold">{{ toFixedLen(i) }}</view>
-                    <view class="flex-1 w-0 text-28 text-fore-title font-bold truncate">{{ m.majorName }}</view>
-                    <uv-icon name="trash" size="18" color="error" @tap.stop="$emit('delete', m)" />
-                </view>
-
-                <!-- 手柄:不 stop / prevent -->
-                <view class="ml-10" @touchstart="onHandleDown(i)" @touchend="onHandleUp">
-                    <uv-icon name="list-dot" size="18" color="primary" />
-                </view>
-            </view>
-        </movable-view>
-    </movable-area>
-    <!-- #endif -->
-</template>
-
-<script setup lang="ts">
-import {VoluntaryMajorItem} from "@/types/voluntary";
-import {VOLUNTARY_SORTING} from "@/types/injectionSymbols";
-// #ifdef H5
-import Sortable from "sortablejs";
-// #endif
-
-const props = defineProps<{
-    majors: VoluntaryMajorItem[];
-}>();
-
-const emits = defineEmits<{
-    (e: "delete", major: VoluntaryMajorItem): void;
-    (e: "update:majors", majors: VoluntaryMajorItem[]): void;
-    (e: "change", majors: VoluntaryMajorItem[]): void;
-}>();
-
-const isSorting = inject(VOLUNTARY_SORTING) || ref(false)
-const innerMajors = ref<VoluntaryMajorItem[]>([]);
-watch(
-    () => props.majors,
-    (v) => (innerMajors.value = v ? [...v] : []),
-    {immediate: true, deep: true}
-);
-
-function toFixedLen(i: number, len: number = 2) {
-    return String(i + 1).padStart(len, "0");
-}
-
-function getKey(m: any, i: number) {
-    // 你的示例 majorId 可能重复,所以拼 i
-    return m.majorId
-}
-
-// ========== H5:SortableJS ==========
-const h5ListRef = ref<any>(null);
-// #ifdef H5
-let sortableIns: Sortable | null = null;
-
-onMounted(async () => {
-    await nextTick();
-    const el = h5ListRef.value?.$el || h5ListRef.value; // 兼容 view ref
-    if (!el) return;
-
-    sortableIns = Sortable.create(el, {
-        animation: 150,
-        handle: ".drag-handle", // 只允许手柄拖
-        forceFallback: true,
-        onStart: () => {
-            isSorting.value = true
-            console.log('sortableIns onStart', true)
-        },
-        onEnd: (evt) => {
-            isSorting.value = false;
-            console.log('sortableIns onEnd', false)
-            // sort logic
-            if (evt.oldIndex == null || evt.newIndex == null) return;
-            const arr = [...innerMajors.value];
-            const [moved] = arr.splice(evt.oldIndex, 1);
-            arr.splice(evt.newIndex, 0, moved);
-            innerMajors.value = arr;
-            emits("update:majors", arr);
-            emits('change', arr)
-        },
-    });
-});
-
-onBeforeUnmount(() => {
-    sortableIns?.destroy();
-    sortableIns = null;
-});
-// #endif
-
-// ========== 小程序:movable-view(重写版) ==========
-const dragging = ref(false);
-const activeIndex = ref(-1);
-const activeY = ref(0);
-
-const rowGapPx = uni.upx2px(20);
-const rowHeightPx = uni.upx2px(92) + rowGapPx;
-const areaHeight = computed(() => innerMajors.value.length * rowHeightPx);
-
-const noop = () => {}; // 关键:给 @touchmove 一个“函数”,不要给 boolean
-
-function getY(i: number) {
-    return i === activeIndex.value ? activeY.value : i * rowHeightPx;
-}
-
-function clampY(y: number) {
-    const maxY = Math.max(0, (innerMajors.value.length - 1) * rowHeightPx);
-    return Math.min(Math.max(y, 0), maxY);
-}
-
-function swap(arr: any[], a: number, b: number) {
-    const t = arr[a];
-    arr[a] = arr[b];
-    arr[b] = t;
-}
-
-function onHandleDown(i: number) {
-    console.log("onHandleDown", i);
-    dragging.value = true;
-    activeIndex.value = i;
-    activeY.value = i * rowHeightPx;
-}
-
-function onHandleUp() {
-    console.log("onHandleUp");
-    // 不要在这里结束;结束交给 movable-view 的 touchend
-}
-
-function onDragChange(e: any, i: number) {
-    // 有了上面的报错修复后,这里应该能打出来
-    console.log("onDragChange", e?.detail, i);
-
-    if (!dragging.value) return;
-    if (i !== activeIndex.value) return;
-    if (e?.detail?.source !== "touch") return;
-
-    const y = clampY(Number(e.detail.y || 0));
-    activeY.value = y;
-
-    const target = Math.round(y / rowHeightPx);
-    if (target === i) return;
-
-    swap(innerMajors.value as any[], i, target);
-    activeIndex.value = target;
-}
-
-function onDragEnd() {
-    console.log("onDragEnd");
-    if (!dragging.value) return;
-
-    dragging.value = false;
-
-    if (activeIndex.value === -1) return;
-    activeY.value = activeIndex.value * rowHeightPx;
-
-    const arr = [...innerMajors.value];
-    activeIndex.value = -1;
-    console.log('emits update:majors', arr)
-    emits("update:majors", arr);
-    emits('change', arr);
-}
-</script>
-
-<style scoped>
-.drag-handle {
-    touch-action: none;
-    -webkit-user-select: none;
-    user-select: none;
-}
-</style>

+ 145 - 134
src/pagesOther/pages/voluntary/list/list.vue

@@ -1,177 +1,188 @@
 <template>
-    <ie-page>
-        <z-paging ref="paging" v-model="list" bg-color="#F6F8FA" safe-area-inset-bottom :scrollable="!isSorting"
-                  :refresher-enabled="!isSorting" @query="handleQuery">
-            <template #top>
-                <ie-navbar title="志愿表"/>
-            </template>
-            <view class="mt-20 bg-warning-light p-28 text-23 leading-38 text-fore-title">
-                <text class="font-bold">说明:</text>
-                目前志愿计划为2025年,排序前两个为第一、二志愿,可通过修改排序重新选择第一、二志愿
-            </view>
-            <view class="p-28 flex flex-col gap-28">
-                <voluntary-item v-for="(item, i) in list" :key="i" :data="item" :index="i" @more="showActions(item)"
-                                @error="handleError" @deleted="handleDeleted"/>
-            </view>
-        </z-paging>
-        <ie-safe-toolbar :height="84" :shadow="false">
-            <view class="px-30 py-16">
-                <ie-button @click="handleAdd">添加志愿</ie-button>
-            </view>
-        </ie-safe-toolbar>
-        <uv-action-sheet ref="actionSheet" :actions="moreActions" safe-area-inset-bottom close-on-click-overlay
-                         cancel-text="取消" @select="handleActionSelect"/>
-    </ie-page>
+  <ie-page>
+    <z-paging ref="paging" v-model="list" bg-color="#F6F8FA" safe-area-inset-bottom :scrollable="!refresherEnabled"
+      :refresher-enabled="refresherEnabled" @query="handleQuery">
+      <template #top>
+        <ie-navbar title="志愿表" />
+      </template>
+      <view class="mt-20 bg-warning-light p-28 text-23 leading-38 text-fore-title">
+        <text class="font-bold">说明:</text>
+        目前志愿计划为2025年,排序前两个为第一、二志愿,可通过修改排序重新选择第一、二志愿
+      </view>
+      <view class="p-28 flex flex-col gap-28">
+        <voluntary-item v-for="(item, i) in list" :key="i" :data="item" :index="i" @more="showActions(item)"
+          @error="handleError" @deleted="handleDeleted" />
+      </view>
+    </z-paging>
+    <ie-safe-toolbar :height="84" :shadow="false">
+      <view class="px-30 py-16">
+        <ie-button @click="handleAdd">添加志愿</ie-button>
+      </view>
+    </ie-safe-toolbar>
+    <uv-action-sheet ref="actionSheet" :actions="moreActions" safe-area-inset-bottom close-on-click-overlay
+      cancel-text="取消" @select="handleActionSelect" />
+  </ie-page>
 </template>
 
 <script setup lang="ts">
-import {SelectedUniversityMajor, VoluntaryRecord} from "@/types/voluntary";
+import { SelectedUniversityMajor, VoluntaryRecord } from "@/types/voluntary";
 import VoluntaryItem from "@/pagesOther/pages/voluntary/list/components/voluntary-item.vue";
-import {VOLUNTARY_SORTING} from "@/types/injectionSymbols";
+import { VOLUNTARY_REFRESHER_ENABLED } from "@/types/injectionSymbols";
 import {
-    addVoluntary,
-    getVoluntaryList,
-    removeVoluntaryByUniversity,
-    sortVoluntaryByUniversity
+  addVoluntary,
+  getVoluntaryList,
+  removeVoluntaryByUniversity,
+  sortVoluntaryByUniversity
 } from "@/api/modules/voluntary";
 import UvActionSheet from "@/uni_modules/uv-action-sheet/components/uv-action-sheet/uv-action-sheet.vue";
-import {useTransferPage} from "@/hooks/useTransferPage";
-import {UniversityPickerPageOptions} from "@/types/transfer";
-import {routes} from "@/common/routes";
+import { useTransferPage } from "@/hooks/useTransferPage";
+import { UniversityPickerPageOptions } from "@/types/transfer";
+import { routes } from "@/common/routes";
+import { useAuth } from "@/hooks/useAuth";
+import { EnumUserRole } from "@/common/enum";
 
 interface ActionItem {
-    id: string;
-    name: string;
-    icon?: string;
-    color?: string;
-    iconColor?: string;
-    disabled?: boolean;
+  id: string;
+  name: string;
+  icon?: string;
+  color?: string;
+  iconColor?: string;
+  disabled?: boolean;
 }
-
-const {transferTo} = useTransferPage()
+const { hasPermission } = useAuth();
+const { transferTo } = useTransferPage()
 const list = ref<VoluntaryRecord[]>([])
 const paging = ref<ZPagingInstance>()
-const isSorting = ref<boolean>(false)
+const refresherEnabled = ref<boolean>(true)
 const actionSheet = ref<InstanceType<typeof UvActionSheet>>()
 const actionRecord = ref<VoluntaryRecord>()
 const moreActions = computed<ActionItem[]>(() => {
-    const records = list.value
-    const current = actionRecord.value
-    const idx = current ? records.indexOf(current) : -1
-    const enableTop = records.length > 1 && idx > 0
-    const enableUp = records.length > 1 && idx > 0
-    const enableDown = records.length > 1 && idx < records.length - 1
-    return [{
-        id: 'top',
-        name: '置顶',
-        icon: 'pushpin-fill',
-        color: 'var(--primary-color)',
-        iconColor: enableTop ? 'primary' : 'info',
-        disabled: !enableTop
-    }, {
-        id: 'up',
-        name: '上移',
-        icon: 'arrow-upward',
-        iconColor: enableUp ? '' : 'info',
-        disabled: !enableUp
-    }, {
-        id: 'down',
-        name: '下移',
-        icon: 'arrow-downward',
-        iconColor: enableDown ? '' : 'info',
-        disabled: !enableDown
-    }, {
-        id: 'delete',
-        name: '删除',
-        icon: 'trash',
-        color: 'var(--danger)',
-        iconColor: 'error'
-    }]
+  const records = list.value
+  const current = actionRecord.value
+  const idx = current ? records.indexOf(current) : -1
+  const enableTop = records.length > 1 && idx > 0
+  const enableUp = records.length > 1 && idx > 0
+  const enableDown = records.length > 1 && idx < records.length - 1
+  return [{
+    id: 'top',
+    name: '置顶',
+    icon: 'pushpin-fill',
+    color: 'var(--primary-color)',
+    iconColor: enableTop ? 'primary' : 'info',
+    disabled: !enableTop
+  }, {
+    id: 'up',
+    name: '上移',
+    icon: 'arrow-upward',
+    iconColor: enableUp ? '' : 'info',
+    disabled: !enableUp
+  }, {
+    id: 'down',
+    name: '下移',
+    icon: 'arrow-downward',
+    iconColor: enableDown ? '' : 'info',
+    disabled: !enableDown
+  }, {
+    id: 'delete',
+    name: '删除',
+    icon: 'trash',
+    color: 'var(--danger)',
+    iconColor: 'error'
+  }]
 })
 
 const handleQuery = () => {
-    getVoluntaryList().then(res => {
-        paging.value?.completeByNoMore(res.data, true)
-    }).catch(e => paging.value?.completeByError(e))
+  getVoluntaryList().then(res => {
+    paging.value?.completeByNoMore(res.data, true)
+  }).catch(e => paging.value?.completeByError(e))
 }
 
 const showActions = (record: VoluntaryRecord) => {
-    actionRecord.value = record
-    actionSheet.value?.open()
+  actionRecord.value = record
+  actionSheet.value?.open()
 }
 
 const handleActionSelect = async (e: ActionItem) => {
-    const record = actionRecord.value
-    const recordList = list.value
-    if (!record) return
-    const idx = recordList.findIndex(r => r == record)
-    if (['top', 'up', 'down'].includes(e.id)) {
-        try {
-            switch (e.id) {
-                case 'top':
-                    if (idx > 0) {
-                        recordList.splice(idx, 1)
-                        recordList.unshift(record)
-                        await sortVoluntaryByUniversity(recordList.map(r => r.universityId))
-                        uni.$ie.showSuccess('保存成功')
-                    }
-                    break;
-                case 'up':
-                    if (idx > 0) {
-                        recordList.splice(idx, 1)
-                        recordList.splice(idx - 1, 0, record)
-                        await sortVoluntaryByUniversity(recordList.map(r => r.universityId))
-                        uni.$ie.showSuccess('保存成功')
-                    }
-                    break;
-                case 'down':
-                    if (idx < recordList.length - 1) {
-                        recordList.splice(idx, 1)
-                        recordList.splice(idx + 1, 0, record)
-                        await sortVoluntaryByUniversity(recordList.map(r => r.universityId))
-                        uni.$ie.showSuccess('保存成功')
-                    }
-                    break;
-            }
-        } catch (e) {
-            console.log('action ex', e)
-            paging.value?.reload() // 发生异常时,重新加载列表
-        }
-    } else if (e.id === 'delete') {
-        await uni.$ie.showConfirm({
-            title: '志愿删除提醒',
-            content: `删除'${record.universityName}',将同时删除该院校下所有意向专业。\n确认删除?!`
-        })
-        await removeVoluntaryByUniversity(record.universityId)
-        uni.$ie.showSuccess('删除成功')
-        paging.value?.reload()
-    } else {
-        throw new Error('Unsupported action id: ' + e.id)
+  const record = actionRecord.value
+  const recordList = list.value
+  if (!record) return
+  const idx = recordList.findIndex(r => r == record)
+  if (['top', 'up', 'down'].includes(e.id)) {
+    try {
+      switch (e.id) {
+        case 'top':
+          if (idx > 0) {
+            recordList.splice(idx, 1)
+            recordList.unshift(record)
+            await sortVoluntaryByUniversity(recordList.map(r => r.universityId))
+            uni.$ie.showSuccess('保存成功')
+          }
+          break;
+        case 'up':
+          if (idx > 0) {
+            recordList.splice(idx, 1)
+            recordList.splice(idx - 1, 0, record)
+            await sortVoluntaryByUniversity(recordList.map(r => r.universityId))
+            uni.$ie.showSuccess('保存成功')
+          }
+          break;
+        case 'down':
+          if (idx < recordList.length - 1) {
+            recordList.splice(idx, 1)
+            recordList.splice(idx + 1, 0, record)
+            await sortVoluntaryByUniversity(recordList.map(r => r.universityId))
+            uni.$ie.showSuccess('保存成功')
+          }
+          break;
+      }
+    } catch (e) {
+      console.log('action ex', e)
+      paging.value?.reload() // 发生异常时,重新加载列表
+    }
+  } else if (e.id === 'delete') {
+    // 改为 showModal,避免 reject 错误上报
+    const confirm = await uni.$ie.showModal({
+      title: '志愿删除提醒',
+      content: `删除'${record.universityName}',将同时删除该院校下所有意向专业。\n确认删除?`
+    })
+    if (confirm) {
+      await removeVoluntaryByUniversity(record.universityId)
+      uni.$ie.showSuccess('删除成功')
+      paging.value?.reload()
     }
+  } else {
+    throw new Error('Unsupported action id: ' + e.id)
+  }
 }
 
 const handleError = () => {
-    // 内部异常,重新取数
-    paging.value?.reload()
+  // 内部异常,重新取数
+  paging.value?.reload()
 }
 
 const handleDeleted = () => {
-    paging.value?.reload()
+  paging.value?.reload()
 }
 
 const handleAdd = async () => {
-    const option: UniversityPickerPageOptions = {
-        title: '选择你的意向院校专业',
-        fromVoluntary: true
-    }
-    const picked = await transferTo(routes.targetPicker, {data: option})
-    if (!picked) return
+  const hasAuth = hasPermission([EnumUserRole.VIP]);
+  if (!hasAuth) {
+    return;
+  }
+  const option: UniversityPickerPageOptions = {
+    title: '选择你的意向院校专业',
+    fromVoluntary: true
+  }
+  const picked = await transferTo(routes.targetPicker, { data: option })
+  if (!picked) return
+  setTimeout(async () => {
     await addVoluntary(picked)
     uni.$ie.showSuccess('保存成功')
     paging.value?.reload()
+  }, 300)
 }
 
-provide(VOLUNTARY_SORTING, isSorting)
+provide(VOLUNTARY_REFRESHER_ENABLED, refresherEnabled)
 </script>
 
 <style lang="scss"></style>

+ 29 - 32
src/pagesOther/pages/voluntary/result/components/voluntary-result-compare.vue

@@ -1,46 +1,43 @@
 <template>
-    <view class="p-28 bg-white rounded-xl">
-        <voluntary-result-title title="专业报录比">
-            <template #right>
-                <ie-picker v-model="year" :list="years" icon="arrow-down" width="" color="text-primary"
-                           icon-color="primary"/>
-            </template>
-        </voluntary-result-title>
-        <view class="pt-28 px-100 flex justify-between items-center">
-            <view class="flex flex-col items-center gap-18">
-                <view class="px-25 py-10 bg-primary text-white font-bold text-28 rounded">报考人数</view>
-                <view class="text-60 text-fore-title">{{currentHistory.application||'-'}}</view>
-            </view>
-            <view class="flex flex-col items-center gap-18">
-                <view class="h-48"></view>
-                <view class="text-60 text-fore-title">:</view>
-            </view>
-            <view class="flex flex-col items-center gap-18">
-                <view class="px-25 py-10 bg-primary text-white font-bold text-28 rounded">计划人数</view>
-                <view class="text-60 text-fore-title">{{currentHistory.admission||'-'}}</view>
-            </view>
-        </view>
+  <view class="p-28 bg-white rounded-xl">
+    <voluntary-result-title title="专业报录比">
+      <template #right>
+        <ie-picker v-model="year" :list="years" icon="arrow-down" width="" color="text-primary" icon-color="primary" />
+      </template>
+    </voluntary-result-title>
+    <view class="pt-28 px-100 flex justify-between items-center">
+      <view class="flex flex-col items-center gap-18">
+        <view class="px-25 py-10 bg-primary text-white font-bold text-28 rounded">报考比例</view>
+        <view class="text-60 text-fore-title">{{ currentHistory.application || '-' }}</view>
+      </view>
+      <view class="flex flex-col items-center gap-18">
+        <view class="h-48"></view>
+        <view class="text-60 text-fore-title">:</view>
+      </view>
+      <view class="flex flex-col items-center gap-18">
+        <view class="px-25 py-10 bg-primary text-white font-bold text-28 rounded">计划比例</view>
+        <view class="text-60 text-fore-title">{{ currentHistory.admission || '-' }}</view>
+      </view>
     </view>
+  </view>
 </template>
 
 <script setup lang="ts">
 import VoluntaryResultTitle from "@/pagesOther/pages/voluntary/result/components/plus/voluntary-result-title.vue";
-import {VOLUNTARY_RESULT} from "@/types/injectionSymbols";
-import {VoluntaryResult} from "@/types/voluntary";
+import { VOLUNTARY_RESULT } from "@/types/injectionSymbols";
+import type { VoluntaryResult, VoluntaryResultHistory } from "@/types/voluntary";
 
 const result = inject(VOLUNTARY_RESULT) || ref({} as VoluntaryResult)
 const historyList = computed(() => result?.value.histories || [])
-const years = computed(() => historyList.value.map(h => ({label: h.year + '年', value: h.year})))
+const years = computed(() => historyList.value.map(h => ({ label: h.year + '年', value: h.year })))
 const year = ref<string | number>('')
-const currentHistory = computed(() => historyList.value.find(h => h.year == year.value) || {})
+const currentHistory = computed(() => historyList.value.find(h => h.year == year.value) || {} as VoluntaryResultHistory)
 
 watch(historyList, (list) => {
-    if (list.length) {
-        year.value = list[0].year
-    }
-}, {immediate: true})
+  if (list.length) {
+    year.value = list[0].year
+  }
+}, { immediate: true })
 </script>
 
-<style scoped>
-
-</style>
+<style scoped></style>

+ 2 - 2
src/pagesStudy/pages/index/compoentns/index-practice-entry.vue

@@ -28,7 +28,7 @@
             <ie-image :is-oss="true" src="/study-bg13.png" custom-class="absolute bottom-0 left-0 w-full h-full z-0"
               mode="aspectFill" />
           </view>
-          <!-- <view class="bg-gradient-to-r from-[#32B5FD] to-[#79DCFD] flex-1 rounded-15 relative overflow-hidden">
+          <view class="bg-gradient-to-r from-[#32B5FD] to-[#79DCFD] flex-1 rounded-15 relative overflow-hidden">
             <view class="mt-30 p-30 z-1 relative">
               <view class="text-30 text-white font-bold">必刷题</view>
               <view class="mt-8 text-24 text-white">高频考题,一网打尽</view>
@@ -39,7 +39,7 @@
             </view>
             <ie-image :is-oss="true" src="/study-bg13.png" custom-class="absolute bottom-0 left-0 w-full h-full z-0"
               mode="aspectFill" />
-          </view> -->
+          </view>
         </template>
         <template v-else>
           <view class="bg-gradient-to-r from-[#0088FE] to-[#31A0FC] flex-1 rounded-15 relative overflow-hidden">

+ 0 - 7
src/pagesStudy/pages/index/index.vue

@@ -40,13 +40,6 @@ const { hasPermission } = useAuth();
 const iePageRef = ref<InstanceType<typeof IePage>>();
 const { hasDirectedSchool, directedSchoolList, getExamType, isVHS } = storeToRefs(userStore);
 const firstDirectedSchool = computed(() => directedSchoolList.value[0] || {});
-
-const loadData = async () => {
-  await userStore.getDirectedSchoolList();
-}
-onLoad(() => {
-  loadData();
-});
 </script>
 
 <style></style>

+ 7 - 3
src/pagesStudy/pages/knowledge-practice/knowledge-practice.vue

@@ -77,10 +77,14 @@ const loadKnowledgeList = async () => {
   }
   try {
     uni.$ie.showLoading();
-    const { data } = await getKnowledgeList({
+    const params: Study.KnowledgeListRequestDTO = {
       subjectId: currentSubjectId.value,
-      directed: prevData.value.directed
-    });
+      directed: prevData.value.directed,
+    };
+    if (userStore.isVHS) {
+      params.questionType = prevData.value.questionType;
+    }
+    const { data } = await getKnowledgeList(params);
     treeData.value = data as Study.KnowledgeNode[];
     pagingRef.value?.complete(data);
   } catch (error) {

+ 4 - 1
src/pagesSystem/pages/login/login.vue

@@ -189,7 +189,10 @@ const handleMobileLogin = async (params: LoginRequestDTO) => {
       if (res.token) {
         userStore.login(res.token).then(({ success, userInfo }) => {
           if (success) {
-            transferBack(true);
+            // transferBack(true);
+            transferTo(routes.pageIndex, {
+              type: 'reLaunch'
+            });
           } else {
             uni.$ie.showToast('登录失败')
           }

+ 14 - 7
src/pagesSystem/pages/webview/webview.vue

@@ -17,20 +17,27 @@ const pageTitle = ref('');
 const webviewParams = ref('');
 
 onLoad(() => {
-  const { url, params, title, showNavbar: _showNavbar } = prevData.value
+  console.log(prevData.value)
+  const { url, params, title, showNavbar: _showNavbar = true } = prevData.value
   pageTitle.value = title;
-  showNavbar.value = JSON.parse(_showNavbar) ?? true;
+  showNavbar.value = JSON.parse(_showNavbar);
   try {
     uni.$ie.showLoading();
-    if (typeof params === 'string') {
-      webviewParams.value = JSON.parse(params);
+    if (params) {
+      if (typeof params === 'string') {
+        webviewParams.value = JSON.parse(params);
+      } else {
+        webviewParams.value = params;
+      }
+      webviewSrc.value = `${url}?${Object.entries(webviewParams.value).map(([key, value]) => `${key}=${value}`).join('&')}`;
     } else {
-      webviewParams.value = params;
+      webviewSrc.value = url;
     }
-    webviewSrc.value = `${url}?${Object.entries(webviewParams.value).map(([key, value]) => `${key}=${value}`).join('&')}`;
   } catch (error) {
     uni.$ie.showToast('参数错误');
-    transferBack();
+    setTimeout(() => {
+      transferBack();
+    }, 500);
   }
 });
 const handleLoad = () => {

+ 1 - 1
src/types/injectionSymbols.ts

@@ -51,7 +51,7 @@ export const VOLUNTARY_MODEL = Symbol('VOLUNTARY_MODEL') as InjectionKey<Ref<Vol
 export const VOLUNTARY_RESULT = Symbol('VOLUNTARY_RESULT') as InjectionKey<Ref<Voluntary.VoluntaryResult>>
 
 export const UNIVERSITY_FILTER = Symbol('UNIVERSITY_FILTER') as InjectionKey<Ref<University.UniversityQueryDto>>
-export const VOLUNTARY_SORTING = Symbol('VOLUNTARY_SORTING') as InjectionKey<Ref<boolean>>
+export const VOLUNTARY_REFRESHER_ENABLED = Symbol('VOLUNTARY_REFRESHER_ENABLED') as InjectionKey<Ref<boolean>>
 
 export const UNIVERSITY_DETAIL = Symbol('UNIVERSITY_DETAIL') as InjectionKey<Ref<University.UniversityDetail>>
 export const MAJOR_TREE = Symbol('MAJOR_TREE') as InjectionKey<Ref<Major.MajorItem[]>>

+ 1 - 0
src/types/study.ts

@@ -311,6 +311,7 @@ export interface SubjectListRequestDTO {
 export interface KnowledgeListRequestDTO {
   subjectId: number;
   directed: boolean;
+  questionType?: boolean
 }
 
 export interface OpenExamineeRequestDTO {

+ 7 - 0
src/types/transfer.ts

@@ -44,4 +44,11 @@ export interface ExamAnalysisPageOptions {
 export interface SimulationAnalysisPageOptions {
   examineeId: number;
   paperType: EnumPaperType;
+}
+
+export interface UniversityPickerPageOptions {
+  title: string;
+  fromVoluntary: boolean;
+  selectedUniversityId?: string;
+  selectedMajorId?: string;
 }

+ 6 - 0
src/uni_modules/uv-icon/components/uv-icon/uv-icon.vue

@@ -120,6 +120,9 @@
 			},
 			// 判断传入的name属性,是否图片路径,只要带有"/"均认为是图片形式
 			isImg() {
+        if (!this.name) {
+          return false;
+        }
 				const isBase64 = this.name.indexOf('data:') > -1 && this.name.indexOf('base64') > -1;
 				return this.name.indexOf('/') !== -1 || isBase64;
 			},
@@ -132,6 +135,9 @@
 			},
 			// 通过图标名,查找对应的图标
 			icon() {
+        if (!this.name) {
+          return '';
+        }
 				// 如果内置的图标中找不到对应的图标,就直接返回name值,因为用户可能传入的是unicode代码
 				const code = icons['uvicon-' + this.name];
 				// #ifdef APP-NVUE

+ 27 - 0
src/utils/update.ts

@@ -0,0 +1,27 @@
+
+export function checkUpdate() {
+  const { scene } = uni.getEnterOptionsSync();
+  // 从朋友圈单页模式打开页面
+  if (scene === 1154) {
+    return;
+  }
+  const updateManager = uni.getUpdateManager();
+  updateManager.onCheckForUpdate(res => {});
+  updateManager.onUpdateReady(() => {
+      uni.showModal({
+          title: '更新提示',
+          content: '新版本已经准备好,是否重启应用?',
+          success(res) {
+              if (res.confirm) {
+                  updateManager.applyUpdate();
+              }
+          }
+      });
+  });
+  updateManager.onUpdateFailed((error) => {
+      uni.showModal({
+          title: '更新失败',
+          content: '请删除小程序后重新打开'
+      });
+  });
+}

+ 1 - 1
vite.config.js

@@ -48,7 +48,7 @@ export default defineConfig(({ mode }) => ({
         'uni-app',
         'pinia'
       ],
-      exclude: ['createApp'],
+      ignore: ['createApp', 'h5'],
       eslintrc: {
         enabled: true
       }