Bläddra i källkod

voluntary - form & result init

abpcoder 2 veckor sedan
förälder
incheckning
9ad3c54bbe

+ 4 - 0
src/common/routes.ts

@@ -35,6 +35,10 @@ export const routes = {
    * 测录取概率
    */
   voluntaryIndex: '/pagesOther/pages/voluntary/index/index',
+  /*
+  * 测录取概率-结果
+  * */
+  voluntaryResult: '/pagesOther/pages/voluntary/result/result'
 } as const;
 
 export type Routes = keyof typeof routes;

+ 108 - 96
src/main.ts

@@ -9,7 +9,7 @@ import './preload'
 import tool from '@/utils/uni-tool'
 import * as Pinia from 'pinia';
 import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
-import { useImage } from '@/hooks/useImage';
+import {useImage} from '@/hooks/useImage';
 
 // #ifndef VUE3
 import Vue from 'vue'
@@ -19,115 +19,127 @@ Vue.config.productionTip = false
 Vue.use(uvUiTools)
 App.mpType = 'app'
 const app = new Vue({
-  ...App
+    ...App
 })
 app.$mount()
 // #endif
 
 // #ifdef VUE3
-import { createSSRApp } from 'vue'
+import {createSSRApp} from 'vue'
 import "./static/style/tailwind.scss";
 
 export function createApp() {
-  const app = createSSRApp(App)
-  app.use(uvUiTools)
-  
-  uni.$ie = tool;
+    const app = createSSRApp(App)
+    app.use(uvUiTools)
 
-  uni.$uv.setConfig({
-    props: {
-      loadingPage: {
-        loadingText: { default: '' },
-        image: { default: '/static/logo/loading1.gif' },
-        class: { default: 'mx-loading-page' }
-      },
-      navbar: {
-        placeholder: { default: true },
-        clickHover: { default: true },
-        statusBarHeight: { default: 0 }
-      },
-      statusBar: {
-        statusBarHeight: { default: 0 }
-      },
-      tabs: {
-        activeStyle: { default: () => ({ color: 'var(--primary-color)' }) }
-      },
-      steps: {
-        activeColor: { default: 'var(--primary-color)' }
-      },
-      search: {
-        color: { default: 'var(--main-color)' },
-        actionStyle: { default: () => ({ color: 'var(--primary-color)' }) }
-      },
-      empty: {
-        icon: { default: '/static/icon-empty.png' },
-        height: { default: 140 },
-        width: { default: 140 },
-        text: { default: '暂无相关数据' }
-      },
-      icon: {
-        customClass: {
-          default: ''
-        }
-      },
-      popup: {
-        theme: {
-          default: 'theme-ie'
-        }
-      },
-      image: {
-        customClass: {
-          default: ''
-        }
-      },
-      cell: {
-        disableHover: {
-          default: false
-        }
-      },
-      collapseItem: {
-        padding: {
-          default: '12px 15px;'
+    uni.$ie = tool;
+
+    uni.$uv.setConfig({
+        props: {
+            loadingPage: {
+                loadingText: {default: ''},
+                image: {default: '/static/logo/loading1.gif'},
+                class: {default: 'mx-loading-page'}
+            },
+            navbar: {
+                placeholder: {default: true},
+                clickHover: {default: true},
+                statusBarHeight: {default: 0}
+            },
+            statusBar: {
+                statusBarHeight: {default: 0}
+            },
+            tabs: {
+                activeStyle: {default: () => ({color: 'var(--primary-color)'})}
+            },
+            steps: {
+                activeColor: {default: 'var(--primary-color)'}
+            },
+            search: {
+                color: {default: 'var(--main-color)'},
+                actionStyle: {default: () => ({color: 'var(--primary-color)'})}
+            },
+            empty: {
+                icon: {default: '/static/icon-empty.png'},
+                height: {default: 140},
+                width: {default: 140},
+                text: {default: '暂无相关数据'}
+            },
+            icon: {
+                customClass: {
+                    default: ''
+                }
+            },
+            popup: {
+                theme: {
+                    default: 'theme-ie'
+                }
+            },
+            image: {
+                customClass: {
+                    default: ''
+                }
+            },
+            cell: {
+                disableHover: {
+                    default: false
+                }
+            },
+            collapseItem: {
+                padding: {
+                    default: '12px 15px;'
+                }
+            },
+            input: {
+                fontSize: {default: '30rpx'},
+                disabledColor: {default: 'var(--back-light)'},
+                customStyle: {
+                    default: () => ({
+                        height: '70rpx',
+                        paddingLeft: '40rpx',
+                        paddingRight: '40rpx',
+                        borderRadius: '24rpx'
+                    })
+                }
+            }
         }
-      }
-    }
-  })
+    })
 
-  const { resolvePath } = useImage();
-  uni.$zp = {
-    config: {
-      'default-page-size': 20,
-      'refresher-title-style': {
-        fontSize: '28rpx'
-      },
-      'loading-more-title-custom-style': {
-        fontSize: '26rpx'
-      },
-      // 底部安全区域以placeholder形式实现
-      'use-safe-area-placeholder': true
-      // 'empty-view-img-style': {
-      //   width: '364rpx',
-      //   height: '252rpx'
-      // },
-      // 'empty-view-img': resolvePath('/pagesStudy/static/image/icon-empty.png'),
-      // 'empty-view-title-style': {
-      //   color: '#B3B3B3',
-      //   fontSize: '30rpx',
-      //   marginTop: '40rpx'
-      // },
-      // 'empty-view-style': {
-      //   marginTop: '-200rpx'
-      // }
+    const {resolvePath} = useImage();
+    uni.$zp = {
+        config: {
+            'default-page-size': 20,
+            'refresher-title-style': {
+                fontSize: '28rpx'
+            },
+            'loading-more-title-custom-style': {
+                fontSize: '26rpx'
+            },
+            // 底部安全区域以placeholder形式实现
+            'use-safe-area-placeholder': true
+            // 'empty-view-img-style': {
+            //   width: '364rpx',
+            //   height: '252rpx'
+            // },
+            // 'empty-view-img': resolvePath('/pagesStudy/static/image/icon-empty.png'),
+            // 'empty-view-title-style': {
+            //   color: '#B3B3B3',
+            //   fontSize: '30rpx',
+            //   marginTop: '40rpx'
+            // },
+            // 'empty-view-style': {
+            //   marginTop: '-200rpx'
+            // }
+        }
     }
-  }
 
-  const pinia = Pinia.createPinia();
-  app.use(pinia);
-  pinia.use(piniaPluginPersistedstate);
+    const pinia = Pinia.createPinia();
+    app.use(pinia);
+    pinia.use(piniaPluginPersistedstate);
 
-  return {
-    app
-  }
+    return {
+        app
+    }
 }
 
 // #endif

+ 7 - 0
src/pages.json

@@ -104,6 +104,13 @@
           "style": {
             "navigationBarTitleText": ""
           }
+        },
+        {
+          "path": "pages/voluntary/result/result",
+          "style": {
+            "navigationBarTitleText": "",
+            "enablePullDownRefresh": true
+          }
         }
       ]
     },

