Selaa lähdekoodia

college detail - tab profile cc

abpcoder 1 päivä sitten
vanhempi
commit
1d88a8755c

+ 13 - 0
src/pagesOther/pages/university/detail/components/college-brochure.vue

@@ -0,0 +1,13 @@
+<template>
+
+</template>
+
+<script>
+export default {
+    name: "college-brochure"
+}
+</script>
+
+<style scoped>
+
+</style>

+ 13 - 0
src/pagesOther/pages/university/detail/components/college-enroll.vue

@@ -0,0 +1,13 @@
+<template>
+
+</template>
+
+<script>
+export default {
+    name: "college-enroll"
+}
+</script>
+
+<style scoped>
+
+</style>

+ 13 - 0
src/pagesOther/pages/university/detail/components/college-exam.vue

@@ -0,0 +1,13 @@
+<template>
+
+</template>
+
+<script>
+export default {
+    name: "college-exam"
+}
+</script>
+
+<style scoped>
+
+</style>

+ 13 - 0
src/pagesOther/pages/university/detail/components/college-plan.vue

@@ -0,0 +1,13 @@
+<template>
+
+</template>
+
+<script>
+export default {
+    name: "college-plan"
+}
+</script>
+
+<style scoped>
+
+</style>

+ 233 - 0
src/pagesOther/pages/university/detail/components/college-profile.vue

