feat: 电子药箱小程序 - 4Tab药品管理
- 电子药箱: 药品库存管理 + 手动录入 + 今日用药打卡 - 药品百科: 搜索 + 分类筛选 + 20种药品静态数据 - 惠教中心: 4条药品使用指南课程 - 我的: 家庭成员信息管理 - 自定义TabBar + navigation-bar组件 - SVG药品分类插图
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/* 容器样式 */
|
||||
.quiz-container {
|
||||
padding: 40rpx 30rpx 230rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
/* 题目卡片 */
|
||||
.question-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 题目标题 */
|
||||
.question-title {
|
||||
margin-bottom: 30rpx;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline;
|
||||
font-size: 28rpx;
|
||||
margin-right: 16rpx;
|
||||
border-radius: 6rpx;
|
||||
font-size: 36rpx;
|
||||
color: #04befe
|
||||
}
|
||||
|
||||
.question-text {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
/* 选项区域 */
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
/* 选项项 */
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #f8f8f8;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.option-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 选项文本 */
|
||||
.option-item text {
|
||||
margin-left: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
/* 正确答案样式 */
|
||||
.option-item.correct {
|
||||
background: #f6ffed;
|
||||
border: 2rpx solid #b7eb8f;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.option-item.correct text {
|
||||
color: #52c41a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 错误答案样式 */
|
||||
.option-item.wrong {
|
||||
background: #fff2f0;
|
||||
border: 2rpx solid #ffccc7;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.option-item.wrong text {
|
||||
color: #ff4d4f;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 选中状态样式 - 使用微信小程序兼容的方式 */
|
||||
.option-item radio:checked + text,
|
||||
.option-item checkbox:checked + text {
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 由于微信小程序不支持:has()选择器,我们通过JS动态添加class来实现选中状态样式 */
|
||||
.option-item.selected {
|
||||
background: #e6f7ff;
|
||||
// border: 2rpx solid #91d5ff;
|
||||
}
|
||||
|
||||
.option-item.selected text {
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// 定义接口类型
|
||||
interface Question {
|
||||
id: number;
|
||||
type: number;
|
||||
question: string;
|
||||
option: Record<string, any>;
|
||||
}
|
||||
|
||||
interface AnswerItem {
|
||||
queId: string;
|
||||
ans: string[];
|
||||
suc?: boolean;
|
||||
}
|
||||
|
||||
interface QuizComponent {
|
||||
properties: {
|
||||
questions: Question[];
|
||||
answerList: AnswerItem[];
|
||||
};
|
||||
data: {
|
||||
selectedAnswers: Record<number, string[]>;
|
||||
renderQuestions: Array<
|
||||
Question & { options: Array<{ key: string; value: string }> }
|
||||
>;
|
||||
};
|
||||
setData: (data: Partial<Record<string, any>>, callback?: () => void) => void;
|
||||
triggerEvent: (name: string, data?: any) => void;
|
||||
isSelected: (qid: string | number, key: string) => boolean;
|
||||
getSelectedAnswer: (qid: string | number, index: number) => string;
|
||||
isCorrect: (qid: string, key: string) => boolean;
|
||||
isWrong: (qid: string, key: string) => boolean;
|
||||
handleRadioChange: (e: WechatMiniprogram.RadioGroupChange) => void;
|
||||
handleCheckboxChange: (e: WechatMiniprogram.CheckboxGroupChange) => void;
|
||||
getFormattedAnswers: () => Array<{
|
||||
questionId: number;
|
||||
answers: string[];
|
||||
questionType: number;
|
||||
}>;
|
||||
getAllAnswers: () => Array<{
|
||||
questionId: number;
|
||||
answers: string[];
|
||||
questionType: number;
|
||||
}>;
|
||||
isAllAnswered: () => boolean;
|
||||
getProgress: () => { answered: number; total: number; percentage: number };
|
||||
}
|
||||
|
||||
// 定义常量
|
||||
const QUESTION_TYPE = {
|
||||
SINGLE: 1,
|
||||
MULTIPLE: 2,
|
||||
};
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
questions: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
answerList: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
},
|
||||
|
||||
observers: {
|
||||
"questions, answerList": function (this: QuizComponent) {
|
||||
const renderQuestions = (this.properties.questions || []).map(
|
||||
(q: Question) => ({
|
||||
...q,
|
||||
options: Object.keys(q.option || {}).map((k) => ({
|
||||
key: k,
|
||||
value: q.option[k],
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
const answerList = Array.isArray(this.properties.answerList)
|
||||
? this.properties.answerList
|
||||
: [];
|
||||
const selectedAnswers = answerList.reduce((acc, item) => {
|
||||
if (item && item.queId && item.ans) {
|
||||
acc[Number(item.queId)] = Array.isArray(item.ans)
|
||||
? item.ans
|
||||
: [item.ans];
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<number, string[]>);
|
||||
|
||||
this.setData({
|
||||
renderQuestions,
|
||||
selectedAnswers,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
selectedAnswers: {} as Record<number, string[]>,
|
||||
renderQuestions: [] as Array<
|
||||
Question & { options: Array<{ key: string; value: string }> }
|
||||
>,
|
||||
},
|
||||
|
||||
methods: {
|
||||
isSelected(this: QuizComponent, qid: string | number, key: string) {
|
||||
const selected = this.data.selectedAnswers[Number(qid)];
|
||||
return Array.isArray(selected) && selected.includes(key);
|
||||
},
|
||||
|
||||
getSelectedAnswer(this: QuizComponent, qid: string | number, index: number) {
|
||||
const selected = this.data.selectedAnswers[Number(qid)];
|
||||
return Array.isArray(selected) ? selected[index] : "";
|
||||
},
|
||||
|
||||
isCorrect(this: QuizComponent, qid: string, key: string) {
|
||||
const answerList = Array.isArray(this.properties.answerList)
|
||||
? this.properties.answerList
|
||||
: [];
|
||||
return answerList.some((item: AnswerItem) => {
|
||||
const answers = Array.isArray(item.ans) ? item.ans : [];
|
||||
return String(item.queId) === String(qid) && answers.includes(key) && item.suc;
|
||||
});
|
||||
},
|
||||
|
||||
isWrong(this: QuizComponent, qid: string, key: string) {
|
||||
const answerList = Array.isArray(this.properties.answerList)
|
||||
? this.properties.answerList
|
||||
: [];
|
||||
return answerList.some((item: AnswerItem) => {
|
||||
const answers = Array.isArray(item.ans) ? item.ans : [];
|
||||
return String(item.queId) === String(qid) && answers.includes(key) && !item.suc;
|
||||
});
|
||||
},
|
||||
|
||||
// 处理单选变化 { queId: q.id, ans: selectedAnswers }
|
||||
handleRadioChange(
|
||||
this: QuizComponent,
|
||||
e: WechatMiniprogram.RadioGroupChange
|
||||
) {
|
||||
const { questionId } = e.currentTarget.dataset;
|
||||
const value = e.detail.value;
|
||||
if (!questionId) return;
|
||||
const numericQuestionId = Number(questionId);
|
||||
const newAnswers = value ? [value] : [];
|
||||
|
||||
this.setData({
|
||||
[`selectedAnswers.${numericQuestionId}`]: newAnswers,
|
||||
});
|
||||
this.triggerEvent("change", {
|
||||
queId: numericQuestionId,
|
||||
ans: newAnswers,
|
||||
});
|
||||
},
|
||||
|
||||
// 处理多选变化
|
||||
handleCheckboxChange(
|
||||
this: QuizComponent,
|
||||
e: WechatMiniprogram.CheckboxGroupChange
|
||||
) {
|
||||
const { questionId } = e.currentTarget.dataset;
|
||||
const values = Array.isArray(e.detail.value) ? e.detail.value : [];
|
||||
if (!questionId) return;
|
||||
const numericQuestionId = Number(questionId);
|
||||
this.setData({
|
||||
[`selectedAnswers.${numericQuestionId}`]: values,
|
||||
});
|
||||
|
||||
this.triggerEvent("change", {
|
||||
queId: numericQuestionId,
|
||||
ans: values,
|
||||
});
|
||||
},
|
||||
|
||||
getFormattedAnswers(this: QuizComponent) {
|
||||
const answers = [];
|
||||
for (const [questionId, selectedOptions] of Object.entries(
|
||||
this.data.selectedAnswers
|
||||
)) {
|
||||
if (selectedOptions && selectedOptions.length > 0) {
|
||||
answers.push({
|
||||
queId: Number(questionId),
|
||||
ans: selectedOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
return answers;
|
||||
},
|
||||
|
||||
getAllAnswers(this: QuizComponent) {
|
||||
return this.getFormattedAnswers();
|
||||
},
|
||||
|
||||
isAllAnswered(this: QuizComponent) {
|
||||
const totalQuestions = this.properties.questions.length;
|
||||
const answeredQuestions = Object.values(this.data.selectedAnswers).filter(
|
||||
(answers) => answers && answers.length > 0
|
||||
).length;
|
||||
return totalQuestions === answeredQuestions;
|
||||
},
|
||||
|
||||
getProgress(this: QuizComponent) {
|
||||
const totalQuestions = this.data.renderQuestions.length;
|
||||
const answeredQuestions = Object.values(this.data.selectedAnswers).filter(
|
||||
(answers) => answers && answers.length > 0
|
||||
).length;
|
||||
return {
|
||||
answered: answeredQuestions,
|
||||
total: totalQuestions,
|
||||
percentage:
|
||||
totalQuestions > 0
|
||||
? Math.round((answeredQuestions / totalQuestions) * 100)
|
||||
: 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
<!--components/my-quiz/my-quiz.wxml-->
|
||||
<view class="quiz-container">
|
||||
<block wx:for="{{renderQuestions}}" wx:key="id" wx:for-item="q" wx:for-index="qi">
|
||||
<view class="question-card">
|
||||
<view class="question-title">
|
||||
<text class="tag">{{qi + 1}}.</text>
|
||||
<text class="tag" wx:if="{{q.type == 2}}">[ 多选题 ]</text>
|
||||
<text class="tag" wx:else>[ 单选题 ]</text>
|
||||
{{q.question}}
|
||||
</view>
|
||||
<view class="options">
|
||||
<!-- 单选题使用 radio-group -->
|
||||
<radio-group
|
||||
wx:if="{{q.type == 1}}"
|
||||
bindchange="handleRadioChange"
|
||||
data-question-id="{{q.id}}"
|
||||
value="{{getSelectedAnswer(q.id, 0)}}"
|
||||
>
|
||||
<block wx:for="{{q.options}}" wx:key="key" wx:for-item="op" wx:if="{{op.value}}">
|
||||
<label class="option-item {{isCorrect(q.id, op.key) ? 'correct' : isWrong(q.id, op.key) ? 'wrong' : ''}} {{getSelectedAnswer(q.id, 0) === op.key ? 'selected' : ''}}">
|
||||
<radio
|
||||
value="{{op.key}}"
|
||||
checked="{{getSelectedAnswer(q.id, 0) === op.key}}"
|
||||
/>
|
||||
<text>{{op.key}}. {{op.value}}</text>
|
||||
</label>
|
||||
</block>
|
||||
</radio-group>
|
||||
|
||||
<!-- 多选题使用 checkbox-group -->
|
||||
<checkbox-group
|
||||
wx:if="{{q.type == 2}}"
|
||||
bindchange="handleCheckboxChange"
|
||||
data-question-id="{{q.id}}"
|
||||
value="{{selectedAnswers[q.id] || []}}"
|
||||
>
|
||||
<block wx:for="{{q.options}}" wx:key="key" wx:for-item="op" wx:if="{{op.value}}">
|
||||
<label class="option-item {{isCorrect(q.id, op.key) ? 'correct' : isWrong(q.id, op.key) ? 'wrong' : ''}} {{isSelected(q.id, op.key) ? 'selected' : ''}}">
|
||||
<checkbox
|
||||
value="{{op.key}}"
|
||||
checked="{{isSelected(q.id, op.key)}}"
|
||||
/>
|
||||
<text>{{op.key}}. {{op.value}}</text>
|
||||
</label>
|
||||
</block>
|
||||
</checkbox-group>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
Reference in New Issue
Block a user