feat: 测评首页编写

master
xjs 2025-03-21 18:02:37 +08:00
parent e20ecf787a
commit 6f25dee2fa
14 changed files with 1138 additions and 137 deletions

View File

@ -1,4 +1,12 @@
<template> <template>
<!-- TabBar占位块 - 与TabBar高度一致 -->
<view
v-if="showPlaceholder"
class="tabbar-placeholder"
:style="{ height: `${tabbarTotalHeight}px` }"
></view>
<!-- TabBar组件 -->
<view class="custom-tabbar pb-safe"> <view class="custom-tabbar pb-safe">
<view class="tabbar-content"> <view class="tabbar-content">
<view <view
@ -25,12 +33,28 @@
<script setup lang="ts"> <script setup lang="ts">
import { TabesItem } from '@/service/app/types' import { TabesItem } from '@/service/app/types'
import { tabbarList } from '@/hooks/useTabbarList' import { tabbarList } from '@/hooks/useTabbarList'
import { ref, computed, onMounted } from 'vue'
defineProps({ const props = defineProps({
currentPage: { currentPage: {
type: Number, type: Number,
default: 0, default: 0,
}, },
//
showPlaceholder: {
type: Boolean,
default: true,
},
})
//
const safeAreaBottom = ref(0)
// TabBar (TabBar + )
const tabbarTotalHeight = computed(() => {
// 100rpxpx
const tabbarHeight = uni.upx2px(100)
return tabbarHeight + safeAreaBottom.value
}) })
const changeItem = (item: TabesItem) => { const changeItem = (item: TabesItem) => {
@ -41,6 +65,20 @@ const changeItem = (item: TabesItem) => {
onMounted(() => { onMounted(() => {
uni.hideTabBar() uni.hideTabBar()
//
uni.getSystemInfo({
success: (res) => {
if (res.safeAreaInsets) {
safeAreaBottom.value = res.safeAreaInsets.bottom || 0
}
},
})
})
//
defineExpose({
tabbarTotalHeight,
}) })
</script> </script>
@ -106,4 +144,16 @@ onMounted(() => {
position: absolute; position: absolute;
bottom: 5rpx; bottom: 5rpx;
} }
/* 占位块样式 */
.tabbar-placeholder {
width: 100%;
box-sizing: border-box;
}
/* 安全区域适配 */
.pb-safe {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
</style> </style>

217
src/components/tab/Tabs.vue Normal file
View File

@ -0,0 +1,217 @@
<template>
<view class="tab-container">
<!-- tab标题栏 -->
<scroll-view
scroll-x
class="tab-scroll-view"
:scroll-left="scrollLeft"
scroll-with-animation
show-scrollbar="false"
:id="tabScrollId"
>
<view class="tab-items-container">
<view
v-for="(item, index) in tabs"
:key="index"
class="tab-item"
:class="{ active: currentIndex === index }"
@click="handleTabClick(index)"
:id="`tab-item-${index}`"
>
<text class="tab-text">{{ item.title }}</text>
</view>
<!-- 独立的滑块元素 -->
<view
class="tab-line"
:style="{
transform: `translateX(${lineLeft}px)`,
width: `${lineWidth}rpx`,
backgroundColor: props.themeColor,
}"
></view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed, watch, nextTick, onMounted } from 'vue'
// IDTab
const tabScrollId = `tab-scroll-${Date.now()}`
//
const props = defineProps({
// tabs[{title: '1'}, {title: '2'}]
tabs: {
type: Array,
default: () => [],
},
//
modelValue: {
type: Number,
default: 0,
},
//
themeColor: {
type: String,
default: '#3C9CFD',
},
//
lineWidth: {
type: [Number, String],
default: 48,
},
})
//
const emit = defineEmits(['update:modelValue', 'change'])
//
const currentIndex = ref(props.modelValue)
//
const scrollLeft = ref(0)
//
const lineLeft = ref(0)
// props
watch(
() => props.modelValue,
(newVal) => {
if (currentIndex.value !== newVal) {
currentIndex.value = newVal
updateTabPosition()
}
},
)
// currentIndex
watch(
() => currentIndex.value,
(newVal, oldVal) => {
if (newVal !== oldVal) {
updateTabPosition()
//
emit('update:modelValue', newVal)
emit('change', {
index: newVal,
item: props.tabs[newVal] || {},
})
}
},
)
// tabs
watch(
() => props.tabs,
() => {
nextTick(() => {
updateTabPosition()
})
},
{ deep: true },
)
//
onMounted(() => {
nextTick(() => {
updateTabPosition()
})
})
const instance = getCurrentInstance()
// -
const updateTabPosition = () => {
nextTick(() => {
//
const query = uni.createSelectorQuery().in(instance)
//
query.select(`#${tabScrollId}`).boundingClientRect()
query.select(`#tab-item-${currentIndex.value}`).boundingClientRect()
query.exec((res) => {
if (res && res[0] && res[1]) {
const scrollView = res[0]
const currentTab = res[1]
// 1. - 使
const tabCenter = currentTab.left + currentTab.width / 2 - scrollView.left
// 2.
const lineWidthPx = uni.upx2px(Number(props.lineWidth))
lineLeft.value = tabCenter - lineWidthPx / 2
// 3. 使
const offsetLeft = currentTab.left - scrollView.left
scrollLeft.value = offsetLeft - scrollView.width / 2 + currentTab.width / 2
}
})
})
}
//
const handleTabClick = (index) => {
if (currentIndex.value !== index) {
currentIndex.value = index
}
}
</script>
<style scoped>
.tab-container {
width: 100%;
}
.tab-scroll-view {
white-space: nowrap;
width: 100%;
height: 88rpx;
background-color: #ffffff;
border-bottom: 1rpx solid #f5f5f5;
position: relative;
}
.tab-items-container {
display: inline-flex;
height: 100%;
position: relative;
width: 100%;
justify-content: space-around;
}
.tab-item {
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 32rpx;
position: relative;
height: 100%;
}
.tab-text {
font-size: 28rpx;
color: #333333;
transition: all 0.3s;
}
.tab-item.active .tab-text {
font-weight: bold;
color: v-bind('props.themeColor');
}
.tab-line {
position: absolute;
height: 6rpx;
border-radius: 6rpx;
bottom: 0;
left: 0;
/* 平滑过渡效果 */
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
</style>

View File

@ -1,68 +1,219 @@
<template> <template>
<div class="collapse-container"> <view class="uni-collapse">
<div class="collapse-header" @click="toggleCollapse"> <slot />
<slot name="header"></slot> </view>
<div class="collapse-icon" :class="{ 'is-active': isCollapsed }">
<i class="el-icon-arrow-down"></i>
</div>
</div>
<div class="collapse-content" :class="{ 'is-collapsed': isCollapsed }">
<slot></slot>
</div>
</div>
</template> </template>
<script> <script setup>
export default { import { ref, computed, watch, provide, reactive, onMounted, nextTick } from 'vue'
name: 'Collapse',
props: { const props = defineProps({
defaultCollapsed: { value: {
type: Boolean, type: [String, Array],
default: false, default: '',
},
}, },
data() { modelValue: {
return { type: [String, Array],
isCollapsed: this.defaultCollapsed, default: '',
},
accordion: {
//
type: [Boolean, String],
default: true,
},
})
const emit = defineEmits(['change', 'activeItem', 'input', 'update:modelValue'])
//
const childrens = ref([])
const names = ref([])
// 使 vue2 vue3
const dataValue = computed(() => {
// 使modelValueVue3使valueVue2
if (props.modelValue !== '') {
return props.modelValue
}
return props.value
})
//
const emitValue = (val) => {
emit('input', val)
emit('update:modelValue', val)
}
//
const register = (childInstance) => {
console.log('注册子组件:', childInstance.nameSync)
childrens.value.push(childInstance)
}
//
const unregister = (childInstance) => {
const index = childrens.value.findIndex((child) => child.nameSync === childInstance.nameSync)
if (index !== -1) {
childrens.value.splice(index, 1)
}
}
// -
const setOpen = (val) => {
console.log('设置打开状态:', val, childrens.value.length)
//
const isString = typeof val === 'string' || typeof val === 'number'
const isArray = Array.isArray(val)
//
childrens.value.forEach((vm) => {
if (typeof vm.setOpen === 'function') {
vm.setOpen(false) // 使setOpen
} else {
vm.isOpen = false //
}
})
//
childrens.value.forEach((vm) => {
if (isString) {
// -
if (val.toString() === vm.nameSync) {
if (typeof vm.setOpen === 'function') {
vm.setOpen(true)
} else {
vm.isOpen = true
}
}
} else if (isArray) {
// -
val.forEach((v) => {
if (v.toString() === vm.nameSync) {
if (typeof vm.setOpen === 'function') {
vm.setOpen(true)
} else {
vm.isOpen = true
}
}
})
}
})
emitValue(val)
}
//
watch(
() => dataValue.value,
(val) => {
console.log('折叠面板值变化:', val, '手风琴模式:', props.accordion)
if (val !== undefined && val !== null) {
setOpen(val)
} }
}, },
methods: { { immediate: true }, //
toggleCollapse() { )
this.isCollapsed = !this.isCollapsed
}, // -
}, const setAccordion = (self) => {
if (!props.accordion) return
console.log('设置手风琴模式,当前组件:', self?.uid || self?.nameSync)
childrens.value.forEach((vm) => {
//
const isSameComponent =
// self
(self.uid && vm.instance?.uid && self.uid === vm.instance.uid) ||
// selfnameSync
(self.nameSync && self.nameSync === vm.nameSync) ||
// selfvm
self === vm
//
if (!isSameComponent) {
console.log('关闭其他面板:', vm.nameSync)
vm.isOpen = false
}
})
} }
//
const resize = () => {
childrens.value.forEach((vm) => {
// #ifndef APP-NVUE
vm.getCollapseHeight && vm.getCollapseHeight()
// #endif
// #ifdef APP-NVUE
vm.getNvueHwight && vm.getNvueHwight()
// #endif
})
}
//
const onChange = (isOpen, self) => {
let activeItem = ''
if (props.accordion) {
//
activeItem = isOpen ? self.nameSync : ''
} else {
//
const activeItems = []
childrens.value.forEach((vm) => {
if (vm.isOpen) {
activeItems.push(vm.nameSync)
}
})
activeItem = activeItems
}
console.log('折叠面板状态变化:', activeItem)
emit('change', activeItem)
emitValue(activeItem)
}
//
provide('uniCollapseContext', {
childrens,
names,
setAccordion,
onChange,
register,
unregister,
//
getActiveNames: () => {
//
if (props.accordion) {
return typeof dataValue.value === 'string' || typeof dataValue.value === 'number'
? [dataValue.value.toString()]
: []
}
//
return Array.isArray(dataValue.value) ? dataValue.value.map((v) => v.toString()) : []
},
})
//
onMounted(() => {
nextTick(() => {
console.log('折叠面板挂载完成,当前值:', dataValue.value)
if (dataValue.value !== undefined && dataValue.value !== null && dataValue.value !== '') {
setOpen(dataValue.value)
}
})
})
</script> </script>
<style scoped> <style lang="scss">
.collapse-container { .uni-collapse {
display: grid; /* #ifndef APP-NVUE */
grid-template-rows: auto 1fr;
width: 100%; width: 100%;
}
.collapse-header {
display: flex; display: flex;
align-items: center; /* #endif */
justify-content: space-between; /* #ifdef APP-NVUE */
cursor: pointer; flex: 1;
} /* #endif */
flex-direction: column;
.collapse-icon { background-color: #fff;
transition: transform 0.3s ease;
}
.collapse-icon.is-active {
transform: rotate(180deg);
}
.collapse-content {
overflow: hidden;
transition: grid-template-rows 0.3s ease;
grid-template-rows: 1fr;
}
.collapse-content.is-collapsed {
grid-template-rows: 0fr;
} }
</style> </style>

View File

@ -0,0 +1,447 @@
<template>
<view class="uni-collapse-item">
<!-- onClick(!isOpen) -->
<view
@click="onClick(!isOpen)"
class="uni-collapse-item__title"
:class="{
'is-open': isOpen && titleBorder === 'auto',
'uni-collapse-item-border': titleBorder !== 'none',
}"
>
<view class="uni-collapse-item__title-wrap">
<slot name="title" :expanded="isOpen">
<view class="uni-collapse-item__title-box" :class="{ 'is-disabled': disabled }">
<image v-if="thumb" :src="thumb" class="uni-collapse-item__title-img" />
<text class="uni-collapse-item__title-text">{{ title }}</text>
</view>
</slot>
</view>
<view
v-if="showArrow && !$slots.title"
:class="{
'uni-collapse-item__title-arrow-active': isOpen,
'uni-collapse-item--animation': showAnimation === true,
}"
class="uni-collapse-item__title-arrow"
>
<view :color="disabled ? '#ddd' : '#bbb'" class="i-carbon-chevron-down" />
</view>
</view>
<view
class="uni-collapse-item__wrap"
:class="{ 'is--transition': showAnimation }"
:style="{ height: (isOpen ? height : 0) + 'px' }"
>
<view
:id="elId"
ref="collapseHook"
class="uni-collapse-item__wrap-content"
:class="{ open: isheight, 'uni-collapse-item--border': border && isOpen }"
>
<slot></slot>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
inject,
watch,
onMounted,
onUnmounted,
onUpdated,
getCurrentInstance,
nextTick,
} from 'vue'
// #ifdef APP-NVUE
const dom = weex.requireModule('dom')
// #endif
/**
* CollapseItem 折叠面板子组件
* @description 折叠面板子组件
* @property {String} title 标题文字
* @property {String} thumb 标题左侧缩略图
* @property {String} name 唯一标志符
* @property {Boolean} open = [true|false] 是否展开组件
* @property {Boolean} titleBorder = [true|false] 是否显示标题分隔线
* @property {String} border = ['auto'|'show'|'none'] 是否显示分隔线
* @property {Boolean} disabled = [true|false] 是否展开面板
* @property {Boolean} showAnimation = [true|false] 开启动画
* @property {Boolean} showArrow = [true|false] 是否显示右侧箭头
*/
const props = defineProps({
//
title: {
type: String,
default: '',
},
name: {
type: [Number, String],
default: '',
},
//
disabled: {
type: Boolean,
default: false,
},
// #ifdef APP-PLUS
// ,app
showAnimation: {
type: Boolean,
default: false,
},
// #endif
// #ifndef APP-PLUS
//
showAnimation: {
type: Boolean,
default: true,
},
// #endif
//
open: {
type: Boolean,
default: false,
},
//
thumb: {
type: String,
default: '',
},
// 线
titleBorder: {
type: String,
default: 'auto',
},
border: {
type: Boolean,
default: true,
},
showArrow: {
type: Boolean,
default: true,
},
})
// IDbug
const elId = ref(`Uni_${Math.ceil(Math.random() * 10e5).toString(36)}`)
const isOpen = ref(false)
const isheight = ref(null)
const height = ref(0)
const nameSync = ref('0')
const collapseHook = ref(null)
const proxy = getCurrentInstance()
//
const collapseContext = inject('uniCollapseContext', null)
// open
watch(
() => props.open,
(val) => {
isOpen.value = val
onClick(val, 'init')
},
)
//
const init = (type) => {
// #ifndef APP-NVUE
getCollapseHeight(type)
// #endif
// #ifdef APP-NVUE
getNvueHwight(type)
// #endif
}
//
const uninstall = () => {
if (collapseContext) {
collapseContext.unregister({
nameSync: nameSync.value,
})
}
}
// -
const onClick = (isOpenVal, type) => {
if (props.disabled) return
//
const oldIsOpen = isOpen.value
//
isOpen.value = isOpenVal
//
if (isOpen.value && !oldIsOpen && collapseContext) {
//
collapseContext.setAccordion({
nameSync: nameSync.value,
uid: proxy.uid,
instance: proxy,
})
}
// onChange
if (type !== 'init' && collapseContext) {
collapseContext.onChange(isOpen.value, {
nameSync: nameSync.value,
isOpen: isOpen.value,
})
}
}
// APP-NVUE
const getCollapseHeight = (type, index = 0) => {
const views = uni.createSelectorQuery().in(proxy)
views
.select(`#${elId.value}`)
.fields(
{
size: true,
},
(data) => {
// TODO
if (index >= 10) return
if (!data) {
index++
getCollapseHeight(false, index)
return
}
// #ifdef APP-NVUE
height.value = data.height + 1
// #endif
// #ifndef APP-NVUE
height.value = data.height
// #endif
isheight.value = true
if (type) return
onClick(isOpen.value, 'init')
},
)
.exec()
}
// NVUE
const getNvueHwight = (type) => {
// #ifdef APP-NVUE
dom.getComponentRect(collapseHook.value, (option) => {
if (option && option.result && option.size) {
height.value = option.size.height + 1
isheight.value = true
if (type) return
onClick(props.open, 'init')
}
})
// #endif
}
// -
onMounted(() => {
if (!collapseContext) {
console.error('找不到折叠面板父组件上下文!')
return
}
// nameSync
nameSync.value = props.name !== '' ? props.name.toString() : `${collapseContext.childrens.length}`
console.log('折叠项挂载:', nameSync.value)
// -
collapseContext.register({
nameSync: nameSync.value,
isOpen: isOpen.value,
instance: proxy,
uid: proxy.uid,
// 使
setOpen: (val) => {
isOpen.value = val
nextTick(() => init())
},
})
//
init()
//
nextTick(() => {
if (collapseContext.getActiveNames) {
const activeNames = collapseContext.getActiveNames()
console.log('当前活动项:', activeNames, '本项:', nameSync.value)
if (activeNames.includes(nameSync.value.toString())) {
console.log('默认展开:', nameSync.value)
isOpen.value = true
//
init()
}
}
})
})
//
onUpdated(() => {
nextTick(() => {
init(true)
})
})
//
onUnmounted(() => {
uninstall()
})
</script>
<style lang="scss">
.uni-collapse-item {
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
&__title {
/* #ifndef APP-NVUE */
display: flex;
width: 100%;
box-sizing: border-box;
/* #endif */
flex-direction: row;
align-items: center;
transition: border-bottom-color 0.3s;
// transition-property: border-bottom-color;
// transition-duration: 5s;
&-wrap {
width: 100%;
flex: 1;
}
&-box {
padding: 0 15px;
/* #ifndef APP-NVUE */
display: flex;
width: 100%;
box-sizing: border-box;
/* #endif */
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 48px;
line-height: 48px;
background-color: #fff;
color: #303133;
font-size: 13px;
font-weight: 500;
/* #ifdef H5 */
cursor: pointer;
outline: none;
/* #endif */
&.is-disabled {
.uni-collapse-item__title-text {
color: #999;
}
}
}
&.uni-collapse-item-border {
border-bottom: 1px solid #ebeef5;
}
&.is-open {
border-bottom-color: transparent;
}
&-img {
height: 22px;
width: 22px;
margin-right: 10px;
}
&-text {
flex: 1;
font-size: 14px;
/* #ifndef APP-NVUE */
white-space: nowrap;
color: inherit;
/* #endif */
/* #ifdef APP-NVUE */
lines: 1;
/* #endif */
overflow: hidden;
text-overflow: ellipsis;
}
&-arrow {
/* #ifndef APP-NVUE */
display: flex;
box-sizing: border-box;
/* #endif */
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
margin-right: 10px;
transform: rotate(0deg);
&-active {
transform: rotate(-180deg);
}
}
}
&__wrap {
/* #ifndef APP-NVUE */
will-change: height;
box-sizing: border-box;
/* #endif */
background-color: #fff;
overflow: hidden;
position: relative;
height: 0;
&.is--transition {
// transition: all 0.3s;
transition-property: height, border-bottom-width;
transition-duration: 0.3s;
/* #ifndef APP-NVUE */
will-change: height;
/* #endif */
}
&-content {
position: absolute;
font-size: 13px;
color: #303133;
// transition: height 0.3s;
border-bottom-color: transparent;
border-bottom-style: solid;
border-bottom-width: 0;
&.uni-collapse-item--border {
border-bottom-width: 1px;
border-bottom-color: red;
border-bottom-color: #ebeef5;
}
&.open {
position: relative;
}
}
}
&--animation {
transition-property: transform;
transition-duration: 0.3s;
transition-timing-function: ease;
}
}
</style>