@@ -0,0 +1,233 @@
+<template>
+    <view class="p-30">
+        <uv-read-more ref="more" show-height="120" close-text="展开全部" toggle>
+            <uv-parse :content="baseInfo.introduction" content-style="color:#1A1A1A; font-size: 28rpx"
+                      container-style="padding:30rpx; border-radius: 24rpx; background-color: var(--back-light)"/>
+        </uv-read-more>
+        <view class="mt-30 flex justify-between items-center text-28 gap-20">
+            <view class="bg-secondary-light text-secondary" :class="buttonClass" @click="handleWebsite">
+                <uv-icon name="share-fill" color="var(--secondary)"/>
+                <text class="text-secondary">招生官网</text>
+            </view>
+            <view class="bg-primary-100 text-primary" :class="buttonClass" @click="handlePhone">
+                <uv-icon name="phone-fill" color="primary"/>
+                <text class="text-primary">招生电话</text>
+            </view>
+        </view>
+        <view class="mt-30">
+            <view class="text-32 font-bold text-fore-title">开设专业</view>
+            <uv-gap v-if="loading" height="15"/>
+            <uv-skeleton v-if="loading" loading rows="3"/>
+            <view v-for="(g,i) in grouped" :key="i"
+                  class="mt-28 p-28 flex justify-between items-center bg-back-light rounded-lg">
+                <view class="text-28 font-bold text-fore-title truncate">{{ g.root.name }}</view>
+                <view class="text-24 text-fore-title flex items-center" @click="handleProfessionGroup(g)">
+                    <text>{{ g.count }}个专业</text>
+                    <uv-icon name="arrow-right"/>
+                </view>
+            </view>
+        </view>
+        <!--        <uv-action-sheet ref="actionSheet" :title="baseInfo.tel" :actions="actions" safe-area-inset-bottom-->
+        <!--                         close-on-click-overlay cancel-text="取消" @select="handleActionSelect"/>-->
+        <ie-popup ref="popup" :show-toolbar="false">
+            <template v-if="popupGroup">
+                <view class="h-90 flex justify-center items-center text-32 font-bold">{{ popupGroup.root.name }}</view>
+                <scroll-view scroll-y :style="{maxHeight: '50vh', backgroundColor: 'var(--back-light)'}">
+                    <uv-cell-group v-for="(g,i) in popupGroup.subGroups" :key="i" title="1">
+                        <template #title>
+                            <text class="text-24 text-fore-tip">{{g.parent.name}}</text>
+                        </template>
+                        <uv-cell v-for="p in g.list" :title="p.name" custom-class="bg-white"/>
+                    </uv-cell-group>
+                </scroll-view>
+            </template>
+        </ie-popup>
+    </view>
+</template>
+
+<script setup lang="ts">
+import {MAJOR_TREE, UNIVERSITY_DETAIL} from "@/types/injectionSymbols";
+import {University, UniversityDetail, UniversityProfession} from "@/types/university";
+import {MajorItem} from "@/types/major";
+import UvReadMore from "@/uni_modules/uv-read-more/components/uv-read-more/uv-read-more.vue";
+import UvActionSheet from "@/uni_modules/uv-action-sheet/components/uv-action-sheet/uv-action-sheet.vue";
+import IePopup from "@/components/ie-popup/ie-popup.vue";
+
+interface ActionItem {
+    id: string;
+    name: string;
+    icon?: string;
+    color?: string;
+    iconColor?: string;
+    disabled?: boolean;
+}
+
+interface ProfessionSubGroup {
+    parent: MajorItem;
+    list: UniversityProfession[];
+}
+
+interface ProfessionGroup {
+    root: MajorItem;
+    subGroups: ProfessionSubGroup[];
+    count: number;
+}
+
+defineProps({
+    loading: Boolean
+})
+
+const detail = inject(UNIVERSITY_DETAIL) || ref({} as UniversityDetail)
+const majorTree = inject(MAJOR_TREE) || ref([])
+const baseInfo = computed<University>(() => detail.value.baseInfo || {})
+const professions = computed<UniversityProfession[]>(() => detail.value.professions || [])
+const more = ref<InstanceType<typeof UvReadMore>>()
+const actionSheet = ref<InstanceType<typeof UvActionSheet>>()
+const buttonClass = "flex-1 py-16 rounded-lg flex justify-center items-center gap-8"
+
+const actions = computed(() => [{
+    id: 'call',
+    name: '打电话'
+}, {
+    id: 'copy',
+    name: '复制'
+}])
+
+const popup = ref<InstanceType<typeof IePopup>>()
+const popupGroup = ref<ProfessionGroup>()
+const grouped = computed<ProfessionGroup[]>(() => {
+    if (!majorTree.value || !professions.value) return [];
+
+    // 预先构建三级节点到其路径的映射
+    const nodePathCache = new Map<string, { root: MajorItem; parent: MajorItem }>();
+
+    const buildPathCache = (node: MajorItem, root?: MajorItem, parent?: MajorItem) => {
+        if (node.children?.length) {
+            // 非叶子节点
+            const newRoot = root || node;
+            const isRoot = !root;
+            const newParent = isRoot ? undefined : (parent || node);
+
+            node.children.forEach(child => buildPathCache(child, newRoot, newParent));
+        } else {
+            // 叶子节点(三级节点)
+            if (root && parent) {
+                nodePathCache.set(node.code, {root, parent});
+            }
+        }
+    };
+
+    majorTree.value.forEach(node => buildPathCache(node));
+
+    // 分组逻辑
+    const rootMap = new Map<string, {
+        root: MajorItem;
+        parentMap: Map<string, {
+            parent: MajorItem;
+            list: UniversityProfession[];
+        }>;
+        count: number;
+    }>();
+
+    professions.value.forEach(profession => {
+        const path = nodePathCache.get(profession.code);
+        if (!path) return;
+
+        const {root, parent} = path;
+
+        let rootEntry = rootMap.get(root.code);
+        if (!rootEntry) {
+            rootEntry = {
+                root,
+                parentMap: new Map(),
+                count: 0
+            };
+            rootMap.set(root.code, rootEntry);
+        }
+
+        let parentEntry = rootEntry.parentMap.get(parent.code);
+        if (!parentEntry) {
+            parentEntry = {
+                parent,
+                list: []
+            };
+            rootEntry.parentMap.set(parent.code, parentEntry);
+        }
+
+        parentEntry.list.push(profession);
+        rootEntry.count++;
+    });
+
+    // 转换为最终格式
+    return Array.from(rootMap.values())
+        .map(({root, parentMap, count}) => ({
+            root,
+            subGroups: Array.from(parentMap.values()),
+            count
+        }))
+});
+
+const handleWebsite = () => {
+    if (baseInfo.value.webSite) {
+        uni.$ie.openBrowser(baseInfo.value.webSite)
+    } else {
+        uni.$ie.showToast('未提供网址')
+    }
+}
+
+const handlePhone = () => {
+    if (baseInfo.value.tel) {
+        //UvActionSheet 在这里打开有异常,所以先使用粘贴板
+        // actionSheet.value?.open()
+        uni.setClipboardData({
+            data: baseInfo.value.tel,
+            showToast: false,
+            success: () => {
+                uni.showModal({
+                    title: '提示',
+                    content: '号码已复制',
+                    showCancel: false
+                })
+            }
+        })
+    } else {
+        uni.$ie.showToast('未提供电话')
+    }
+}
+
+const handleActionSelect = (e: ActionItem) => {
+    if (e.id == 'call') {
+        uni.makePhoneCall({phoneNumber: baseInfo.value.tel})
+    } else if (e.id == 'copy') {
+        uni.setClipboardData({
+            data: baseInfo.value.tel,
+            success: () => {
+                uni.showModal({
+                    title: '提示',
+                    content: '号码已复制',
+                    showCancel: false
+                })
+            }
+        })
+    }
+}
+
+const handleProfessionGroup = (g: ProfessionGroup) => {
+    popupGroup.value = g
+    popup.value?.open()
+}
+
+watch(() => baseInfo.value.introduction, async (val) => {
+    if (val) {
+        await nextTick()
+        more.value?.init()
+    }
+})
+</script>
+
+<style scoped lang="scss">
+::v-deep .uv-cell-group__title__text {
+    font-size: 12px;
+    color: #999999;
+}
+</style>