+ 141 - 0
src/pagesOther/pages/voluntary/index/components/voluntary-form-core.vue

@@ -0,0 +1,141 @@
+<template>
+    <view v-if="rulesInit" class="mt-40 flex flex-col gap-30">
+        <view v-for="(r,i) in data.rules" :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>
+</template>
+
+<script setup lang="ts" name="VoluntaryFormCore">
+import {VOLUNTARY_FORM, VOLUNTARY_MODEL} from "@/types/injectionSymbols";
+import {EnrollRule, EnrollRuleItem, SelectedCollegeMajorWithRules, VoluntaryModel} from "@/types/voluntary";
+import {Ref} from "@vue/runtime-core";
+import _ from "lodash";
+
+const data = inject(VOLUNTARY_FORM) as Ref<SelectedCollegeMajorWithRules>
+const model = inject(VOLUNTARY_MODEL) as Ref<VoluntaryModel>
+const form = ref<{ validate: () => Promise<void> }[] | null>(null)
+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 validate = () => {
+    if (!form.value) return
+    const validates = form.value.map(f => f.validate())
+    return Promise.all(validates)
+}
+
+watch([data, model], ([data, model]) => {
+    if (!data || !model) return
+    const autoRules: Record<string, any> = {}
+    data.rules.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 (!score) {
+                            cb(`请填写${r.label}分数`)
+                            return
+                        }
+                        if (score < 0 || score > total) {
+                            cb(`分数不能超过总分${total}`)
+                            return
+                        }
+                        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})
+
+defineExpose({validate})
+</script>
+
+<style scoped>
+
+</style>

+ 27 - 0
src/pagesOther/pages/voluntary/index/components/voluntary-form-major.vue

