Skip to content

自定义组件

为了提升开发效率和界面复用性,我们将常用的界面元素抽取并封装成了组件。以下是这些组件的详细说明。

弹出层

针对答题卡功能,我们开发了一个通用组件。该组件采用从屏幕底部滑出的展现方式,自定义插槽区域可用于展示题目编号,并通过颜色区分已答、未答、已标记。

示例

js
<xm-popup ref="answerSheet" name="答题卡" class="answer-sheet">
    <view class="answer-sheet-head">
        <view class="answer-sheet-head__state answer-sheet-head__state--mark"></view>
        <text class="answer-sheet-head__name">标记</text>
        <view class="answer-sheet-head__state answer-sheet-head__state--answer"></view>
        <text class="answer-sheet-head__name">已答</text>
    </view>
    <view class="answer-sheet-main">
        <template v-for="(examQuestion, index) in examQuestions" :index="index" :key="index">
            <text v-if="examQuestion.type === 1" @click="curQuestionIndex = index" class="answer-sheet_chapter-name">{{ examQuestion.chapterName }}</text>
            <view
                v-else
                @click="curQuestionIndex = index"
                :class="[
                    'answer-sheet__question-no',
                    { 'answer-sheet__question-no--answer': isAnswer(examQuestion) },
                    { 'answer-sheet__question-no--mark': isMark(examQuestion) }
                ]"
            >
                <text>{{ examQuestion.no }}</text>
            </view>
        </template>
    </view>
</xm-popup>
js
<template>
	<uni-popup ref="popup" type="share" safeArea background-color="#fff" border-radius="10px 10px 10px 10px" class="xm-popup">
		<view class="xm-popup-head">
			<text class="xm-popup-head__name">{{ name }}</text>
			<uni-icons customPrefix="iconfont" type="icon-guanbi" @click="popup.close()" color="#231815" size="34rpx" class="xm-popup-head__close-btn"></uni-icons>
		</view>
		<view class="xm-popup-main">
			<scroll-view scroll-y="true" class="xm-popup-main__scroll">
				<slot></slot>
			</scroll-view>
		</view>
	</uni-popup>
</template>
<script lang="ts" setup>
// 打开
function open() {
	popup.value.open();
}
</script>

属性

属性类型默认值说明必填
namestring’‘头部名称,如:答题卡

倒计时

参考PC端倒计时

空白页

当列表无内容时,展示骨架屏作为占位。

示例

js
<xm-empty v-if="!todoExerList?.length"></xm-empty>
js
<template>
	<view class="xm-empty">
		<image src="@/static/img/list-blank.png" class="xm-empty-img"></image>
		<view class="xm-empty-txt">暂无数据</view>
	</view>
</template>

试题

试题是一个核心组件,在用户答题界面、错题预览等多个场景中均有应用。

示例

js
<xm-question
    v-model="examQuestion.userAnswers"
    :type="examQuestion.questionType"
    :markType="examQuestion.markType"
    :title="examQuestion.title"
    :score="examQuestion.score"
    :answers="examQuestion.answers"
    :userScore="examQuestion.userScore"
    :options="examQuestion.options"
    :analysis="examQuestion.analysis"
    :editable="examing"
    :analysisShow="analysisShow"
    @change="(answers: string[]) => answer(examQuestion, answers)"
>
    <template #title-pre>
        <text class="mypaper-main__question-cur-no">{{ examQuestion.no }}、</text>
    </template>
    <template #title-post>
        <text>({{ examQuestion.score }}分)</text>
    </template>
</xm-question>
js
<template>
	<view class="question">
		<!-- 标题 -->
		<view class="question-title">
			<slot name="title-pre"></slot>
			<text class="question-title__text">{{ title }}</text>
			<slot name="title-post"></slot>
		</view>
		<!-- 单选题选项 -->
		<radio-group
			v-if="type === 1"
			@change="(e: any) => { userAnswers[0] = e.detail.value; $emit('update:modelValue', userAnswers); $emit('change', userAnswers) }"
			class="question-option__radio-wrap"
		>
			<label
				v-for="(option, index) in options"
				:key="index"
				:class="['question-option', { 'is-checked': isChecked(optionLabs(index)) }, { 'is-succ': isSucc(optionLabs(index)) }, { 'is-err': isErr(optionLabs(index)) }]"
			>
				<radio :value="optionLabs(index)" :checked="isChecked(optionLabs(index))" :disabled="!editable" class="question-option__radio-hover" />
				<view class="question-option__radio">
					<view class="question-option__radio-inner"></view>
				</view>
				<text class="question-option__content">{{ optionLabs(index) }}、{{ option }}</text>
			</label>
		</radio-group>
    </view>