+ 56 - 15
src/pagesOther/pages/university/detail/detail.vue

@@ -1,63 +1,104 @@
 <template>
-    <ie-page bg-color="#F6F8FA">
+    <ie-page>
         <ie-navbar :title="prevData.name" transparent bg-color="#FFFFFF" title-color="black" keep-title-color/>
-        <ie-image :src="baseInfo.logo" custom-class="w-full h-360" />
+        <ie-image :src="baseInfo.bannerUrl||baseInfo.logo" custom-class="w-full h-360"/>
         <view class="-mt-60 z-1 rounded-t-3xl p-30 bg-white">
-            <college-info :info="baseInfo" :loading="loading" />
+            <college-info :info="baseInfo" :loading="loading"/>
         </view>
-        <uv-gap height="10"/>
-        <uv-sticky :offset-top="baseStickyTop">
-            <ie-tabs-swiper v-model="current" :list="tabs" :scrollable="false">
-            </ie-tabs-swiper>
+        <uv-gap height="10" bg-color="#F6F8FA"/>
+        <uv-sticky :offset-top="appStore.isH5?0:baseStickyTop">
+            <view :style="{height: tabHeight + 'px'}">
+                <ie-tabs-swiper v-model="current" :list="tabs" :scrollable="false">
+                    <swiper class="swiper h-full" :current="current" @change="handleChangeSwiper">
+                        <swiper-item v-for="(item, index) in tabs" :key="index" class="h-full">
+                            <scroll-view scroll-y :style="{height: (tabHeight-44) + 'px'}">
+                                <college-profile v-if="item.slot==='profile'&&item.visited" :loading="loading"/>
+                                <college-brochure v-if="item.slot==='brochure'&&item.visited"/>
+                                <college-plan v-if="item.slot==='plan'&&item.visited"/>
+                                <college-enroll v-if="item.slot==='enroll'&&item.visited"/>
+                                <college-exam v-if="item.slot==='exam'&&item.visited"/>
+                            </scroll-view>
+                        </swiper-item>
+                    </swiper>
+                </ie-tabs-swiper>
+            </view>
         </uv-sticky>
-        <view style="height: 1000px"></view>
     </ie-page>
 </template>
 <script lang="ts" setup>
 
 import {useTransferPage} from "@/hooks/useTransferPage";
 import {UniversityDetail, University} from "@/types/university";
+import {MajorItem} from "@/types/major";
 import {universityDetail} from "@/api/modules/university";
 import CollegeInfo from "@/pagesOther/pages/university/detail/components/college-info.vue";
 import {SwiperTabItem} from "@/types";
 import {useNavbar} from "@/hooks/useNavbar";