@@ -0,0 +1,27 @@
+<template>
+    <view class="mt-24 p-28 flex items-center gap-24 bg-back-light rounded-xl">
+        <ie-image :src="data.universityLogo" custom-class="w-80"/>
+        <view class="flex-1">
+            <view class="text-28 text-fore-title font-bold">{{ data.universityName }}</view>
+            <view class="flex items-center gap-8">
+                <view class="mt-8 w-fit text-20 text-primary border border-solid border-primary rounded-4 px-10 py-4">
+                    {{ data.majorName }}
+                </view>
+                <view class="mt-8 w-fit text-20 text-info border border-solid border-info rounded-4 px-10 py-4">
+                    {{ data.groupName }}
+                </view>
+            </view>
+        </view>
+    </view>
+</template>
+
+<script setup lang="ts" name="VoluntaryFormMajor">
+import {VOLUNTARY_FORM} from "@/types/injectionSymbols";
+import {SelectedCollegeMajorWithRules} from "@/types/voluntary";
+
+const data = inject(VOLUNTARY_FORM) || {} as SelectedCollegeMajorWithRules
+</script>
+
+<style scoped>
+
+</style>

+ 51 - 0
src/pagesOther/pages/voluntary/index/components/voluntary-form-rule.vue

@@ -0,0 +1,51 @@
+<template>
+    <!-- 气泡框容器 -->
+    <view class="mt-30 relative overflow-visible rounded-xl border border-solid border-warning-disabled bg-white p-24
+                bg-gradient-to-b from-warning-light to-white">
+        <!-- 三角 + 缺口(以气泡框 top 边为基准定位) -->
+        <view class="absolute top-0 left-80 z-10 overflow-visible">
+            <!-- 缺口:盖住气泡框的上边框(加高一点避免 1px 缝) -->
+            <view class="absolute top-0 -left-30">
+                <view class="w-60 h-8 bg-warning-light"></view>
+            </view>
+
+            <!-- 外层三角:边框色 -->
+            <view
+                class="absolute w-0 h-0 border-solid border-t-0
+             -top-24 -left-24
+             border-l-24 border-r-24 border-b-24
+             border-l-transparent border-r-transparent border-b-warning-disabled"
+            ></view>
+
+            <!-- 内层三角:背景色(白),下移 1rpx 压住拼接处细缝 -->
+            <view
+                class="absolute w-0 h-0 border-solid border-t-0
+             -top-21 -left-22
+             border-l-22 border-r-22 border-b-22
+             border-l-transparent border-r-transparent border-b-warning-light"
+            ></view>
+        </view>
+
+        <!-- 内容 -->
+        <view class="flex items-center">
+            <uv-icon name="info-circle" color="warning"/>
+            <text class="ml-12 text-28 text-fore-title font-bold">计分规则说明</text>
+        </view>
+        <view class="mt-18 text-23 text-fore-title leading-38">
+            <view>考试由{{data.rules.map(r=>r.category).join('+')}}{{data.rules.length}}部分组成。其中:</view>
+            <view v-for="(c,i) in data.rules" :key="i">{{c.category}}={{c.content}}</view>
+        </view>
+    </view>
+
+</template>
+
+<script setup lang="ts" name="VoluntaryFormRule">
+import {VOLUNTARY_FORM} from "@/types/injectionSymbols";
+import {SelectedCollegeMajorWithRules} from "@/types/voluntary";
+
+const data = inject(VOLUNTARY_FORM) || {} as SelectedCollegeMajorWithRules
+</script>
+
+<style scoped>
+
+</style>

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

@@ -0,0 +1,16 @@
+<template>
+    <view class="mt-40 p-24 rounded-xl flex justify-between items-center bg-gradient-to-r from-primary-100 to-primary-50">
+        <view class="text-24 text-fore-title">没有模考成绩/估分不确定?</view>
+        <view class="text-28 text-primary font-bold flex items-center">
+            <text class="mr-6">去模拟测试</text>
+            <uv-icon name="arrow-right" color="primary"/>
+        </view>
+    </view>
+</template>
+
+<script lang="ts" setup name="VoluntaryFormSimulate">
+</script>
+
+<style scoped>
+
+</style>

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

@@ -0,0 +1,23 @@
+<template>
+    <voluntary-form-major/>
+    <voluntary-form-rule/>
+    <voluntary-form-simulate/>
+    <voluntary-form-core ref="form"/>
+</template>
+
+<script setup lang="ts">
+import VoluntaryFormMajor from "@/pagesOther/pages/voluntary/index/components/voluntary-form-major.vue";
+import VoluntaryFormRule from "@/pagesOther/pages/voluntary/index/components/voluntary-form-rule.vue";
+import VoluntaryFormSimulate from "@/pagesOther/pages/voluntary/index/components/voluntary-form-simulate.vue";
+import VoluntaryFormCore from "@/pagesOther/pages/voluntary/index/components/voluntary-form-core.vue";
+
+const form = ref<InstanceType<typeof VoluntaryFormCore> | null>(null)
+const validate = () => {
+    return form.value?.validate()
+}
+defineExpose({validate})
+</script>
+
+<style scoped>
+
+</style>