</template>

属性

属性类型默认值说明必填
v-modelstring[][]用户答案
titlestring''题干
optionsstring[][]试题选项
typenumber1试题类型(1:单选;2:多选;3:填空;4:判断;5:问答)
mark-typenumber1阅卷方式(1:客观题;2:主观题;)
answersstring[][]标准答案
scorenumber0分数
user-scorenumbernull用户分数
analysisstringnull解析
editablebooleanfalse可编辑(true:是;false:否)
answer-showbooleanfalse标准答案显示(true:用户答案显示;false:标准答案显示)
analysis-showbooleanfalse解析显示(true:显示;false:不显示)

插槽

参数描述必填
title-pre属性title前面追加内容,如题号。
title-post属性title后面追加内容,如分数。

事件

事件参数说明必填
change(value: string[]) => void答案改变时自动触发change事件

滑动

用户考试时,页面增加了左右滑动翻题效果,在正常题量下,使用swiper组件也会卡顿,影响用户体验。参考官网和市面上解决方案,我们完成了如下插件:

  • 启用无限循环:为swiper组件配置circular属性,使用户滑动至末尾时能自动跳转至开头,实现无缝衔接的滑动效果。
  • 固定展示项:将swiper-item的展示数量固定为3个,包括当前题目及其紧邻的前后两题。这样做能显著减少DOM操作,提高滑动流畅度。
  • 动态数据加载:当用户滑动swiper时,根据滑动方向动态加载并更新这3个swiper-item中的数据内容。这样既能保证数据实时性,又能避免一次性加载过多数据导致卡顿。

示例

js
<xm-swiper v-model="curQuestionIndex" :items="examQuestions">
    <template #default="{ item: examQuestion }">
        <scroll-view scroll-y="true" style="height: 100%">
            {{ examQuestion }}
        </scroll-view>
    </template>
</xm-swiper>
vue
<template>
	<swiper :current="curSwiperIndex" :circular="true" @change="(e: any) => e.detail.source === 'touch' && synIndex(e.detail.current)">
		<swiper-item v-for="(item, index) in swiperItems" :key="index">
			<slot :item="item"></slot>
		</swiper-item>
	</swiper>
</template>
<script lang="ts" setup>

/************************计算属性相关*************************/
const swiperItems = computed(() => {
	let curItem = props.items[curItemIndex.value];
	let itemLen = props.items.length;

	let nextItemIndex = curItemIndex.value >= itemLen - 1 ? 0 : curItemIndex.value + 1;
	let nextItem = props.items[nextItemIndex];

	let preItemIndex = curItemIndex.value <= 0 ? itemLen - 1 : curItemIndex.value - 1;
	let preItem = props.items[preItemIndex];

	let tempItems = [];
	if (curSwiperIndex.value === 0) {
		tempItems.push(curItem);
		tempItems.push(nextItem);
		tempItems.push(preItem);
	} else if (curSwiperIndex.value === 1) {
		tempItems.push(preItem);
		tempItems.push(curItem);
		tempItems.push(nextItem);
	} else if (curSwiperIndex.value === 2) {
		tempItems.push(nextItem);
		tempItems.push(preItem);
		tempItems.push(curItem);
	}
	return tempItems;
});

/************************事件相关*****************************/
/**
 * 同步索引
 * 类似两个齿轮相互带动转动,如:a1b1 a2b2 a3b3 a1b4 a2b1
 *  		  b1
 * 	 a1  	b4  b2
 * 	a3 a2	  b3
 *
 * @param newSwiperIndex 最新滑动索引
 */
function synIndex(newSwiperIndex: number) {
	let itemLen = props.items.length;
	let oldSwiperIndex = curSwiperIndex.value;
	let swipeRight = oldSwiperIndex - newSwiperIndex === -2 || oldSwiperIndex - newSwiperIndex === 1;
	if (swipeRight) {
		curSwiperIndex.value <= 0 ? (curSwiperIndex.value = 2) : curSwiperIndex.value--; // 索引同步右滑
		curItemIndex.value <= 0 ? (curItemIndex.value = itemLen - 1) : curItemIndex.value--;
	} else {
		curSwiperIndex.value >= 2 ? (curSwiperIndex.value = 0) : curSwiperIndex.value++; // 索引同步左滑
		curItemIndex.value >= itemLen - 1 ? (curItemIndex.value = 0) : curItemIndex.value++;
	}

	emit('update:modelValue', curItemIndex.value);
}
</script>

属性

参数类型默认值描述必填
v-modelnumbernull当前选中索引
itemsany[][{}]待滑动列表

插槽

参数描述必填
item属性items当前循环到的项

小猫考试