View File

@ -70,6 +70,8 @@ const { columns } = useTable()
.table-row .table-cell { .table-row .table-cell {
text-align: center; text-align: center;
padding-left: 10rpx;
padding-right: 10rpx;
} }
.table-row:first-child { .table-row:first-child {

View File

@ -11,14 +11,22 @@
<wd-index-bar sticky class="overflow-y h-0 flex-auto"> <wd-index-bar sticky class="overflow-y h-0 flex-auto">
<view v-for="item in cities" :key="item.letter"> <view v-for="item in cities" :key="item.letter">
<wd-index-anchor :index="item.letter" /> <wd-index-anchor :index="item.letter" />
<wd-cell <!-- <wd-cell
border border
clickable clickable
v-for="city in item.provinces" v-for="city in item.provinces"
:key="city.id" :key="city.id"
:title="city.provincename" :title="city.provincename"
@click="chooseCity(city)" @click="chooseCity(city)"
></wd-cell> ></wd-cell> -->
<view
v-for="city in item.provinces"
:key="city.id"
@click="chooseCity(city)"
class="px-[32rpx] py-[24rpx] province-item"
>
{{ city.provincename }}
</view>
</view> </view>
</wd-index-bar> </wd-index-bar>
</view> </view>
@ -92,4 +100,12 @@ onLoad(() => {
height: calc(100vh - var(--window-top) - constant(safe-area-inset-bottom)); height: calc(100vh - var(--window-top) - constant(safe-area-inset-bottom));
height: calc(100vh - var(--window-top) - env(safe-area-inset-bottom)); height: calc(100vh - var(--window-top) - env(safe-area-inset-bottom));
} }
.province-item {
font-size: 28rpx;
color: #000000;
}
.province-item:not(:last-child) {
border-bottom: 1rpx solid #e5e5e5;
}
</style> </style>

View File

@ -47,17 +47,22 @@
</button> </button>
</view> </view>
<view class="card-swiper mb-[32rpx]"> <view class="card-swiper mb-[32rpx] h-[126rpx]">
<wd-swiper <swiper
:display-multiple-items="universityBaseInfo?.universityResult.imglist ? 3 : 0" class="mx-[32rpx]"
custom-image-class="custom-image" circular
custom-next-image-class="custom-image-prev" :autoplay="true"
custom-prev-image-class="custom-image-prev"
:list="universityBaseInfo?.universityResult.imglist || []"
previousMargin="8rpx"
nextMargin="8rpx"
:indicator="false" :indicator="false"
></wd-swiper> :display-multiple-items="universityBaseInfo?.universityResult.imglist ? 3 : 0"
>
<swiper-item
v-for="item in universityBaseInfo?.universityResult.imglist"
:key="item"
class="flex justify-center"
>
<image :src="item" mode="scaleToFill" class="w-full h-full mx-[4rpx] rounded-[8rpx]" />
</swiper-item>
</swiper>
</view> </view>
<z-tabs <z-tabs
@ -198,41 +203,4 @@ const handleStar = () => {
font-size: 24rpx !important; font-size: 24rpx !important;
} }
:deep(.wd-button__icon) {
margin-right: 8rpx !important;
line-height: 1;
}
.card-swiper {
--wot-swiper-radius: 0;
--wot-swiper-item-padding: 0 8rpx;
--wot-swiper-nav-dot-color: #e7e7e7;
--wot-swiper-nav-dot-active-color: #4d80f0;
:deep(.custom-image) {
height: 126rpx !important;
border-radius: 8rpx 8rpx 8rpx 8rpx;
}
:deep(.custom-image-prev) {
height: 126rpx !important;
}
:deep(.wd-swiper__track) {
height: 126rpx !important;
}
}
:deep(.tabs-wrapper) {
--wot-tabs-nav-fs: 28rpx;
--wot-tabs-nav-height: 80rpx;
flex: 1 1 auto;
}
:deep(.wd-tabs__line) {
bottom: 0 !important;
}
:deep(.wd-tabs__nav--wrap) {
border-bottom: 2rpx solid #f7f7f7;
}
</style> </style>

View File

@ -1,16 +1,12 @@
<template> <template>
<scroll-view class="h-full"> <scroll-view class="h-full">
<view class="font-semibold text-[#303030] text-[28rpx] mb-[32rpx] px-[32rpx]"> <view class="font-semibold text-[#303030] text-[28rpx] px-[32rpx]">
{{ subMajorList.name }} {{ subMajorList.name }}
<text class="text-[22rpx] text-[#bfbfbf] font-normal">({{ subMajorList.count }})</text> <text class="text-[22rpx] text-[#bfbfbf] font-normal">({{ subMajorList.count }})</text>
</view> </view>
<wd-collapse v-model="value" accordion>
<wd-collapse-item <Collapse v-model="collapseValue" accordion type="line">
:name="item.key" <CollapseItem v-for="(item, index) in subMajorList.items" :key="item.key" :name="`${index}`">
custom-class="custom-collapse-item"
v-for="item in subMajorList.items"
:key="item.key"
>
<template #title="{ expanded }"> <template #title="{ expanded }">
<view class="header"> <view class="header">
<text :class="`text-[24rpx] text-[#303030] ${expanded ? 'text-[#1580FF]!' : ''}`"> <text :class="`text-[24rpx] text-[#303030] ${expanded ? 'text-[#1580FF]!' : ''}`">
@ -30,9 +26,9 @@
</view> </view>
</view> </view>
</template> </template>
<view :scroll-x="true" class="flex flex-col gap-[48rpx] h-full"> <view class="flex flex-col gap-[48rpx] h-full mb-[40rpx]">
<view <view
class="text-[24rpx] text-[#303030] ml-[26rpx]" class="text-[24rpx] text-[#303030] ml-[32rpx]"
v-for="childMajor in item.childMajors" v-for="childMajor in item.childMajors"
:key="childMajor.zymc" :key="childMajor.zymc"
@click="navigatorToMajorInfo(childMajor)" @click="navigatorToMajorInfo(childMajor)"
@ -40,12 +36,15 @@
{{ childMajor.zymc }} {{ childMajor.zymc }}
</view> </view>
</view> </view>
</wd-collapse-item> </CollapseItem>
</wd-collapse> </Collapse>
</scroll-view> </scroll-view>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import Collapse from '@/pages-sub/components/collapse/Collapse.vue'
import CollapseItem from '@/pages-sub/components/collapse/CollapseItem.vue'
const props = defineProps({ const props = defineProps({
subMajorList: { subMajorList: {
type: Object as () => { type: Object as () => {
@ -65,17 +64,7 @@ const props = defineProps({
}, },
}) })
const value = ref<string>('') const collapseValue = ref<string>('0')
watch(
() => props.subMajorList,
(newV) => {
if (newV?.items?.length > 0) {
value.value = newV.items[0].key
}
},
{ immediate: true },
)
const navigatorToMajorInfo = (item: unknown) => { const navigatorToMajorInfo = (item: unknown) => {
const _item = item as { specId: string } const _item = item as { specId: string }
@ -94,5 +83,10 @@ const navigatorToMajorInfo = (item: unknown) => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 32rpx;
}
:deep(.uni-collapse-item__wrap-content) {
border-bottom: 0;
} }
</style> </style>

View File

@ -29,7 +29,8 @@
<image <image
src="@/pages-sub/static/images/ucenter/vip-flag.png" src="@/pages-sub/static/images/ucenter/vip-flag.png"
mode="scaleToFill" mode="scaleToFill"
v-if="userStore.userInfo?.estimatedAchievement.isVIP" class="w-[120rpx] h-[28rpx]"
v-if="isVIP"
/> />
<image <image
v-else v-else
@ -67,12 +68,14 @@
</view> </view>
</view> </view>
<button class="submit-button" @click="handleActive"></button> <button class="submit-button" @click="handleActive" :disabled="isVIP">
{{ isVIP ? '已开通' : '立即兑换' }}
</button>
<view <view
class="text-[#999] text-[24rpx] text-center mt-[20rpx] mb-[30rpx] text-center" class="text-[#999] text-[24rpx] text-center mt-[20rpx] mb-[30rpx] text-center"
v-show="userStore.userInfo.estimatedAchievement.isVIP" v-show="isVIP"
> >
到期时间:2025-08-31 到期时间:{{ endTime }}
</view> </view>
</view> </view>
<view class="custom-background h-[780rpx] w-full absolute top-0 left-0 z-[-1]"></view> <view class="custom-background h-[780rpx] w-full absolute top-0 left-0 z-[-1]"></view>
@ -121,7 +124,7 @@
<script setup lang="ts"> <script setup lang="ts">
import Navbar from '@/pages-sub/components/navbar/Navbar.vue' import Navbar from '@/pages-sub/components/navbar/Navbar.vue'
import MessageBox from '@/pages-sub/components/messageBox/MessageBox.vue' import MessageBox from '@/pages-sub/components/messageBox/MessageBox.vue'
import { activeCard } from '@/service/index/api' import { activeCard, getBingInfo } from '@/service/index/api'
import { useUserStore } from '@/store/user' import { useUserStore } from '@/store/user'
@ -130,6 +133,10 @@ const userStore = useUserStore()
const cardNumber = ref('') const cardNumber = ref('')
const cardPassword = ref('') const cardPassword = ref('')
const isVIP = computed(() => {
return userStore.userInfo.estimatedAchievement.isVIP
})
const navigatorBack = () => { const navigatorBack = () => {
uni.navigateBack() uni.navigateBack()
} }
@ -147,6 +154,8 @@ const handleActive = () => {
}).then((res) => { }).then((res) => {
if (res.code === 200) { if (res.code === 200) {
showMessageBox.value = true showMessageBox.value = true
userStore.setIsVIP(true)
userStore.setVipCode(cardNumber.value)
} else { } else {
uni.showToast({ uni.showToast({
title: res.message, title: res.message,
@ -155,6 +164,17 @@ const handleActive = () => {
} }
}) })
} }
const endTime = ref('')
onLoad(() => {
getBingInfo().then((res) => {
if (res.code === 200) {
endTime.value = (res.result as { endTime: string }).endTime
cardNumber.value = (res.result as { cardCode: string }).cardCode
}
})
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -66,7 +66,7 @@
"path": "pages/evaluation/index/index", "path": "pages/evaluation/index/index",
"type": "page", "type": "page",
"style": { "style": {
"navigationBarTitleText": "测评" "navigationBarTitleText": "测评"
}, },
"needLogin": true "needLogin": true
}, },

View File

@ -0,0 +1,63 @@
<template>
<view class="item-wrapper relative mt-[32rpx]">
<view
class="flag text-[22rpx] text-center absolute top-0 right-0"
:class="{ free: item.isFree }"
>
{{ isFree ? '免费' : 'VIP' }}
</view>
<view class="flex items-stretch gap-[30rpx]">
<view class="flex flex-col gap-[29rpx] mt-[16rpx]">
<view class="text-[#333] text-[40rpx] font-semibold">{{ item.name }}</view>
<view class="text-[24rpx] text-[#999]">
{{ item.summary }}
</view>
</view>
<image
:src="item.minImg"
mode="scaleToFill"
class="w-[240rpx] h-[240rpx] min-w-[240rpx] min-h-[240rpx]"
/>
</view>
</view>
</template>
<script lang="ts" setup>
defineProps({
isFree: {
type: Boolean,
default: true,
},
item: {
type: Object,
default: () => {},
},
})
onLoad(() => {})
</script>
<style scoped lang="scss">
.item-wrapper {
background: linear-gradient(270deg, #e5f2fa 0%, #dae8fa 100%);
border-radius: 18rpx;
padding: 32rpx;
.flag {
width: 64rpx;
height: 32rpx;
border-radius: 0rpx 16rpx 0rpx 16rpx;
}
.free {
background: linear-gradient(51deg, #00c3fc 0%, #00acf7 100%);
color: #fff;
}
.vip {
background: linear-gradient(229deg, #fab55e 0%, #ffeec3 100%);
color: #663800;
}
}
</style>

View File

@ -1,17 +1,72 @@
<route lang="json5" type="page"> <route lang="json5" type="page">
{ {
style: { style: {
navigationBarTitleText: '测评', navigationBarTitleText: '测评',
}, },
needLogin: true, needLogin: true,
} }
</route> </route>
<template> <template>
<view class="">测评</view> <view class="flex flex-col h-screen">
<TabBar :current-page="3"></TabBar> <Tabs v-model:modelValue="currentIndex" :tabs="tabs" @change="handleChange"></Tabs>
<swiper class="flex-1" :current="currentIndex">
<swiper-item>
<view class="mx-[32rpx] overflow-auto h-full">
<EvaluationItem v-for="item in academicList" :key="item.id" :item="item"></EvaluationItem>
</view>
</swiper-item>
<swiper-item>
<view class="mx-[32rpx] overflow-auto h-full">
<EvaluationItem v-for="item in healthList" :key="item.id" :item="item"></EvaluationItem>
</view>
</swiper-item>
<swiper-item>
<view class="mx-[32rpx] overflow-auto h-full">
<EvaluationItem v-for="item in learningList" :key="item.id" :item="item"></EvaluationItem>
</view>
</swiper-item>
</swiper>
<TabBar :current-page="3"></TabBar>
</view>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import TabBar from '@/components/bar/TabBar.vue' import TabBar from '@/components/bar/TabBar.vue'
import Tabs from '@/components/tab/Tabs.vue'
import EvaluationItem from '../components/EvaluationItem.vue'
import { getEvaluationList } from '@/service/index/api'
const tabs = [
{
title: '学职定位',
},
{
title: '心理健康',
},
{
title: '学习状态',
},
]
const currentIndex = ref(0)
const handleChange = (e: any) => {
console.log(e)
}
const academicList = ref([])
const healthList = ref([])
const learningList = ref([])
onLoad(() => {
getEvaluationList({ menuid: '340509778657349' }).then((res) => {
academicList.value = res.result as any[]
})
getEvaluationList({ menuid: '339908117110853' }).then((res) => {
healthList.value = res.result as any[]
})
getEvaluationList({ menuid: '340509778686021' }).then((res) => {
learningList.value = res.result as any[]
})
})
</script> </script>

View File

@ -331,6 +331,10 @@ export const activeCard = (params: { cardCode: string; cardPwd: string }) => {
return http.post('/api/Zyvip/bind', params) return http.post('/api/Zyvip/bind', params)
} }
export const getBingInfo = () => {
return http.get('/api/Zyvip/bingInfo')
}
export const getAdmissionTrends = (params: { export const getAdmissionTrends = (params: {
ascription?: string[] | null ascription?: string[] | null
/** /**
@ -362,3 +366,7 @@ export const getAdmissionTrendsByCollege = (params: {
}) => { }) => {
return http.get('/api/admissionstreds/major', params) return http.get('/api/admissionstreds/major', params)
} }
export const getEvaluationList = (params: { menuid: string }) => {
return http.get('/api/busScale/list', params)
}

View File

@ -173,6 +173,14 @@ export const useUserStore = defineStore(
userInfo.value.wishList = [] userInfo.value.wishList = []
} }
const setIsVIP = (val: boolean) => {
userInfo.value.estimatedAchievement.isVIP = val
}
const setVipCode = (val: string) => {
userInfo.value.estimatedAchievement.vipCode = val
}
// 清除用户信息 // 清除用户信息
const clearUserInfo = () => { const clearUserInfo = () => {
userInfo.value = { ...initState } userInfo.value = { ...initState }
@ -205,6 +213,8 @@ export const useUserStore = defineStore(
deleteWishListMajor, deleteWishListMajor,
sortWishMajorList, sortWishMajorList,
sortWishCollegeList, sortWishCollegeList,
setIsVIP,
setVipCode,
} }
}, },
{ {