+ 104 - 6
src/pagesOther/pages/voluntary/index/index.vue

@@ -1,7 +1,10 @@
 <template>
     <ie-page bg-color="#F6F8FA">
-        <ie-navbar title="测录取概率" transparent bg-color="#FFFFFF" title-color="black" :keep-title-color="true" />
-        <ie-image is-oss src="/volunteer/voluntary/index/banner.png" />
+        <ie-navbar title="测录取概率" transparent bg-color="#FFFFFF" title-color="black" :keep-title-color="true"/>
+        <!-- #ifdef MP-WEIXIN -->
+        <view class="h-90 bg-gradient-to-r from-white to-cyan-100"/>
+        <!-- #endif -->
+        <ie-image is-oss src="/volunteer/voluntary/index/banner.png"/>
         <view class="mx-30 -mt-180 z-1 bg-white rounded-xl p-35">
             <view class="flex justify-between items-center">
                 <view class="text-lg text-fore-title">报考院校专业</view>
@@ -10,11 +13,12 @@
                     <uv-icon name="arrow-right" color="info"/>
                 </view>
             </view>
-            <ie-empty :image="img" text="请选择你的报考院校专业~"/>
+            <ie-empty v-if="!data.rules?.length" :image="emptyImg" text="请选择你的报考院校专业~"/>
+            <voluntary-form v-else ref="form"/>
         </view>
-        <ie-safe-toolbar :height="100" :shadow="false">
+        <ie-safe-toolbar :height="84" :shadow="false">
             <view class="px-30 py-16">
-                <ie-button>测录取概率</ie-button>
+                <ie-button @click="handleSubmit">测录取概率</ie-button>
             </view>
         </ie-safe-toolbar>
     </ie-page>
@@ -23,8 +27,102 @@
 <script setup lang="ts">
 
 import config from "@/config";
+import VoluntaryForm from "@/pagesOther/pages/voluntary/index/components/voluntary-form.vue";
+import {SelectedCollegeMajorWithRules, VoluntaryDto, VoluntaryModel} from "@/types/voluntary";
+import {VOLUNTARY_FORM, VOLUNTARY_MODEL} from "@/types/injectionSymbols";
+import {useTransferPage} from "@/hooks/useTransferPage";
+import {routes} from "@/common/routes";
 
-const img = computed(() =>  config.ossUrl + '/volunteer/voluntary/index/empty_data.png')
+const emptyImg = computed(() => config.ossUrl + '/volunteer/voluntary/index/empty_data.png')
+
+const form = ref<InstanceType<typeof VoluntaryForm> | null>(null)
+const data = ref<SelectedCollegeMajorWithRules>({} as SelectedCollegeMajorWithRules)
+const model = ref<VoluntaryModel>({})
+const {transferTo} = useTransferPage<any, VoluntaryDto>()
+
+const handleSubmit = async () => {
+    await form.value.validate()
+    const bigData = {data: data.value, model: model.value}
+    transferTo(routes.voluntaryResult, {bigData})
+}
+
+onMounted(async () => {
+    // api get rules
+    await nextTick()
+    data.value = {
+        // code: "20949",
+        majorAncestors: "交通运输大类>铁道运输类",
+        majorId: "68526",
+        majorName: "铁道交通运营管理",
+        groupName: '专业组一',
+        // notice: "",
+        universityId: "20949",
+        universityLogo: "https://mingxuejingbang.oss-cn-beijing.aliyuncs.com/ie/universityLog/23b6da550a584ea6b60886c6ae97b610.jpg",
+        universityName: "湖南铁道职业技术学院",
+        rules: [{
+            category: '文化素质',
+            content: '语(100分)+数(100分)+外(100分)',
+            details: [{
+                enumRuleCategory: 'Enroll',
+                enumInputType: 'Score',
+                label: '语文',
+                options: ["100"],
+                fieldName: '语文',
+                required: true,
+                readonly: true,
+                defaultValue: 80
+            }, {
+                enumRuleCategory: 'Enroll',
+                enumInputType: 'Score',
+                label: '数学',
+                options: ["100"],
+                fieldName: '数学',
+                required: true,
+                defaultValue: 84
+            }, {
+                enumRuleCategory: 'Enroll',
+                enumInputType: 'Score',
+                label: '外语',
+                options: ["100"],
+                fieldName: '外语',
+                required: true,
+                defaultValue: 88
+            }]
+        }, {
+            category: '职业技能',
+            content: '机试(200分)+ 技能展示(100分)',
+            details: [{
+                enumRuleCategory: 'Enroll',
+                enumInputType: 'Score',
+                label: '笔试/机试',
+                options: ["200"],
+                fieldName: '笔试/机试',
+                required: true,
+                defaultValue: 180
+            }, {
+                enumRuleCategory: 'Enroll',
+                enumInputType: 'Score',
+                label: '技能展示',
+                options: ["100"],
+                fieldName: '技能展示',
+                required: true,
+                defaultValue: 81
+            }]
+        }]
+    }
+    // init model
+    data.value.rules.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
+            }
+        })
+    })
+})
+
+provide(VOLUNTARY_FORM, data)
+provide(VOLUNTARY_MODEL, model)
 </script>
 
 <style lang="scss">