+import {useAppStore} from "@/store/appStore";
+import {MAJOR_TREE, UNIVERSITY_DETAIL} from "@/types/injectionSymbols";
+import CollegeProfile from "@/pagesOther/pages/university/detail/components/college-profile.vue";
+import CollegeBrochure from "@/pagesOther/pages/university/detail/components/college-brochure.vue";
+import CollegePlan from "@/pagesOther/pages/university/detail/components/college-plan.vue";
+import CollegeEnroll from "@/pagesOther/pages/university/detail/components/college-enroll.vue";
+import CollegeExam from "@/pagesOther/pages/university/detail/components/college-exam.vue";
+import {getMajorTree} from "@/api/modules/major";
 
 const {prevData} = useTransferPage()
 const {baseStickyTop} = useNavbar()
 const detail = ref<UniversityDetail>({} as UniversityDetail)
 const baseInfo = computed<University>(() => detail.value.baseInfo || {} as University)
+const majorTree = ref<MajorItem[]>([])
 const loading = ref(true)
+const appStore = useAppStore()
 
 const current = ref(0)
+const tabHeight = computed(() => appStore.sysInfo.screenHeight - baseStickyTop.value)
 const tabs = ref<SwiperTabItem[]>([{
     name: '概况',
-    slot: 'profile'
+    slot: 'profile',
+    visited: true
 }, {
     name: '简章',
-    slot: 'brochure'
+    slot: 'brochure',
+    visited: false
 }, {
     name: '计划',
-    slot: 'plan'
+    slot: 'plan',
+    visited: false
 }, {
     name: '录取',
-    slot: 'enroll'
+    slot: 'enroll',
+    visited: false
 }, {
     name: '考试大纲',
-    slot: 'exam'
+    slot: 'exam',
+    visited: false
 }])
 
+const handleChangeSwiper = function (e: any) {
+    current.value = e.detail.current
+    tabs.value[current.value].visited = true
+}
+
+provide(UNIVERSITY_DETAIL, detail)
+provide(MAJOR_TREE, majorTree)
 onMounted(() => {
     uni.$ie.showLoading()
     loading.value = true
     universityDetail({code: prevData.value.code})
-        .then(res => detail.value = res.data)
+        .then(res => {
+            detail.value = res.data
+            return getMajorTree({})
+        })
+        .then(res => majorTree.value = res.data)
         .finally(() => {
             uni.$ie.hideLoading()
             loading.value = false
         })
 })
 // 必须手动触发才能保证 navbar.transparent 正常工作
-onPageScroll(() => {})
+onPageScroll(() => {
+})
 </script>
 
 <style lang="scss" scoped>

+ 3 - 0
src/static/theme/theme.module.scss

