2 커밋 0c859dad91 ... f40a114ed0

작성자 SHA1 메시지 날짜
  shmily1213 f40a114ed0 优化排序组件效果 4 주 전
  shmily1213 9c5b7247e5 修改院校库省市选择UI 1 개월 전

+ 9 - 0
src/api/modules/university.ts

@@ -512,3 +512,12 @@ export function removeConcernedUniversity(params: any) {
 export function getCollectedUniversities(params: any) {
     return flyio.get('/front/customer/university/list', params) as Promise<ApiResponseList<Major.University>>;
 }
+
+/**
+ * 获取院校位置
+ * @param params
+ * @returns
+ */
+export function getUniversityLocation() {
+    return flyio.get('/front/university/locations2', {}) as Promise<ApiResponse<{cityName: string[], location: string}[]>>;
+}

+ 32 - 19
src/components/ie-dropdown/ie-dropdown-item.vue

@@ -3,7 +3,7 @@
     <view :id="`dropdown-trigger-${config.prop}`" class="w-full flex items-center justify-center gap-10 h-full relative"
       @click="handleClick">
       <view class="relative">
-        <view class="text-xs text-center ellipsis-1" :class="[show ? 'text-primary' : 'text-fore-title ']">
+        <view class="text-[14px] text-center ellipsis-1" :class="[show ? 'text-primary' : 'text-fore-title ']">
           {{ config.label }}
         </view>
         <view class="absolute -top-4 -right-4 w-6 h-6 bg-red-500 rounded-full" v-if="hasValue"></view>
@@ -19,24 +19,27 @@
       <view class="relative z-1 overflow-hidden ">
         <view class="left-0 right-0 z-1 w-full bg-white box-border transition-transform duration-300 ease-out"
           :style="contentStyle" @click="">
-          <scroll-view class="relative max-h-[300px]" scroll-y>
-            <uv-checkbox-group v-model="checkboxValue" placement="column" iconPlacement="right" borderBottom>
-              <uv-checkbox :customStyle="{ marginBottom: '0', paddingBottom: '0', height: '38px', padding: '0 10px' }"
-                v-for="(item, index) in config.options" :key="index" :label="item.label"
-                :name="item.value"></uv-checkbox>
-            </uv-checkbox-group>
-          </scroll-view>
-          <view class="flex items-center justify-between gap-24 p-[12px]">
-            <view class="flex-1">
-              <uv-button type="primary" plain shape="circle" @click="handleReset">重置</uv-button>
+          <slot v-if="config.slot" :name="config.slot"></slot>
+          <template v-else>
+            <scroll-view class="relative max-h-[300px]" scroll-y>
+              <uv-checkbox-group v-model="checkboxValue" placement="column" iconPlacement="right" borderBottom>
+                <uv-checkbox :customStyle="{ marginBottom: '0', paddingBottom: '0', height: '38px', padding: '0 10px' }"
+                  v-for="(item, index) in config.options" :key="index" :label="item.label"
+                  :name="item.value"></uv-checkbox>
+              </uv-checkbox-group>
+            </scroll-view>
+            <view class="flex items-center justify-between gap-24 p-[12px]">
+              <view class="flex-1">
+                <uv-button type="primary" plain shape="circle" @click="handleReset">重置</uv-button>
+              </view>
+              <view class="flex-1">
+                <uv-button type="primary" shape="circle" @click="handleSubmit">
+                  <text>确定</text>
+                  <text v-if="checkboxValue.length > 0">({{ checkboxValue.length }})</text>
+                </uv-button>
+              </view>
             </view>
-            <view class="flex-1">
-              <uv-button type="primary" shape="circle" @click="handleSubmit">
-                <text>确定</text>
-                <text v-if="checkboxValue.length > 0">({{ checkboxValue.length }})</text>
-              </uv-button>
-            </view>
-          </view>
+          </template>
         </view>
       </view>
     </view>
@@ -44,7 +47,7 @@
   </view>
 </template>
 <script lang="ts" setup>
-import { type Dropdown } from '@/types';
+import type { Dropdown } from '@/types';
 import { DROPDOWN_SYMBOL } from './ie-dropdown-hooks';
 import { getCurrentInstance } from 'vue';
 
@@ -90,6 +93,9 @@ const contentStyle = computed(() => {
 });
 
 watch(() => dropdown?.openIndex.value, (newVal) => {
+  if (props.config.custom) {
+    return;
+  }
   if (newVal === props.index) {
     open();
   } else {
@@ -163,11 +169,18 @@ const handleMaskClick = () => {
   }
 }
 
+const emits = defineEmits<{
+  (e: 'click'): void;
+  (e: 'open', config: Dropdown.DropdownItem): void;
+  (e: 'close', config: Dropdown.DropdownItem): void;
+}>();
 const handleClick = () => {
   if (isOpen.value) {
     dropdown?.close();
+    emits('close', props.config);
   } else {
     dropdown?.open(props.index);
+    emits('open', props.config);
   }
 }
 </script>

+ 23 - 1
src/components/ie-dropdown/ie-dropdown.vue

@@ -1,7 +1,13 @@
 <template>
   <view class="flex items-center gap-20 px-20 h-[44px] border-0 border-b border-solid border-border-light">
     <view v-for="(item, index) in configs" :key="item.prop" class="flex-1 min-w-1 h-full">
-      <ie-dropdown-item :config="item" :index="index" :absolute="absolute" />
+      <ie-dropdown-item :config="item" :index="index" :absolute="absolute" @open="handleOpen" @close="handleClose">
+        <template #[getSlotName(item.slot)]>
+          <view v-if="item.slot">
+            <slot :name="getSlotName(item.slot)" :index="index"></slot>
+          </view>
+        </template>
+      </ie-dropdown-item>
     </view>
   </view>
 </template>
@@ -10,9 +16,25 @@ import IeDropdownItem from './ie-dropdown-item.vue';
 import type { Dropdown } from '@/types';
 import { useDropdown, DROPDOWN_SYMBOL } from './ie-dropdown-hooks';
 
+const getSlotName = (slot?: string) => {
+  return slot ? `${slot}` : '';
+}
+
 const emit = defineEmits<{
   (e: 'change', value: any): void;
+  (e: 'open', config: Dropdown.DropdownItem): void;
+  (e: 'close', config: Dropdown.DropdownItem): void;
 }>();
+const handleOpen = (config: Dropdown.DropdownItem) => {
+  if (config.custom) {
+    emit('open', config);
+  }
+}
+const handleClose = (config: Dropdown.DropdownItem) => {
+  if (config.custom) {
+    emit('close', config);
+  }
+}
 const modelValue = defineModel<Record<string, any>>();
 const props = defineProps<{
   configs: Dropdown.DropdownItem[];

+ 1 - 1
src/components/ie-safe-toolbar/ie-safe-toolbar.vue

@@ -25,7 +25,7 @@ const props = defineProps({
   },
   bgColor: {
     type: String,
-    default: '#FFFFFF'
+    default: 'transparent'
   }
 });
 const minHeight = computed(() => {

+ 21 - 24
src/pagesMain/pages/me/components/me-menu.vue

@@ -64,26 +64,26 @@ type MenuItem = {
 };
 
 const menus = computed(() => [
-    {
-        name: '测评报告',
-        icon: '/static/personal/test_report.png',
-        pagePath: routes.testCenterList,
-    },
-    {
-        name: '我的收藏',
-        icon: '/static/personal/my_collected.png',
-        pagePath: routes.pageCollect,
-    },
-    {
-        name: '我的志愿表',
-        icon: '/static/personal/my_simulated.png',
-        pagePath: userStore.isVHS ? routes.VHSList : routes.voluntaryList,
-    },
-    {
-        name: '绑定会员卡',
-        icon: '/static/personal/bind_card.png',
-        pagePath: routes.pageCardVerify,
-    }
+  {
+    name: '测评报告',
+    icon: '/static/personal/test_report.png',
+    pagePath: routes.testCenterList,
+  },
+  {
+    name: '我的收藏',
+    icon: '/static/personal/my_collected.png',
+    pagePath: routes.pageCollect,
+  },
+  {
+    name: '我的志愿表',
+    icon: '/static/personal/my_simulated.png',
+    pagePath: userStore.isVHS ? routes.VHSList : routes.voluntaryList,
+  },
+  {
+    name: '绑定会员卡',
+    icon: '/static/personal/bind_card.png',
+    pagePath: routes.pageCardVerify,
+  }
 ]);
 const cellStyle = {
   padding: '30rpx 30rpx'
@@ -114,10 +114,7 @@ const handleQuestion = async () => {
   });
 }
 const handleLogout = async () => {
-  const confirm = await userStore.askLogout();
-  if (confirm) {
-
-  }
+  await userStore.askLogout();
 }
 const handlePhone = async () => {
   userStore.callContactPhone();

+ 1 - 1
src/pagesOther/components/ie-condition-dropdown/ie-condition-dropdown-popup.vue

@@ -59,7 +59,7 @@ const relTop = computed(() => baseBottom.value - baseTop.value - 1)
 // #endif
 // #ifdef MP-WEIXIN
 // TODO:小程序兼容性问题
-const refTop = ref(0)
+const relTop = uni.getSystemInfoSync().statusBarHeight
 // #endif
 
 const handleReset = () => {

+ 208 - 0
src/pagesOther/pages/university/index/components/plus/college-conditions-location.vue

@@ -0,0 +1,208 @@
+<template>
+  <view class="bg-white flex flex-col h-[350px]">
+    <view class="flex flex-1 min-h-1">
+      <scroll-view scroll-y class="h-full w-[80px] bg-back-light">
+        <view v-for="(item, index) in list" :key="item.name" class="h-[36px] px-20 flex items-center"
+          :class="[currentIndex === index ? 'bg-white text-primary' : 'text-fore-title']" @click="handleClick(index)">
+          <view class="text-[13px] ellipsis-1">{{ item.name }}</view>
+        </view>
+      </scroll-view>
+      <scroll-view scroll-y class="flex-1 min-w-1 h-full bg-white">
+        <view class="grid grid-cols-3 gap-20 p-20">
+          <view v-for="item in childrenList" :key="item.name"
+            class="h-[34px] px-20 flex items-center rounded-6 bg-back-light border border-solid relative overflow-hidden"
+            :class="[isSelected(item) ? 'border-primary text-primary' : 'border-transparent text-fore-title']"
+            @click="handleChildrenClick(item)">
+            <view v-if="isSelected(item)" class="absolute bottom-0 -right-2 p-2 bg-primary rounded-tl-4 rounded-br-6">
+              <uv-icon name="checkmark" size="10" color="#fff" />
+            </view>
+            <view class="flex-1 min-w-1 text-[13px] text-center ellipsis-1">{{ item.name }}</view>
+          </view>
+        </view>
+      </scroll-view>
+    </view>
+    <view class="flex items-center justify-between gap-24 p-[12px]">
+      <view class="flex-1">
+        <uv-button type="primary" plain shape="circle" @click="handleReset">重置</uv-button>
+      </view>
+      <view class="flex-1">
+        <uv-button type="primary" shape="circle" @click="handleSubmit">
+          <text>确定</text>
+        </uv-button>
+      </view>
+    </view>
+  </view>
+</template>
+<script lang="ts" setup>
+import { getUniversityLocation } from "@/api/modules/university";
+import type { Dropdown } from '@/types';
+import { DROPDOWN_SYMBOL } from '@/components/ie-dropdown/ie-dropdown-hooks';
+
+const props = defineProps<{
+  index: number;
+}>();
+const dropdown = inject(DROPDOWN_SYMBOL);
+
+type LocationItem = {
+  name: string;
+  parent?: string;
+  children: LocationItem[];
+}
+const list = ref<LocationItem[]>([]);
+const currentIndex = ref(0);
+const childrenList = computed(() => {
+  return list.value[currentIndex.value]?.children || [];
+});
+
+const selectedProvince = ref<string>('');
+const selectedCity = ref<string[]>([]);
+const selected = ref<LocationItem[]>([]);
+
+watch(() => dropdown?.openIndex.value, (newVal) => {
+  if (newVal === props.index) {
+    initValue();
+  }
+});
+
+const isSelected = (item: LocationItem) => {
+  const currentProvince = selected.value.find(province => province.name === item.parent);
+  if (currentProvince) {
+    return currentProvince.children.some(city => city.name === item.name);
+  }
+  return false;
+}
+
+const initValue = () => {
+  const { location } = dropdown?.form.value || {};
+  if (location) {
+    // eg. 湖南:长沙,张家界;河南:开封,郑州; 还原成 selected 数据
+    const locationParams: LocationItem[] = location.split(';').filter((item: string) => item).map((item: string) => {
+      return {
+        name: item.split(':')[0],
+        children: item.split(':')[1].split(',').map(city => {
+          return {
+            name: city,
+            parent: item.split(':')[0],
+            children: []
+          }
+        }),
+      }
+    });
+    const aar: LocationItem[] = [];
+    locationParams.forEach(item => {
+      aar.push({
+        name: item.name,
+        children: item.children
+      });
+    })
+    selected.value = aar;
+    // 找到第一个选中的省份
+    const firstIndex = list.value.findIndex(item => item.name === selected.value[0].name);
+    if (firstIndex !== -1) {
+      currentIndex.value = firstIndex;
+    } else {
+      currentIndex.value = 0;
+    }
+  } else {
+    selected.value = [];
+    currentIndex.value = 0;
+  }
+}
+
+const handleReset = () => {
+  selected.value = [];
+};
+const handleSubmit = () => {
+  // eg. 湖南:长沙,张家界;河南:开封,郑州;
+  const value = selected.value.reduce((acc, item) => {
+    return acc + item.name + ':' + item.children.map(city => city.name).join(',') + ';';
+  }, '');
+  dropdown?.submit(props.index, value);
+  dropdown?.close();
+};
+
+
+const handleClick = (index: number) => {
+  currentIndex.value = index;
+  selectedProvince.value = list.value[index]?.name ?? '';
+  selectedCity.value = [];
+}
+
+const handleChildrenClick = (item: LocationItem) => {
+  selected.value = selected.value.filter(province => province.name === selectedProvince.value);
+  if (item.name === '不限') {
+    const currentProvince = list.value[currentIndex.value];
+    const currentSelected = selected.value.find(province => province.name === currentProvince.name);
+    if (currentSelected) {
+      currentSelected.children = [{
+        name: '不限',
+        children: []
+      }];
+    } else {
+      selected.value.push({
+        name: currentProvince.name,
+        children: [{
+          name: '不限',
+          children: []
+        }]
+      });
+    }
+    return;
+  } else {
+    const currentProvince = list.value[currentIndex.value];
+    const currentSelected = selected.value.find(province => province.name === currentProvince.name);
+    if (currentSelected) {
+      currentSelected.children = currentSelected!.children.filter(city => city.name !== '不限');
+      if (currentSelected.children.findIndex(city => city.name === item.name) === -1) {
+        currentSelected.children.push(item);
+      } else {
+        currentSelected.children = currentSelected.children.filter(city => city.name !== item.name);
+        // 如果没有选中的,就自动选中不限
+        if (currentSelected.children.length === 0) {
+          currentSelected.children.push({
+            name: '不限',
+            parent: currentProvince.name,
+            children: []
+          });
+        }
+      }
+    } else {
+      selected.value.push({
+        name: currentProvince.name,
+        children: [item]
+      });
+    }
+  }
+}
+const loadData = () => {
+  getUniversityLocation().then(res => {
+    list.value = res.data.map(item => {
+      return {
+        name: item.location,
+        children: [
+          {
+            name: '不限',
+            parent: item.location,
+            children: []
+          },
+          ...item.cityName.map(city => {
+            return {
+              name: city,
+              parent: item.location,
+              children: []
+            }
+          })
+        ]
+      }
+    });
+  });
+}
+
+onMounted(() => {
+  selectedProvince.value = '';
+  selectedCity.value = [];
+  currentIndex.value = 0;
+});
+loadData();
+</script>
+<style lang="scss" scoped></style>

+ 100 - 140
src/pagesOther/pages/university/index/components/plus/college-conditions-picker.vue

@@ -1,164 +1,124 @@
 <template>
-    <view>
-        <ie-dropdown :configs="configs" v-model="form" :absolute="absolute" @change="handleChange"/>
-    </view>
+  <view>
+    <ie-dropdown :configs="configs" v-model="form" :absolute="absolute" @change="handleChange">
+      <template #location="{ index }">
+        <college-conditions-location :index="index" />
+      </template>
+    </ie-dropdown>
+  </view>
 </template>
 
 <script setup lang="ts">
-
-import type {Dropdown} from "@/types";
-import {UNIVERSITY_FILTER} from "@/types/injectionSymbols";
-import type {UniversityQueryDto, UniversityFilter} from "@/types/university";
-import {useUserStore} from "@/store/userStore";
+import CollegeConditionsLocation from "./college-conditions-location.vue";
+import type { Dropdown } from "@/types";
+import { UNIVERSITY_FILTER } from "@/types/injectionSymbols";
+import type { UniversityQueryDto, UniversityFilter } from "@/types/university";
+import { useUserStore } from "@/store/userStore";
 
 const props = defineProps<{
-    options: UniversityFilter;
-    absolute?: boolean;
+  options: UniversityFilter;
+  absolute?: boolean;
 }>()
 const filter = inject(UNIVERSITY_FILTER) || ref({} as UniversityQueryDto)
 const userStore = useUserStore()
 
 const emit = defineEmits<{
-    (e: 'change', value: UniversityQueryDto): void;
+  (e: 'change', value: UniversityQueryDto): void;
 }>();
 const options = computed(() => {
-    // return userStore.isHN ? [
-    //     {
-    //         label: '院校层次',
-    //         prop: 'features',
-    //         optionKey: 'features',
-    //         keyName: 'label',
-    //         keyValue: 'value',
-    //     },
-    //     {
-    //         label: '院校类型',
-    //         prop: 'type',
-    //         optionKey: 'types',
-    //         keyName: 'label',
-    //         keyValue: 'value',
-    //     },
-    //     {
-    //         label: '办学类型',
-    //         prop: 'natureTypeCN',
-    //         optionKey: 'natureTypes',
-    //         keyName: 'label',
-    //         keyValue: 'value',
-    //     },
-    //     {
-    //         label: '院校梯队',
-    //         prop: 'tiers',
-    //         optionKey: 'tiers',
-    //         keyName: 'label',
-    //         keyValue: 'value',
-    //     },
-    // ] : [
-    //     {
-    //         label: '院校层次',
-    //         prop: 'features',
-    //         optionKey: 'features',
-    //         keyName: 'label',
-    //         keyValue: 'value',
-    //     },
-    //     {
-    //         label: '院校类型',
-    //         prop: 'type',
-    //         optionKey: 'types',
-    //         keyName: 'label',
-    //         keyValue: 'value',
-    //     },
-    //     {
-    //         label: '办学类型',
-    //         prop: 'natureTypeCN',
-    //         optionKey: 'natureTypes',
-    //         keyName: 'label',
-    //         keyValue: 'value',
-    //     },
-    //     {
-    //         label: '院校省份',
-    //         prop: 'location',
-    //         optionKey: 'locations',
-    //         keyName: 'label',
-    //         keyValue: 'value',
-    //     }
-    // ]
-    return [
-        {
-            label: '院校省市',
-            prop: 'location',
-            optionKey: 'locations',
-            keyName: 'label',
-            keyValue: 'value',
-        },
-        {
-            label: '院校类型',
-            prop: 'type',
-            optionKey: 'types',
-            keyName: 'label',
-            keyValue: 'value',
-        },
-        {
-            label: '办学类型',
-            prop: 'natureTypeCN',
-            optionKey: 'natureTypes',
-            keyName: 'label',
-            keyValue: 'value',
-        },
-        {
-            label: '院校梯队',
-            prop: 'tiers',
-            optionKey: 'tiers',
-            keyName: 'label',
-            keyValue: 'value',
-        }
-    ]
+  return [
+    {
+      label: '院校省市',
+      prop: 'location',
+      optionKey: 'locations',
+      keyName: 'label',
+      keyValue: 'value',
+      slot: 'location',
+    },
+    {
+      label: '院校类型',
+      prop: 'type',
+      optionKey: 'types',
+      keyName: 'label',
+      keyValue: 'value'
+    },
+    {
+      label: '办学类型',
+      prop: 'natureTypeCN',
+      optionKey: 'natureTypes',
+      keyName: 'label',
+      keyValue: 'value'
+    },
+    {
+      label: '院校梯队',
+      prop: 'tiers',
+      optionKey: 'tiers',
+      keyName: 'label',
+      keyValue: 'value'
+    }
+  ]
 })
 const form = ref<Record<string, any>>({});
 const configs = computed<Dropdown.DropdownItem[]>(() => {
-    return options.value.map(item => {
-        const list = props.options[item.optionKey as keyof UniversityFilter] || [];
-        if (Array.isArray(list)) {
-            return {
-                label: item.label,
-                prop: item.prop,
-                keyName: item.keyName,
-                keyValue: item.keyValue,
-                options: list.map(item => {
-                    return {
-                        label: item,
-                        value: item,
-                    }
-                }),
-                value: []
-            }
-        } else {
-            return {
-                label: item.label,
-                prop: item.prop,
-                keyName: item.keyName,
-                keyValue: item.keyValue,
-                options: Object.entries(list).map(([key, value]) => {
-                    return {
-                        label: value,
-                        value: key,
-                    }
-                }),
-                value: []
-            }
-        }
-    });
+  return options.value.map(item => {
+    const list = props.options[item.optionKey as keyof UniversityFilter] || [];
+    if (Array.isArray(list)) {
+      return {
+        label: item.label,
+        prop: item.prop,
+        keyName: item.keyName,
+        keyValue: item.keyValue,
+        options: list.map(item => {
+          return {
+            label: item,
+            value: item,
+          }
+        }),
+        value: [],
+        slot: item.slot,
+      }
+    } else {
+      return {
+        label: item.label,
+        prop: item.prop,
+        keyName: item.keyName,
+        keyValue: item.keyValue,
+        options: Object.entries(list).map(([key, value]) => {
+          return {
+            label: value,
+            value: key,
+          }
+        }),
+        value: [],
+        slot: item.slot,
+      }
+    }
+  });
 });
 const handleChange = (value: any) => {
-    filter.value = {
-        ...filter.value,
-        ...value,
-    }
-    emit('change', filter.value);
+  let locationParams: { location: string, cityName: string[] }[] = [];
+  if (value.location) {
+    locationParams = value.location.split(';').filter((item: string) => item).map((item: string) => {
+      return {
+        location: item.split(':')[0],
+        cityName: item.split(':')[1].split(',').filter(city => city !== '不限'),
+      }
+    });
+  }
+  filter.value = {
+    ...filter.value,
+    ...value,
+    // 省市新的参数要求:location: 湖南,河南; cityName: 长沙,张家界,开封,郑州;
+    location: locationParams.map(item => item.location).join(','),
+    cityName: locationParams.map(item => item.cityName).flat().join(','),
+  }
+  emit('change', filter.value);
 }
 
 watch(() => filter.value.tiers, tiers => {
-    // sync tier from outside
-    form.value.tiers = tiers
-}, {immediate: true})
+  // sync tier from outside
+  form.value.tiers = tiers
+}, { immediate: true })
 </script>
 
 <style scoped></style>

+ 2 - 2
src/pagesOther/pages/vhs/index/components/course-selector.vue

@@ -3,8 +3,8 @@
         <view class="pl-20">
             专业类别
         </view>
-        <view class="py-15 flex gap-20">
-            <uv-tags size="large" type="primary" shape="circle" icon="lock" plain plain-fill
+        <view class="py-15 flex gap-20 text-[15px]">
+            <uv-tags size="medium" type="primary" shape="circle" icon="lock" plain plain-fill
                      :text="currentUser.examMajorName"/>
         </view>
     </view>

+ 3 - 3
src/pagesOther/pages/vhs/index/components/score-form.vue

@@ -5,13 +5,13 @@
         <course-selector/>
         <uv-line margin="15px 0 0 0"/>
         <uv-input v-model="model.score" :disabled="isScoreLocked" placeholder="请输入您的分数"
-                  v-bind="inputCommon">
+                  v-bind="inputCommon" placeholderClass="text-[15px]" font-size="15px">
             <template #prefix>您的分数:</template>
         </uv-input>
-        <uv-input v-if="false" v-model="model.rank.lowestRank" disabled v-bind="inputCommon">
+        <uv-input v-if="false" v-model="model.rank.lowestRank" disabled v-bind="inputCommon" placeholderClass="text-[15px]" font-size="15px">
             <template #prefix>匹配位次:</template>
         </uv-input>
-        <uv-input v-model="model.seatInput" placeholder="输入成绩单位次" v-bind="inputCommon">
+        <uv-input v-model="model.seatInput" placeholder="输入成绩单位次" v-bind="inputCommon" placeholderClass="text-[15px]" font-size="15px">
             <template #prefix>填写位次:</template>
         </uv-input>
         <view class="font-[PingFang] text-content text-2xs mt-20">

+ 5 - 1
src/pagesOther/pages/vhs/index/components/vhs-majors-draggable-list.vue

@@ -36,7 +36,7 @@ const emits = defineEmits<{
   (e: "change", majors: VHS.VoluntaryMajor[]): void;
 }>();
 const touchHandle = ref(false)
-
+const dragRef = ref();
 const handleDragStart = () => {
   touchHandle.value = true;
   // refresherEnabled.value = false;
@@ -56,5 +56,9 @@ const handleDelete = (major: VHS.VoluntaryMajor) => {
 const toFixedLen = (i: number, len: number = 1) => {
   return String(i + 1).padStart(len, "0");
 }
+const refresh = () => {
+  dragRef.value?.refresh();
+}
+defineExpose({ refresh });
 </script>
 <style lang="scss" scoped></style>

+ 9 - 1
src/pagesOther/pages/vhs/index/components/voluntary-cart-popup.vue

@@ -62,7 +62,7 @@
                             <uv-button type="primary" size="mini" plain shape="circle" icon="trash"
                                        icon-color="primary" @click="handleRemoveAll(college)"/>
                         </view>
-                        <vhs-majors-draggable-list :majors="getSelectedSortedMajors(college)"
+                        <vhs-majors-draggable-list ref="draggableListRef" :majors="getSelectedSortedMajors(college)"
                                                    @delete="handleMajorDelete($event, college)"
                                                    @change="handleDragComplete($event, college)" />
                     </view>
@@ -198,9 +198,17 @@ const handleSave = async () => {
     await save(isMock.value)
 }
 
+const draggableListRef = ref();
 const open = () => {
     localPageNum.value = 1
     popup.value.open()
+    setTimeout(() => {
+      nextTick(() => {
+        draggableListRef.value?.forEach(item => {
+          item?.refresh();
+        });
+      });
+    }, 300)
 }
 
 const close = () => {

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

@@ -93,11 +93,13 @@ 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 postRenderRules(target.value, model.value)
-    const bigData: VoluntaryDto = {target: target.value, rules: rules.value, model: model.value, result}
-    transferTo(routes.voluntaryResult, {bigData})
+    try {
+      await form.value?.validate()
+      // make request
+      const { data: result } = await postRenderRules(target.value, model.value)
+      const bigData: VoluntaryDto = { target: target.value, rules: rules.value, model: model.value, result }
+      transferTo(routes.voluntaryResult, { bigData })
+    } catch (error) { }
 }
 
 provide(VOLUNTARY_TARGET, target)

+ 3 - 3
src/pagesSystem/pages/bind-teacher-profile/bind-teacher-profile.vue

@@ -4,7 +4,7 @@
     <uv-form labelPosition="left" :model="examTypeForm" labelWidth="70px" ref="formRef">
       <content-card title="个人信息">
         <uv-form-item label="姓名" prop="name" borderBottom required>
-          <uv-input v-model="form.nickName" border="none" placeholder="请输入姓名" placeholderClass="text-30"
+          <uv-input v-model="form.nickName" border="none" placeholder="请输入姓名" placeholderClass="text-[15px]"
             font-size="15px" :custom-style="customStyle">
           </uv-input>
         </uv-form-item>
@@ -81,13 +81,13 @@
 
       <content-card v-if="showSchoolInfo" title="学校信息">
         <uv-form-item label="学校名称" prop="form.campusName" borderBottom>
-          <uv-input v-model="form.campusName" border="none" placeholder="" placeholderClass="text-30" font-size="15px"
+          <uv-input v-model="form.campusName" border="none" placeholder="" placeholderClass="text-[15px]" font-size="15px"
             :custom-style="customStyle" readonly>
           </uv-input>
           <ie-image slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
         </uv-form-item>
         <uv-form-item label="所在班级" prop="form.campusClassName">
-          <uv-input v-model="form.campusClassName" border="none" placeholder="" placeholderClass="text-30"
+          <uv-input v-model="form.campusClassName" border="none" placeholder="" placeholderClass="text-[15px]"
             font-size="15px" :custom-style="customStyle" readonly>
           </uv-input>
           <ie-image slot="right" src="/static/image/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />

+ 2 - 0
src/types/dropdown.ts

@@ -6,6 +6,8 @@ export interface DropdownItem {
   keyName?: string;
   keyValue?: string;
   options?: DropdownOption[];
+  custom?: boolean;
+  slot?: string;
 }
 
 export interface DropdownOption {

+ 46 - 8
src/uni_modules/lime-drag/components/l-drag/l-drag.vue

@@ -19,7 +19,7 @@
 				:y="item.y"
 				:friction="friction"
 				:damping="damping"
-				:animation="animation"
+				:animation="getItemAnimation(oindex)"
 				:disabled="isDisabled || props.disabled"
 				@touchstart="touchStart" 
 				@change="touchMove" 
@@ -84,7 +84,10 @@
 				/** 旧视图下标 */
 				oldindex: -1,
 				/** 上次原始下标 */
-				lastindex: -1
+				lastindex: -1,
+				/** 当前拖拽坐标(用于小程序 touchend 防回闪) */
+				x: 0,
+				y: 0
 			})
 			
 			const ghostEl = reactive({
@@ -222,13 +225,15 @@
 				active.value = oindex 
 				// 选中项的当前下标
 				dragEl.index = dragEl.oldindex = target.index
+				dragEl.x = target.x || 0
+				dragEl.y = target.y || 0
 				ghostEl.x = target.x||0
 				ghostEl.y = target.y||0
 				dragEl.content = ghostEl.content = target.content
 			}
 			
 			const touchEnd = (e: any) => {
-				setTimeout(() => {
+				const finalize = () => {
 					if(e.target.dataset.remove || active.value==-1) return
 					setDisabled(e, true)
 					isDrag.value = false
@@ -246,8 +251,20 @@
 							isEmit && emitting()
 						})
 					})
-				},80)
-				
+				}
+				// 小程序下 movable-view 在 touchend 会先回到绑定的 x/y,
+				// 先把当前拖拽坐标写回,避免“先回原位”的闪现,同时保留动画
+				// #ifdef MP-WEIXIN
+				const last = cloneList.value[active.value]
+				if (last) {
+					last.x = dragEl.x
+					last.y = dragEl.y
+				}
+				finalize()
+				// #endif
+				// #ifndef MP-WEIXIN
+				setTimeout(finalize, 80)
+				// #endif
 			}
 			const emitting = () => {
 				const clone = [...cloneList.value].sort((a, b) => a.index - b.index)//.map(item => ref(item.content))
@@ -264,6 +281,16 @@
 				// #endif
 				if(oindex != active.value) return
 				const {x, y} = e.detail
+				// 小程序下同步更新绑定坐标,避免 touchend 回弹闪现
+				// #ifdef MP-WEIXIN
+				const activeItem = cloneList.value[active.value]
+				if (activeItem) {
+					activeItem.x = x
+					activeItem.y = y
+				}
+				// #endif
+				dragEl.x = x
+				dragEl.y = y
 				const centerX = x + girdWidth.value / 2
 				const centerY = y + gridHeight.value / 2
 				for (let i = 0; i < cloneList.value.length; i++) {
@@ -303,7 +330,9 @@
 				// 距离
 				let distance = start - toIndex
 				// 找到区间所有的项
-				while(distance) {
+				let steps = 0
+				const stepLimit = isDrag.value ? 1 : Number.POSITIVE_INFINITY
+				while(distance && steps < stepLimit) {
 					distance += speed
 					// 目标
 					const target = isDrag.value ? (dragEl.index += speed)  : (start += speed) 
@@ -321,7 +350,7 @@
 					targetEl.oldindex = targetEl.index
 					targetEl.index = lastIndex
 					activeEl.oldindex = activeEl.index //oIndex
-					activeEl.index = toIndex
+					activeEl.index = target
 					// 到达终点,如果是拖拽则不处理
 					if(!distance && !isDrag.value) {
 						const rect = gridRects[toIndex]
@@ -333,6 +362,7 @@
 							emitting()
 						}
 					}
+					steps++
 				}
 			}
 			/**
@@ -461,6 +491,12 @@
 			onUnmounted(clear)
 			watch(() => props.list, init)
 			
+			const getItemAnimation = (oindex: number) => {
+				// 拖拽中的项不做动画,避免小程序抖动
+				if (isDrag.value && oindex === active.value) return false
+				return animation.value
+			}
+			
 			// #ifdef VUE3
 			expose({
 				remove,
@@ -469,7 +505,8 @@
 				push,
 				unshift,
 				shift,
-				pop
+				pop,
+				refresh: getRect
 			})
 			// #endif
 			return {
@@ -489,6 +526,7 @@
 				
 				active,
 				animation,
+				getItemAnimation,
 				
 				afterEl,
 				ghostEl,

+ 10 - 9
src/utils/uni-tool.ts

@@ -100,15 +100,14 @@ const tool: IeTool = {
   loading: false,
   loadingStartTime: 0,
   minLoadingTime: 500, // 最小显示时间,单位毫秒
-  showToast(title: string = '') {
+  async showToast(title: string = '') {
     // 先立即隐藏,避免上一个 toast存在导致下次的 toast 很快关闭
     uni.hideToast();
-    setTimeout(() => {
-      uni.showToast({
-        title,
-        icon: 'none'
-      });
-    }, 50);
+    await sleep(100);
+    uni.showToast({
+      title,
+      icon: 'none'
+    });
   },
   showSuccess(title: string = '') {
     uni.showToast({
@@ -175,7 +174,7 @@ const tool: IeTool = {
         success: (res) => {
           /* 这里不要动,就是这样的。形成await showConfirm的效果 2026.1.13 */
           /* 如果有必须接收true/false的写法,请使用showModal */
-          if(res.confirm)(resolve(true))
+          if (res.confirm) (resolve(true))
           else reject(false)
         },
         fail: reject
@@ -225,5 +224,7 @@ const tool: IeTool = {
     // #endif
   }
 };
-
+const sleep = (ms: number) => {
+  return new Promise(resolve => setTimeout(resolve, ms));
+}
 export default tool;