+ 157 - 0
src/pagesOther/pages/voluntary/result/components/voluntary-result-analysis.vue

@@ -0,0 +1,157 @@
+<template>
+    <view class="p-28 bg-white rounded-xl">
+        <voluntary-result-title title="概率分析"/>
+        <view class="h-100">
+            <ie-echart :option="option"/>
+        </view>
+    </view>
+</template>
+
+<script setup lang="ts" name="VoluntaryResultAnalysis">
+import VoluntaryResultTitle from "@/pagesOther/pages/voluntary/result/components/voluntary-result-title.vue";
+
+const option = ref({})
+const drawRing = (rate: number | null) => {
+    option.value = {
+        grid: {
+            left: 0,
+            right: 0
+        },
+        series: [
+            {
+                type: 'gauge',
+                center: ['50%', '75%'],
+                startAngle: 180,
+                endAngle: 0,
+                min: 0,
+                max: 100,
+                radius: '120%',
+                itemStyle: {
+                    color: {
+                        type: 'linear',
+                        x: 0,
+                        y: 0,
+                        x2: 1,
+                        y2: 0,
+                        colorStops: [{
+                            offset: 0, color: '#31A0FC'
+                        }, {
+                            offset: 1, color: '#70C8FD'
+                        }]
+                    }
+                },
+                progress: {
+                    show: true,
+                    roundCap: true,
+                    width: 10,
+                },
+                pointer: {
+                    show: false,
+                },
+                anchor: {
+                    show: false
+                },
+                axisLine: {
+                    roundCap: true,
+                    lineStyle: {
+                        color: [[1, '#e2e2e2']],
+                        width: 10
+                    }
+                },
+                axisTick: {
+                    show: false
+                },
+                splitLine: {
+                    show: false
+                },
+                axisLabel: {
+                    show: false,
+                },
+                detail: {
+                    show: true,
+                    formatter: (v: any) => {
+                        return isNaN(v) ? '--' : (v + '%');
+                    },
+                    color: '#333',
+                    fontSize: 20,
+                    fontWeight: 'bold',
+                    offsetCenter: [0, '-20%']
+                },
+                title: {
+                    show: true,
+                    color: '#666666',
+                    fontSize: 10,
+                    offsetCenter: [0, '20%'],
+                },
+                data: [
+                    {
+                        value: rate,
+                        name: rate === null ? '无概率' : '录取概率'
+                    }
+                ]
+            },
+            {
+                type: 'gauge',
+                center: ['50%', '75%'],
+                startAngle: 180,
+                endAngle: 0,
+                min: 0,
+                max: 100,
+                radius: '120%',
+                z: 3,
+                progress: {
+                    show: false,
+                },
+                pointer: {
+                    showAbove: true,
+                    icon: 'circle',
+                    width: 7,
+                    offsetCenter: [0, '-61%'],
+                    itemStyle: {
+                        color: '#fff'
+                    }
+                },
+                anchor: {
+                    show: false
+                },
+                axisLine: {
+                    roundCap: true,
+                    lineStyle: {
+                        color: [[1, 'transparent']],
+                        width: 10
+                    }
+                },
+                axisTick: {
+                    show: false
+                },
+                splitLine: {
+                    show: false
+                },
+                axisLabel: {
+                    show: false,
+                },
+                detail: {
+                    show: false,
+                },
+                title: {
+                    show: false,
+                },
+                data: [
+                    {
+                        value: rate
+                    }
+                ]
+            }
+        ]
+    }
+}
+
+onMounted(async () => {
+    await nextTick()
+    drawRing(70)
+})
+</script>
+
+<style scoped>
+
+</style>

+ 19 - 0
src/pagesOther/pages/voluntary/result/components/voluntary-result-title.vue

@@ -0,0 +1,19 @@
+<template>
+    <view class="relative">
+        <view class="pl-10 text-28 text-fore-title font-bold relative z-10">{{ title }}</view>
+        <view class="w-90 h-20 rounded-full absolute -bottom-3 bg-gradient-to-br from-cyan-300 to-white"/>
+    </view>
+</template>
+
+<script setup lang="ts" name="VoluntaryResultTitle">
+defineProps({
+    title: {
+        type: String,
+        default: ''
+    }
+})
+</script>
+
+<style scoped>
+
+</style>