@@ -66,6 +66,9 @@ page, .theme-ie {
   --border: #dadbde;
   --border-light: #e5e6e9;
   //
+  --secondary: #818CF8;
+  --secondary-light: #F4F6FF;
+  //
   --warning: #f9ae3d;
   --warning-dark: #f79824;
   --warning-disabled: #f9d39b;

+ 1 - 0
src/types/index.ts

@@ -130,6 +130,7 @@ export interface SwiperTabItem {
   slot?: string;
   params?: any;
 
+  visited?: boolean;
   component?: any;
 }
 

+ 5 - 2
src/types/injectionSymbols.ts

@@ -1,6 +1,6 @@
 import type {InjectionKey} from 'vue'
 import {StudyPlan, StudyPlanStats} from './study';
-import {Study, Transfer, University, Voluntary} from '.';
+import {Study, Transfer, University, Voluntary, Major} from '.';
 import {useExam} from '@/composables/useExam';
 
 /**
@@ -51,4 +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_SORTING = Symbol('VOLUNTARY_SORTING') 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/major.ts

@@ -149,4 +149,5 @@ export interface University {
     star: string;
     type: string;
     webSite: string;
+    introduction: string;
 }

+ 20 - 2
src/types/university.ts

@@ -1,4 +1,5 @@
 export interface University {
+    bannerUrl: string;
     address: string;
     area: string | number;
     bxLevel: string;
@@ -21,6 +22,23 @@ export interface University {
     webSite: string;
     tier: string;
     tierName: string;
+    introduction: string;
+    tel: string;
+}
+
+export interface UniversityProfession {
+    "remark": string;
+    "id": string | number;
+    "collegeCode": string;
+    "type": string;
+    "code": string;
+    "name": string;
+    "ranking": number | null;
+    "count": number | null;
+    "hot": number | null;
+    "examType": string;
+    "enrollCode": string;
+    "majorDirection": string;
 }
 
 export interface UniversityDetail {
@@ -28,7 +46,7 @@ export interface UniversityDetail {
     enrollBrochures: [];
     enrollHistories: [];
     planHistories: [];
-    professions: [];
+    professions: UniversityProfession[];
 }
 
 export interface UniversityQueryDto {
@@ -43,7 +61,7 @@ export interface UniversityQueryDto {
 
 export interface UniversityTier {
     typeName: string;
-    typeValue: string|number;
+    typeValue: string | number;
     desc: string;
     list: University[];
 

+ 81 - 0
src/utils/uni-tool.ts

@@ -94,6 +94,9 @@ export interface IeTool {
    * @returns 格式化后的时间
    */
   formatTime(time: number | string, format: string): string;
+
+  /* 打开网址 */
+  openBrowser(url: string): void;
 }
 
 const defaultModalOptions: IModalOptions = {
@@ -206,6 +209,84 @@ const tool: IeTool = {
     }
     // @ts-ignore
     return uni.$uv.timeFormat(time, format)
+  },
+  openBrowser(url: string) {
+    // 判断平台
+    // #ifdef H5
+    window.open(url, '_blank')
+    // #endif
+
+    // #ifdef MP-WEIXIN
+    // 微信小程序
+    if (wx.openBrowser) {
+      wx.openBrowser({
+        url: url,
+        success: () => {
+          console.log('打开浏览器成功')
+        },
+        fail: (err: any) => {
+          console.error('打开浏览器失败', err)
+          // 降级处理:复制链接,提示用户
+          uni.setClipboardData({
+            data: url,
+            success: () => {
+              uni.showModal({
+                title: '提示',
+                content: '链接已复制,请在浏览器中打开',
+                showCancel: false
+              })
+            }
+          })
+        }
+      })
+    } else {
+      // 如果不支持,使用降级方案
+      uni.setClipboardData({
+        data: url,
+        success: () => {
+          uni.showModal({
+            title: '提示',
+            content: '链接已复制,请在浏览器中打开',
+            showCancel: false
+          })
+        }
+      })
+    }
+    // #endif
+
+    // #ifdef MP-ALIPAY
+    // 支付宝小程序
+    if (my.openBrowser) {
+      my.openBrowser({ url: url })
+    } else {
+      // 降级处理
+      uni.setClipboardData({
+        data: url,
+        success: () => {
+          uni.showModal({
+            title: '提示',
+            content: '链接已复制,请在浏览器中打开',
+            showCancel: false
+          })
+        }
+      })
+    }
+    // #endif
+
+    // 其他平台,如App、快应用等,可以根据需要补充
+    // 对于不支持直接打开浏览器的平台,使用降级方案
+    // #ifndef H5 || MP-WEIXIN || MP-ALIPAY
+    uni.setClipboardData({
+      data: url,
+      success: () => {
+        uni.showModal({
+          title: '提示',
+          content: '链接已复制,请在浏览器中打开',
+          showCancel: false
+        })
+      }
+    })
+    // #endif
   }
 };
 

+ 4 - 0
tailwind.config.js

@@ -79,6 +79,10 @@ module.exports = {
                     disabled: "var(--success-disabled)",
                     light: "var(--success-light)",
                 },
+                secondary: {
+                    DEFAULT: "var(--secondary)",
+                    light: "var(--secondary-light)"
+                },
                 danger: {
                     DEFAULT: "var(--danger)",
                     dark: "var(--danger-dark)",