+ 42 - 0
src/pagesOther/pages/voluntary/result/result.vue

@@ -0,0 +1,42 @@
+<template>
+    <ie-page bg-color="#F6F8FA">
+        <ie-navbar title="院校概率分析" transparent bg-color="#FFFFFF" title-color="black" :keep-title-color="true"/>
+        <!-- #ifdef MP-WEIXIN -->
+        <view class="h-90" style="background: linear-gradient(to Right, rgba(100, 200, 255, 1), rgba(165, 229, 255, 0.87))"/>
+        <!-- #endif -->
+        <ie-image is-oss src="/volunteer/voluntary/result/banner.png"/>
+        <view class="px-30 flex flex-col gap-30 -mt-180 z-1">
+            <voluntary-form-major class="!bg-white !mt-0"/>
+            <voluntary-result-analysis />
+        </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 {useTransferPage} from "@/hooks/useTransferPage";
+import {VoluntaryDto} from "@/types/voluntary";
+import VoluntaryFormMajor from "@/pagesOther/pages/voluntary/index/components/voluntary-form-major.vue";
+import VoluntaryResultAnalysis from "@/pagesOther/pages/voluntary/result/components/voluntary-result-analysis.vue";
+import {VOLUNTARY_FORM, VOLUNTARY_MODEL} from "@/types/injectionSymbols";
+
+const {prevData} = useTransferPage<VoluntaryDto, any>()
+const data = computed(() => prevData.value.data)
+const model = computed(() => prevData.value.model)
+
+const handleSubmit = async () => {
+
+}
+
+provide(VOLUNTARY_FORM, data)
+provide(VOLUNTARY_MODEL, model)
+</script>
+
+<style lang="scss">
+
+</style>

+ 2 - 1
src/types/index.ts

@@ -6,6 +6,7 @@ import * as System from './system';
 import * as Major from "./major";
 import * as Career from "./career";
 import * as Tree from "./tree";
+import * as Voluntary from "./voluntary";
 import { VipCardInfo } from "./user";
 import { EnumExamMode, EnumExamType, EnumReviewMode } from "@/common/enum";
 
@@ -130,4 +131,4 @@ export interface SwiperTabItem {
 
 
 
-export { Study, User, News, Transfer, System, Major, Career, Tree };
+export { Study, User, News, Transfer, System, Major, Career, Tree, Voluntary };

+ 13 - 6
src/types/injectionSymbols.ts

@@ -1,7 +1,8 @@
-import type { InjectionKey } from 'vue'
-import { StudyPlan, StudyPlanStats } from './study';
-import { Study, Transfer } from '.';
-import { useExam } from '@/composables/useExam';
+import type {InjectionKey} from 'vue'
+import {StudyPlan, StudyPlanStats} from './study';
+import {Study, Transfer, Voluntary} from '.';
+import {useExam} from '@/composables/useExam';
+
 /**
  * 打开知识点记录详情
  */
@@ -11,7 +12,7 @@ export const OPEN_KNOWLEDGE_DETAIL = Symbol('OPEN_KNOWLEDGE_DETAIL') as Injectio
  * 打开刷题记录详情
  */
 export const OPEN_PRACTICE_DETAIL = Symbol('OPEN_PRACTICE_DETAIL') as InjectionKey<(id: number, name: string) => void>;
- 
+
 /**
  * 打开视频记录详情
  */
@@ -39,4 +40,10 @@ export const OPEN_VIP_POPUP = Symbol('OPEN_VIP_POPUP') as InjectionKey<() => voi
 /**
  * 关闭VIP弹窗
  */
-export const CLOSE_VIP_POPUP = Symbol('CLOSE_VIP_POPUP') as InjectionKey<() => void>;
+export const CLOSE_VIP_POPUP = Symbol('CLOSE_VIP_POPUP') as InjectionKey<() => void>;
+
+/*
+* 计算录取概率
+* */
+export const VOLUNTARY_FORM = Symbol('VOLUNTARY_FORM') as InjectionKey<Ref<Voluntary.SelectedCollegeMajorWithRules>>
+export const VOLUNTARY_MODEL = Symbol('VOLUNTARY_MODEL') as InjectionKey<Ref<Voluntary.VoluntaryModel>>

+ 47 - 0
src/types/voluntary.ts

@@ -0,0 +1,47 @@
+import {SelectedUniversityMajor} from "@/types/study";
+import {DictItem} from "@/types/index";
+
+export type EnumInputType = 'Score' | 'Number' | 'Text' | 'Radio' | 'Picker' | 'Checkbox' | 'Eyesight';
+export type EnumRuleCategory = 'Enroll' | 'Special';
+
+export interface EnrollRuleItem {
+    defaultValue?: string | number;
+    description?: string;
+    dictOptions?: DictItem[];
+    dotDisable?: boolean
+    enumInputType: EnumInputType;
+    enumRuleCategory: EnumRuleCategory;
+    fieldName: string;
+    keyboardMode?: string
+    label: string;
+    max?: number
+    min?: number
+    mutexOption?: string[] | number[];
+    options?: string[] | number[];
+    placeholder?: string;
+    regex?: string;
+    required?: boolean;
+    tips?: string;
+    readonly?: boolean;
+}
+
+export interface EnrollRule {
+    category: string;
+    content: string;
+
+    details: EnrollRuleItem[]
+}
+
+export interface SelectedCollegeMajorWithRules extends SelectedUniversityMajor {
+    groupName: string;
+    rules: EnrollRule[];
+}
+
+export interface VoluntaryModel extends Record<string, string | number | null> {
+
+}
+
+export interface VoluntaryDto {
+    data: SelectedCollegeMajorWithRules;
+    model: VoluntaryModel;
+}

+ 123 - 118
tailwind.config.js

@@ -1,129 +1,134 @@
 /** @type {import('tailwindcss').Config} */
 
 function generateSize(size, unit) {
-  const result = {};
-  for (let i = 0; i <= size; i++) {
-    result[`${i}`] = `${i}${unit}`;
-  }
-  return result;
+    const result = {};
+    for (let i = 0; i <= size; i++) {
+        result[`${i}`] = `${i}${unit}`;
+    }
+    return result;
 }
 
 module.exports = {
-  plugins: [],
-  corePlugins: {
-    container: false,
-    preflight: false,
-  },
-  content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
-  safelist: [{ pattern: /grid-cols-[1-9]/ }],
-  theme: {
-    // 间距
-    spacing: {
-      ...generateSize(500, "rpx"),
+    plugins: [],
+    corePlugins: {
+        container: false,
+        preflight: false,
     },
-    extend: {
-      // 圆角
-      borderRadius: generateSize(50, "px"),
-      // 行高
-      lineHeight: generateSize(375, "rpx"),
-      // 字间距
-      letterSpacing: generateSize(20, "rpx"),
-      // 旋转
-      rotate: generateSize(180, "deg"),
-      zIndex: generateSize(100, ""),
-      fontSize: {
-        base: "15px",
-        sm: "14px",
-        xs: "13px",
-        "2xs": "12px",
-        "3xs": "11px",
-        "4xs": "10px",
-        lg: "16px",
-        xl: "18px",
-        "2xl": "20px",
-        "3xl": "24px",
-        "4xl": "28px",
-        "5xl": "32px",
-        "6xl": "36px",
-        "7xl": "40px",
-        ...generateSize(70, "rpx"),
-      },
-      colors: {
-        transparent: "transparent",
-        primary: {
-          DEFAULT: "var(--primary-color)",
-          50: "var(--primary-color-50)",
-          100: "var(--primary-color-100)",
-          200: "var(--primary-color-200)",
-          300: "var(--primary-color-300)",
-          400: "var(--primary-color-400)",
-          500: "var(--primary-color-500)",
-          600: "var(--primary-color-600)",
-          700: "var(--primary-color-700)",
-          800: "var(--primary-color-800)",
-          900: "var(--primary-color-900)",
-          950: "var(--primary-color-950)",
-          light: "var(--primary-light-color)",
+    content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
+    safelist: [{pattern: /grid-cols-[1-9]/}],
+    theme: {
+        // 间距
+        spacing: {
+            ...generateSize(500, "rpx"),
         },
-        warning: {
-          DEFAULT: "var(--warning)",
-          dark: "var(--warning-dark)",
-          disabled: "var(--warning-disabled)",
-          light: "var(--warning-light)",
-        },
-        success: {
-          DEFAULT: "var(--success)",
-          dark: "var(--success-dark)",
-          disabled: "var(--success-disabled)",
-          light: "var(--success-light)",
-        },
-        danger: {
-          DEFAULT: "var(--danger)",
-          dark: "var(--danger-dark)",
-          disabled: "var(--danger-disabled)",
-          light: "var(--danger-light)",
-        },
-        // 只添加部分颜色,防止覆盖掉老颜色
-        fore: {
-          title: "var(--fore-title)",
-          subtitle: "var(--fore-subtitle)",
-          content: "var(--fore-content)",
-          subcontent: "var(--fore-subcontent)",
-          tip: "var(--fore-tip)",
-          "tip-light": "var(--fore-tip-light)",
-          light: "var(--fore-light)",
-          disabled: "var(--fore-disabled)",
-          placeholder: "var(--fore-placeholder)",
-        },
-        back: {
-          DEFAULT: "var(--back)",
-          light: "var(--back-light)",
-        },
-        border: {
-          DEFAULT: "var(--border)",
-          light: "var(--border-light)",
-        },
-        danger: {
-          DEFAULT: "var(--danger)",
-          dark: "var(--danger-dark)",
-          disabled: "var(--danger-disabled)",
-          light: "var(--danger-light)",
+        extend: {
+            borderWidth: generateSize(100, "rpx"),
+            // 圆角
+            borderRadius: generateSize(50, "px"),
+            // 行高
+            lineHeight: generateSize(375, "rpx"),
+            // 字间距
+            letterSpacing: generateSize(20, "rpx"),
+            // 旋转
+            rotate: generateSize(180, "deg"),
+            zIndex: generateSize(100, ""),
+            fontSize: {
+                base: "15px",
+                sm: "14px",
+                xs: "13px",
+                "2xs": "12px",
+                "3xs": "11px",
+                "4xs": "10px",
+                lg: "16px",
+                xl: "18px",
+                "2xl": "20px",
+                "3xl": "24px",
+                "4xl": "28px",
+                "5xl": "32px",
+                "6xl": "36px",
+                "7xl": "40px",
+                ...generateSize(70, "rpx"),
+            },
+            colors: {
+                transparent: "transparent",
+                primary: {
+                    DEFAULT: "var(--primary-color)",
+                    50: "var(--primary-color-50)",
+                    100: "var(--primary-color-100)",
+                    200: "var(--primary-color-200)",
+                    300: "var(--primary-color-300)",
+                    400: "var(--primary-color-400)",
+                    500: "var(--primary-color-500)",
+                    600: "var(--primary-color-600)",
+                    700: "var(--primary-color-700)",
+                    800: "var(--primary-color-800)",
+                    900: "var(--primary-color-900)",
+                    950: "var(--primary-color-950)",
+                    light: "var(--primary-light-color)",
+                },
+                warning: {
+                    DEFAULT: "var(--warning)",
+                    dark: "var(--warning-dark)",
+                    disabled: "var(--warning-disabled)",
+                    light: "var(--warning-light)",
+                },
+                success: {
+                    DEFAULT: "var(--success)",
+                    dark: "var(--success-dark)",
+                    disabled: "var(--success-disabled)",
+                    light: "var(--success-light)",
+                },
+                danger: {
+                    DEFAULT: "var(--danger)",
+                    dark: "var(--danger-dark)",
+                    disabled: "var(--danger-disabled)",
+                    light: "var(--danger-light)",
+                },
+                info: {
+                    DEFAULT: "var(--info)"
+                },
+                // 只添加部分颜色,防止覆盖掉老颜色
+                fore: {
+                    title: "var(--fore-title)",
+                    subtitle: "var(--fore-subtitle)",
+                    content: "var(--fore-content)",
+                    subcontent: "var(--fore-subcontent)",
+                    tip: "var(--fore-tip)",
+                    "tip-light": "var(--fore-tip-light)",
+                    light: "var(--fore-light)",
+                    disabled: "var(--fore-disabled)",
+                    placeholder: "var(--fore-placeholder)",
+                },
+                back: {
+                    DEFAULT: "var(--back)",
+                    light: "var(--back-light)",
+                },
+                border: {
+                    DEFAULT: "var(--border)",
+                    light: "var(--border-light)",
+                },
+                danger: {
+                    DEFAULT: "var(--danger)",
+                    dark: "var(--danger-dark)",
+                    disabled: "var(--danger-disabled)",
+                    light: "var(--danger-light)",
+                },
+
+            },
+            boxShadow: {
+                up: "0 -1px 3px 0 rgb(0 0 0 / 0.1), 0 -1px 2px -1px rgb(0 0 0 / 0.1)",
+            },
+            flex: {
+                2: "2",
+                3: "3",
+                4: "4",
+                5: "5",
+                6: "6",
+                7: "7",
+                8: "8",
+                9: "9",
+                10: "10",
+            },
         },
-      },
-      boxShadow: {
-        up: "0 -1px 3px 0 rgb(0 0 0 / 0.1), 0 -1px 2px -1px rgb(0 0 0 / 0.1)",
-      },
-      flex: {
-        2: "2",
-        3: "3",
-        4: "4",
-        5: "5",
-        6: "6",
-        7: "7",
-        8: "8",
-        9: "9",
-        10: "10",
-      },
     },
-  },
 };