Merge remote-tracking branch 'origin/feature/feature-20250331-h5' into feature/feature-20250331-h5

This commit is contained in:
陈昱达
2025-03-27 16:35:48 +08:00
21 changed files with 594 additions and 441 deletions

2
components.d.ts vendored
View File

@@ -31,8 +31,6 @@ declare module 'vue' {
VanCol: typeof import('vant/es')['Col']
VanDivider: typeof import('vant/es')['Divider']
VanField: typeof import('vant/es')['Field']
VanGrid: typeof import('vant/es')['Grid']
VanGridItem: typeof import('vant/es')['GridItem']
VanIcon: typeof import('vant/es')['Icon']
VanList: typeof import('vant/es')['List']
VanNavBar: typeof import('vant/es')['NavBar']

View File

@@ -65,6 +65,7 @@
"unplugin-auto-import": "^0.18.6",
"unplugin-vue-components": "^0.27.5",
"vite": "^6.0.0",
"vitest": "^3.0.9",
"vue-tsc": "^2.0.21"
},
"browserslist": [

View File

@@ -34,7 +34,7 @@ export const useQuestionStore = defineStore('questionStore', () => {
return ![102, 104, 105, 201].includes(questions.value[0]?.question_type);
});
// 作用未知
// 作用未知, 应该是 language
const l = ref({});
// 主题颜色
const themeColor = ref({});

110
src/types/question.d.ts vendored Normal file
View File

@@ -0,0 +1,110 @@
export declare interface OptionConfigType {
type?: number;
price?: number;
title?: string;
gradient?: string;
image_url?: string[];
child_area?: null;
option_type?: number;
instructions?: string[];
binding_goods_id?: string;
limit_right_content?: string;
}
export declare interface questionOptionType {
// 包含 HTML 标签的字符串,例如 "<p>选项1</p>"
option?: string;
is_other?: number;
is_fixed?: number;
is_remove_other?: number;
level?: number;
option_key?: string;
option_index?: string;
option_code?: string;
option_config?: OptionConfigType;
parent_option_index?: number;
children?: null;
}
declare interface IQuestionOption {
type: number;
cite_type: number;
relation_type: number;
relation_out_scope: number[];
relation_last_scope: number;
relation_first_scope: number;
relation_question_index: number;
options?: questionOptionType[];
}
// 答案 config 类型
export declare interface IQuestionConfig {
text_type?: number;
include_mark;
disabled?: string[];
version?: string;
scene?: string | null;
shelf?: string | null;
ware?: string | null;
row_option_groups?: string | null;
cell_option_groups?: string | null;
is_repeat?: number;
allow_repeat_num?: number;
repeat_type?: number;
alert_text?: string;
is_required?: number;
is_change_row_cell?: number;
select_random?: number;
row_random?: number;
cell_random?: number;
is_three_dimensions?: number;
material_sn?: string;
scene_information?: string | null;
simple_scene_information?: string | null;
is_behavior?: number;
is_price_tag?: number;
is_brand?: number;
is_initialize?: number;
is_default_perspective?: number;
is_disable_lines_same?: number;
disable_lines_same?: number;
float_window?: number;
is_disable?: number;
float_window_content?: string;
popup_window?: number;
popup_window_content?: string;
is_show?: string[];
quick_type?: number;
is_limit_right_content?: number;
option_group_random_inside?: string | null;
option_group_random_outside?: string | null;
}
// 答案 question
export declare interface IQuestion<QuestionConfig> {
error: string;
answer?: unknown;
id?: string;
title?: string;
stem?: string;
other?: string;
// options 列表项,第一个是默认
list: questionOptionType[];
question_index?: number;
question_type?: number;
config?: QuestionConfig;
created_at?: string;
created_user_id?: number;
updated_user_id?: number | null;
survey_id?: number;
logic_config?: LogicConfig;
options: questionOptionType[];
associate?: any[];
logics_has?: string | null;
last_option_index?: number;
question_code?: string;
question_value?: string;
question_tag?: string;
planet_id?: string;
permissions?: any | null;
}

33
src/types/questions/completion.d.ts vendored Normal file
View File

@@ -0,0 +1,33 @@
declare interface ICompletionConfig {
decimal_few: number;
float_window: number;
float_window_content: string;
include_mark: number;
is_behavior: number;
is_brand: number;
is_default_perspective: number;
is_initialize: number;
is_price_tag: number;
is_required: number;
is_show: string[];
is_three_dimensions: number;
left_prompt: string;
line_height: number;
line_type: number;
material_sn: string;
max: number | '';
min: number | '';
placeholder: string;
popup_window: number;
popup_window_content: string;
quick_type: number;
right_prompt: string;
scene: string | null;
scene_information: string | null;
shelf: string | null;
simple_scene_information: string | null;
text_type: number;
type_name: string;
version: string;
ware: string | null;
}

View File

@@ -62,86 +62,6 @@ type Option = {
cascade?: any[];
};
/**
* 配置类型
* @property {string[]} disabled - 禁用的选项列表(通常为空数组)
* @property {string} version - 版本号(通常为空字符串)
* @property {string | null} scene - 场景(通常为 null
* @property {string | null} shelf - 货架(通常为 null
* @property {string | null} ware - 商品(通常为 null
* @property {string | null} row_option_groups - 行选项组(通常为 null
* @property {string | null} cell_option_groups - 列选项组(通常为 null
* @property {number} is_repeat - 是否重复0 表示否1 表示是)
* @property {number} allow_repeat_num - 允许重复的数量
* @property {number} repeat_type - 重复类型(通常为 0
* @property {string} alert_text - 提示文本
* @property {number} is_required - 是否必填0 表示否1 表示是)
* @property {number} is_change_row_cell - 是否允许更改行列选项0 表示否1 表示是)
* @property {number} select_random - 是否随机选择行0 表示否1 表示是)
* @property {number} row_random - 是否随机选择行选项0 表示否1 表示是)
* @property {number} cell_random - 是否随机选择列选项0 表示否1 表示是)
* @property {number} is_three_dimensions - 是否为三维矩阵0 表示否1 表示是)
* @property {string} material_sn - 材料编号(通常为空字符串)
* @property {string | null} scene_information - 场景信息(通常为 null
* @property {string | null} simple_scene_information - 简单场景信息(通常为 null
* @property {number} is_behavior - 是否为行为相关0 表示否1 表示是)
* @property {number} is_price_tag - 是否为价格标签0 表示否1 表示是)
* @property {number} is_brand - 是否为品牌相关0 表示否1 表示是)
* @property {number} is_initialize - 是否初始化0 表示否1 表示是)
* @property {number} is_default_perspective - 是否为默认视角0 表示否1 表示是)
* @property {number} is_disable_lines_same - 是否禁用行列相同0 表示否1 表示是)
* @property {number} disable_lines_same - 禁用行列相同的值(通常为 1
* @property {number} float_window - 是否显示悬浮窗0 表示否1 表示是)
* @property {number} is_disable - 是否禁用0 表示否1 表示是)
* @property {string} float_window_content - 悬浮窗内容(通常为空字符串)
* @property {number} popup_window - 是否显示弹窗0 表示否1 表示是)
* @property {string} popup_window_content - 弹窗内容(通常为空字符串)
* @property {string[]} is_show - 是否显示的选项列表(通常为空数组)
* @property {number} quick_type - 快速类型(通常为 0
* @property {number} is_limit_right_content - 是否限制右侧内容0 表示否1 表示是)
* @property {string | null} option_group_random_inside - 行列选项组内随机(通常为 null
* @property {string | null} option_group_random_outside - 行列选项组外随机(通常为 null
*/
type Config = {
disabled?: string[];
version?: string;
scene?: string | null;
shelf?: string | null;
ware?: string | null;
row_option_groups?: string | null;
cell_option_groups?: string | null;
is_repeat?: number;
allow_repeat_num?: number;
repeat_type?: number;
alert_text?: string;
is_required?: number;
is_change_row_cell?: number;
select_random?: number;
row_random?: number;
cell_random?: number;
is_three_dimensions?: number;
material_sn?: string;
scene_information?: string | null;
simple_scene_information?: string | null;
is_behavior?: number;
is_price_tag?: number;
is_brand?: number;
is_initialize?: number;
is_default_perspective?: number;
is_disable_lines_same?: number;
disable_lines_same?: number;
float_window?: number;
is_disable?: number;
float_window_content?: string;
popup_window?: number;
popup_window_content?: string;
is_show?: string[];
quick_type?: number;
is_limit_right_content?: number;
option_group_random_inside?: string | null;
option_group_random_outside?: string | null;
};
/**
* 逻辑配置类型
* @property {string} expect - 期望值(通常为空字符串)

View File

@@ -1,65 +0,0 @@
interface OptionConfigType {
type?: number;
price?: number;
title?: string;
gradient?: string;
image_url?: string[];
child_area?: null;
option_type?: number;
instructions?: string[];
binding_goods_id?: string;
limit_right_content?: string;
}
interface questionOptionType {
// 包含 HTML 标签的字符串,例如 "<p>选项1</p>"
option?: string;
is_other?: number;
is_fixed?: number;
is_remove_other?: number;
level?: number;
option_key?: string;
option_index?: string;
option_code?: string;
option_config?: OptionConfigType;
parent_option_index?: number;
children?: null;
}
type questionsList = {
type: number;
cite_type: number;
relation_type: number;
relation_out_scope: number[];
relation_last_scope: number;
relation_first_scope: number;
relation_question_index: number;
options?: questionOptionType[];
};
type question = {
error: string;
answer?: unknown;
id?: string;
title?: string;
stem?: string;
other?: string;
list: questionsList[];
question_index?: number;
question_type?: number;
config?: Config;
created_at?: string;
created_user_id?: number;
updated_user_id?: number | null;
survey_id?: number;
logic_config?: LogicConfig;
options: questionsList[];
associate?: any[];
logics_has?: string | null;
last_option_index?: number;
question_code?: string;
question_value?: string;
question_tag?: string;
planet_id?: string;
permissions?: any | null;
};

View File

@@ -24,26 +24,22 @@
<!-- <img :src="icon" alt="icon" />-->
</div>
</div>
<div
v-if="styleInfo.logo_status && styleInfo.logo_url"
class="example-logo"
:style="[
{
'justify-content':
styleInfo.logo_site === 1
? 'flex-start'
: styleInfo.logo_site === 2
? 'center'
: 'flex-end'
},
{ 'padding-left': styleInfo.logo_site === 1 ? '20px' : '' },
{ 'padding-right': styleInfo.logo_site === 3 ? '20px' : '' },
{ position: styleInfo.head_img_status ? 'absolute' : '' },
!styleInfo.head_img_status && styleInfo.background_status
? `background-color: ${styleInfo.background_color};background-image: url(${styleInfo.background_url})`
: ''
]"
>
<div v-if="styleInfo.logo_status && styleInfo.logo_url" class="example-logo" :style="[
{
'justify-content':
styleInfo.logo_site === 1
? 'flex-start'
: styleInfo.logo_site === 2
? 'center'
: 'flex-end'
},
{ 'padding-left': styleInfo.logo_site === 1 ? '20px' : '' },
{ 'padding-right': styleInfo.logo_site === 3 ? '20px' : '' },
{ position: styleInfo.head_img_status ? 'absolute' : '' },
!styleInfo.head_img_status && styleInfo.background_status
? `background-color: ${styleInfo.background_color};background-image: url(${styleInfo.background_url})`
: ''
]">
<img class="logo" :src="styleInfo.logo_url" alt="logo" />
</div>
@@ -51,14 +47,8 @@
<!-- eslint-disable-next-line -->
<div class="questions">
<!-- 提前终止和正常完成 -->
<q-last
v-if="page === pages.length + 1"
:code="questionsData?.action?.code"
:action="questionsData?.action"
:survey="questionsData?.survey"
:isAnswer="isAnswer"
:isTemplate="isTemplate"
/>
<q-last v-if="page === pages.length + 1" :code="questionsData?.action?.code" :action="questionsData?.action"
:survey="questionsData?.survey" :isAnswer="isAnswer" :isTemplate="isTemplate" />
<!-- 问卷名和描述 -->
<!-- <q-first v-else-if="page === 0" isMobile :title="questionsData?.survey?.title"
:desc="questionsData?.survey?.introduction" :questions="questionsData?.questions" :isAnswer="isAnswer"
@@ -70,13 +60,8 @@
:questionType="question.question_type" :questionIndex="question.question_index"
:showTitle="styleInfo.is_question_number && true" isMobile :isAnswer="isAnswer"> -->
<div
v-for="question in questions"
v-else
:id="'questionIndex' + question.question_index"
:key="question.question_index"
class="question"
>
<div v-for="question in questions" v-else :id="'questionIndex' + question.question_index"
:key="question.question_index" class="question">
<!-- <q-radio-->
<!-- v-if="question.question_type === 1"-->
<!-- :list="question.list"-->
@@ -92,37 +77,17 @@
<!-- :question="question"-->
<!-- />-->
<!-- 单选题 -->
<preview-choice
v-if="question.question_type === 1"
v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)"
:list="question.list"
:config="question.config"
:hideOptions="question.hideOptions"
:stem="question.stem"
:answerSn="questionsData.answer.sn"
:answerSurveySn="questionsData.answer.survey_sn"
:question="question"
@previous="previous"
@next="next"
@change-answer="onRelation($event, question)"
/>
<preview-choice v-if="question.question_type === 1" v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)" :list="question.list"
:config="question.config" :hideOptions="question.hideOptions" :stem="question.stem"
:answerSn="questionsData.answer.sn" :answerSurveySn="questionsData.answer.survey_sn" :question="question"
@previous="previous" @next="next" @change-answer="onRelation($event, question)" />
<!-- 多选题 -->
<preview-checkbox
v-else-if="question.question_type === 2"
v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)"
:list="question.list"
:config="question.config"
:hideOptions="question.hideOptions"
:stem="question.stem"
:answerSn="questionsData.answer.sn"
:answerSurveySn="questionsData.answer.survey_sn"
:question="question"
@change-answer="onRelation($event, question)"
@previous="previous"
@next="next"
/>
<preview-checkbox v-else-if="question.question_type === 2" v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)" :list="question.list"
:config="question.config" :hideOptions="question.hideOptions" :stem="question.stem"
:answerSn="questionsData.answer.sn" :answerSurveySn="questionsData.answer.survey_sn" :question="question"
@change-answer="onRelation($event, question)" @previous="previous" @next="next" />
<!-- &lt;!&ndash; 级联题 &ndash;&gt;-->
<!-- <q-cascader-->
<!-- v-else-if="question.question_type === 3"-->
@@ -133,43 +98,19 @@
<!-- isMobile-->
<!-- />-->
<!-- 填空题 -->
<preview-completion
v-else-if="question.question_type === 4"
:config="question.config"
:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)"
:stem="question.stem"
:answerSn="questionsData.answer.sn"
:answerSurveySn="questionsData.answer.survey_sn"
:question="question"
@previous="previous"
@next="next"
@change-answer="onRelation($event, question)"
/>
<preview-completion v-else-if="question.question_type === 4" :config="question.config" :answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)" :stem="question.stem"
:answerSn="questionsData.answer.sn" :answerSurveySn="questionsData.answer.survey_sn" :question="question"
@previous="previous" @next="next" @change-answer="onRelation($event, question)" />
<!-- 打分题 -->
<preview-rate
v-else-if="question.question_type === 5"
v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)"
:list="question.list"
:config="question.config"
:question="question"
isMobile
@change-answer="onRelation($event, question)"
/>
<preview-rate v-else-if="question.question_type === 5" v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)" :list="question.list"
:config="question.config" :question="question" isMobile @change-answer="onRelation($event, question)" />
<!-- 图文说明题 -->
<preview-text-with-images
:answerIndex="getQuestionIndex(questionsData.questions, question)"
:stem="question.stem"
v-else-if="question.question_type === 6"
:config="question.config"
@previous="previous"
@next="next"
v-model:answer="question.answer"
:answerSn="questionsData.answer.sn"
:answerSurveySn="questionsData.answer.survey_sn"
:question="question"
/>
<preview-text-with-images :answerIndex="getQuestionIndex(questionsData.questions, question)"
:stem="question.stem" v-else-if="question.question_type === 6" :config="question.config" @previous="previous"
@next="next" v-model:answer="question.answer" :answerSn="questionsData.answer.sn"
:answerSurveySn="questionsData.answer.survey_sn" :question="question" />
<!-- &lt;!&ndash; 日期时间题 &ndash;&gt;-->
<!-- <q-date-->
<!-- v-else-if="question.question_type === 7"-->
@@ -179,45 +120,20 @@
<!-- isMobile-->
<!-- />-->
<!-- 矩阵填空题 -->
<preview-matrix-text
v-else-if="question.question_type === 8"
v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)"
:list="question.list"
:questionIndex="question.question_index"
:config="question.config"
:stem="question.stem"
:question="question"
@previous="previous"
@next="next"
@change-answer="onRelation($event, question)"
/>
<preview-matrix-text v-else-if="question.question_type === 8" v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)" :list="question.list"
:questionIndex="question.question_index" :config="question.config" :stem="question.stem" :question="question"
@previous="previous" @next="next" @change-answer="onRelation($event, question)" />
<!-- 矩阵单选题 -->
<preview-matrix-radio
v-else-if="question.question_type === 9"
v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)"
:list="question.list"
:questionIndex="question.question_index"
:config="question.config"
:stem="question.stem"
:answerSn="questionsData.answer.sn"
:answerSurveySn="questionsData.answer.survey_sn"
:question="question"
@change-answer="onRelation($event, question)"
@previous="previous"
@next="next"
/>
<preview-matrix-radio v-else-if="question.question_type === 9" v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)" :list="question.list"
:questionIndex="question.question_index" :config="question.config" :stem="question.stem"
:answerSn="questionsData.answer.sn" :answerSurveySn="questionsData.answer.survey_sn" :question="question"
@change-answer="onRelation($event, question)" @previous="previous" @next="next" />
<!-- 矩阵多选题 -->
<preview-matrix-checkbox
v-else-if="question.question_type === 10"
v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)"
:list="question.list"
:config="question.config"
:question="question"
@change-answer="onRelation($event, question)"
/>
<preview-matrix-checkbox v-else-if="question.question_type === 10" v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)" :list="question.list"
:config="question.config" :question="question" @change-answer="onRelation($event, question)" />
<!-- &lt;!&ndash; 矩阵打分题 &ndash;&gt;-->
<!-- <matrix-rate-->
<!-- v-else-if="question.question_type === 11"-->
@@ -285,16 +201,10 @@
<!-- isMobile-->
<!-- />-->
<!-- 文件上传题 -->
<preview-file-upload
v-else-if="question.question_type === 18"
v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)"
:config="question.config"
:question="question"
isMobile
:questionIndex="question.question_index"
@change-answer="onRelation($event, question)"
/>
<preview-file-upload v-else-if="question.question_type === 18" v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)" :config="question.config"
:question="question" isMobile :questionIndex="question.question_index"
@change-answer="onRelation($event, question)" />
<!-- &lt;!&ndash; 地理位置题 &ndash;&gt;-->
<!-- <q-map-->
<!-- v-else-if="question.question_type === 19"-->
@@ -323,14 +233,9 @@
<!-- isMobile-->
<!-- />-->
<!-- 签名题 -->
<preview-sign
v-else-if="question.question_type === 22"
:config="question.config"
:question="question"
v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)"
@change-answer="onRelation($event, question)"
/>
<preview-sign v-else-if="question.question_type === 22" :config="question.config" :question="question"
v-model:answer="question.answer" :answerIndex="getQuestionIndex(questionsData.questions, question)"
@change-answer="onRelation($event, question)" />
<!-- &lt;!&ndash; 知情同意书 &ndash;&gt;-->
<!-- <q-consent-->
<!-- v-else-if="question.question_type === 23"-->
@@ -454,26 +359,12 @@
<!-- :question="question"-->
<!-- />-->
<!-- &lt;!&ndash; 高级题型-NPS &ndash;&gt;-->
<preview-n-p-s
v-else-if="question.question_type === 106"
v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)"
:isPreview="isPreview"
:title="question.title"
:stem="question.stem"
:list="question.list"
:config="question.config"
:isAnswer="isAnswer"
:questionIndex="question.question_index"
:label="question.title"
:loading="loading"
:isTemplate="isTemplate"
:showTitle="styleInfo.is_question_number"
:question="question"
@previous="previous"
@next="next"
@change-answer="onRelation($event, question)"
/>
<preview-n-p-s v-else-if="question.question_type === 106" v-model:answer="question.answer"
:answerIndex="getQuestionIndex(questionsData.questions, question)" :isPreview="isPreview"
:title="question.title" :stem="question.stem" :list="question.list" :config="question.config"
:isAnswer="isAnswer" :questionIndex="question.question_index" :label="question.title" :loading="loading"
:isTemplate="isTemplate" :showTitle="styleInfo.is_question_number" :question="question" @previous="previous"
@next="next" @change-answer="onRelation($event, question)" />
</div>
<!-- <LangTranslate v-if="isAnswer && styleInfo.is_yip" translate-key="PoweredByDigitalTechnologyCenterYIP"
@@ -484,33 +375,14 @@
</div>
<!-- 分页 -->
<!-- eslint-disable max-len -->
<pfe-pagination
v-if="isPreview"
class="pagination"
:isPreview="isPreview"
:page="page"
:pages="pages.length + 1"
:min="styleInfo.is_home ? 0 : 1"
:loading="loading"
:showPrevious="styleInfo.is_up_button"
:showStart="styleInfo.is_start_button"
:startText="styleInfo.start_button_text"
:showSubmit="styleInfo.is_submit_button"
:submitText="
localPageTimer.is_show && localPageTimer.short_time
<pfe-pagination v-if="isPreview" class="pagination" :isPreview="isPreview" :page="page" :pages="pages.length + 1"
:min="styleInfo.is_home ? 0 : 1" :loading="loading" :showPrevious="styleInfo.is_up_button"
:showStart="styleInfo.is_start_button" :startText="styleInfo.start_button_text"
:showSubmit="styleInfo.is_submit_button" :submitText="localPageTimer.is_show && localPageTimer.short_time
? `${localPageTimer.short_time}S`
: styleInfo.submit_button_text
"
:buttonTextColor="styleInfo.button_text_color"
:buttonColor="styleInfo.button_color"
:nextText="
localPageTimer.is_show && localPageTimer.short_time ? `${localPageTimer.short_time}S` : ''
"
:nextDisabled="localPageTimer.short_time"
isMobile
@previous="previous"
@next="next"
>
" :buttonTextColor="styleInfo.button_text_color" :buttonColor="styleInfo.button_color" :nextText="localPageTimer.is_show && localPageTimer.short_time ? `${localPageTimer.short_time}S` : ''
" :nextDisabled="localPageTimer.short_time" isMobile @previous="previous" @next="next">
</pfe-pagination>
</div>
<!-- eslint-enable max-len -->
@@ -593,7 +465,7 @@ console.log(`now page is template? ${isTemplate.value}`);
getQuestions();
// 更新数据
async function getQuestions() {
async function getQuestions () {
let { data } = await AnswerApi.getQuetions({
id: route.query.sn,
data: {
@@ -615,7 +487,7 @@ async function getQuestions() {
}
// 上一页
async function previous() {
async function previous () {
if (prevLoading.value || loading.value) {
return;
}
@@ -638,7 +510,7 @@ async function previous() {
}
// 下一页
async function next(callbackBeforePage) {
async function next (callbackBeforePage) {
// console.log(`click next button`, prevLoading.value || loading.value);
// if (prevLoading.value || loading.value) {
// return;
@@ -659,7 +531,7 @@ async function next(callbackBeforePage) {
// 开始答题
// 答题
async function answer(callback, callbackBeforePage) {
async function answer (callback, callbackBeforePage) {
if ((questions.value.length || !questionsData.value.questions.length) && !props.isTemplate) {
// 表单验证(当前页)
const errors = questions.value.filter((question) => {
@@ -865,62 +737,6 @@ async function answer(callback, callbackBeforePage) {
isError = true;
question.error = translatedText.value.PleaseUploadAtLeastOneFiles(config.min_number);
} else if (answer && questionType === 4) {
question.error = '';
// 填空题
const { value } = answer;
const newValue = value.replace(/\n|\r|\r\n/g, '');
switch (config.text_type) {
// 字母
case 3:
// eslint-disable-next-line
const reg =
/^[a-zA-Z·~@#¥%…&*()—\-+={}|《》?:“”【】、;‘’,。`!$^()_<>?:",./;'\\[\]]+$/;
isError =
config.include_mark === 1
? !reg.test(newValue) || !newValue.length
: !/^[a-zA-Z]+$/.test(newValue) || !newValue.length;
question.error = isError ? translatedText.value.PleaseEnterEnglishLetters : '';
break;
// 中文
case 4:
isError =
config.include_mark === 1
? !/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|[a-zA-Z·~@#¥%…&*()—\-+={}|《》?:“”【】、;‘’,。`!$^()_<>?:",./;'\\[\]])+$/.test(
newValue
) || !newValue.length
: !/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])+$/.test(
newValue
) || !newValue.length;
question.error = isError ? translatedText.value.PleaseEnterChineseWords : '';
break;
// 邮箱
case 5:
isError =
!/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
value
);
question.error = isError ? translatedText.value.PleaseEnterACorrectEmail : '';
break;
// 手机号
case 6:
isError = !/^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(value);
question.error = isError ? translatedText.value.PleaseEnterACorrectPhone : '';
break;
// 身份证号
case 7:
isError =
!/^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/.test(
value
);
question.error = isError ? translatedText.value.PleaseEnterACorrectID : '';
break;
default:
break;
}
if (!isError && value.length < config.min && ![1, 2].includes(config.text_type)) {
isError = true;
question.error = translatedText.value.PleaseEnterMoreThanOneCharacters(config.min);
}
} else if (answer && questionType === 8) {
// 矩阵填空题
question.error = '';
@@ -1224,7 +1040,7 @@ async function answer(callback, callbackBeforePage) {
}
// 关联引用
function onRelation(
function onRelation (
// 避免出现参数 undefined 情况
{ options, value, list } = {},
{ question_type: _questionType, question_index: _questionIndex, related, answer } = {}
@@ -1340,7 +1156,7 @@ function onRelation(
});
}
function jumpImmediately() {
function jumpImmediately () {
const code = questionsData.value.action?.code;
if (page.value !== pages.value.length + 1 && ![20004, 20011, 20016].includes(code)) {
return;
@@ -1393,7 +1209,7 @@ function jumpImmediately() {
}
// 更新答案
function updateAnswer(auto) {
function updateAnswer (auto) {
if (auto) {
auto.forEach((autoItem) => {
const question = questionsData.value.questions.find(
@@ -1416,7 +1232,7 @@ function updateAnswer(auto) {
}
}
// 选项隐藏
function hideOptions(hide) {
function hideOptions (hide) {
const questionIndex = hide?.question_index;
if (questionIndex) {
const question = questionsData.value.questions.find(
@@ -1427,7 +1243,7 @@ function hideOptions(hide) {
}
// 要搜索的数组和要查找的连续数字的数量n
function hasNConsecutiveNumbers(arr, n, onTrue, onFalse) {
function hasNConsecutiveNumbers (arr, n, onTrue, onFalse) {
let count = 1;
let warnStart = 0;
let prevNum = arr[0];
@@ -1450,7 +1266,7 @@ function hasNConsecutiveNumbers(arr, n, onTrue, onFalse) {
// eslint-disable
// 跳转链接
function toUrl(url) {
function toUrl (url) {
// 创建一个新的变量来存储修改后的 URL
let modifiedUrl = url;
@@ -1473,7 +1289,7 @@ function toUrl(url) {
* 清空 answer 答案
* @param questions
*/
function clearAnswer(questions) {
function clearAnswer (questions) {
if (!questions) return;
questions.forEach((question) => {
if (!question.answer) return;
@@ -1483,6 +1299,7 @@ function clearAnswer(questions) {
</script>
<style lang="scss" scoped>
@import '@/assets/css/main';
:deep(.van-cell::after) {
display: none;
}
@@ -1490,7 +1307,8 @@ function clearAnswer(questions) {
.preview-icon {
position: relative;
flex: 1;
width: 65px; /* 根据实际图片大小调整 */
width: 65px;
/* 根据实际图片大小调整 */
height: 50px;
margin-right: 40px;
margin-left: 30px;
@@ -1502,8 +1320,10 @@ function clearAnswer(questions) {
position: absolute;
bottom: -70px;
left: -10px;
width: 65px; /* 根据实际图片大小调整 */
height: 140px; /* 根据实际图片大小调整 */
width: 65px;
/* 根据实际图片大小调整 */
height: 140px;
/* 根据实际图片大小调整 */
background: url('@/assets/img/create-right-back.png') no-repeat center center;
background-size: cover;
@@ -1535,6 +1355,7 @@ function clearAnswer(questions) {
}
}
}
.end-text {
position: fixed;
bottom: 65px;
@@ -1544,11 +1365,13 @@ function clearAnswer(questions) {
color: #a5a5ac;
background-color: #f2f2f2;
z-index: 10;
& .el-text {
font-size: 12px;
color: #4b4b59 !important;
}
}
.preview-info {
display: flex;
grid-template-columns: 1fr 80px;

View File

@@ -12,40 +12,47 @@
<script setup lang="ts">
import { computed, defineEmits, ref, watch } from 'vue';
import Completion from '@/views/Design/components/Questions/Completion.vue';
import Rate from '@/views/Design/components/Questions/Rate.vue';
// 预览新增 v-model
// const config = defineModel('config');
const answer = defineModel<{ value: string | number }>('answer', { default: { value: '' } });
import type { IQuestion } from '@/types/question';
import { validateCompletion } from '@/views/Survey/views/Preview/components/questions/validate/previewCompletion';
interface ICompletionAnswer {
value: string;
}
const answer = defineModel<ICompletionAnswer>('answer', { default: {} });
const answerIndex = computed(() => {
return question.value.title;
});
// const stem = defineModel('stem');
const question = defineModel<question>('question', { default: () => {} });
// const list = defineModel<questionsList[]>('list');
// const answerSn = defineModel<string>('answerSn');
// const answerSurveySn = defineModel<answerSurveySn>('answerSurveySn');
// const modelValue = defineModel('modelValue');
// // 预览新增 emit ['changeAnswer', 'previous', 'next']
const question = defineModel<IQuestion<ICompletionConfig>>('question', { default: () => {} });
const emit = defineEmits(['previous', 'next', 'update:modelValue', 'saveOption', 'changeAnswer']);
// console.log(`answer`, answer.value);
// console.log(question.value);
const completionValue = ref(answer.value?.value ?? '');
// console.log(`question:`, question.value);
// console.log(`list: `, list.value);
const completionValue = ref<string>(answer.value?.value ?? '');
// 进行提交答案
watch(
() => completionValue.value,
() => {
const res = {
value: completionValue.value
};
(value) => {
// 答案校验,生成最终答案
const { isError } = validateCompletion(question.value, question.value.config!, String(value));
console.log(`isError, question`, isError, question.value);
if (isError) return;
const res = generateAnswer(value);
answer.value = res;
question.value!.answer = res;
emit('changeAnswer', res);
// answer emit 提交失效
// emit('changeAnswer', res);
}
);
/**
* 生成最终答案
* @param answer {string}
*/
function generateAnswer(answer: string) {
return { value: answer };
}
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,5 @@
interface ICompletionAnswer {
value: string;
}
export { ICompletionAnswer };

View File

@@ -0,0 +1,23 @@
// 语言转换类型
// export interface ITranslatedText {
// [key: string]: string | ((value: number) => string);
// }
import { language } from '@/views/Survey/views/Preview/js/language';
type originLanguage = typeof language;
export type languageArea = 'zh' | 'en';
/**
* 转换之后的 translatedText
* originLanguage[K] 属性 value
* 对应的属性格式为
* ```typescript
* ITranslatedText{
* some: string,
* other: (value: number) => string
* }
* ```
*/
export type ITranslatedText = {
[K in keyof originLanguage]: originLanguage[K][languageArea];
};

View File

@@ -0,0 +1,89 @@
import type { IQuestion } from '@/types/question';
import { getLanguage } from '@/views/Survey/views/Preview/js/language';
import { validateEnglishLetters } from '@/views/Survey/views/Preview/components/questions/validate/validateEnglishLetters';
import { validateChineseLetter } from '@/views/Survey/views/Preview/components/questions/validate/validateChineseLetter';
import { validateEmail } from '@/views/Survey/views/Preview/components/questions/validate/validateEmail';
import { validatePhone } from '@/views/Survey/views/Preview/components/questions/validate/validatePhone';
import { validateIDCard } from '@/views/Survey/views/Preview/components/questions/validate/validateIDCard';
import type { ITranslatedText } from '@/views/Survey/views/Preview/components/questions/validate/validateChineseLetter';
import { validateMinLength,validateMaxLength } from '@/views/Survey/views/Preview/components/questions/validate/validateStringLength';
/**
* 生成对应的语言文字
* 目标语言暂时写死,有需求后续更改
*/
const translatedText = getLanguage(['zh']) as ITranslatedText;
/**
* @description 填空题验证
* @date 2022-11-22
* @param {IQuestion<ICompletionConfig>} question - 问卷
* @param {IQuestionConfig} config - 相关配置
* @param {ICompletionAnswer} answer - 答案
*/
function validateCompletion(
question: IQuestion<ICompletionConfig>,
config: ICompletionConfig,
answer: string
) {
// 是否有错误
let isError = false;
question.error = '';
// 处理空白字符
const newValue = answer.replace(/\n|\r|\r\n/g, '');
// 根据对应的类型进行验证
switch (config.text_type) {
// 字母校验
case 3:
question.error = validateEnglishLetters(config, newValue, translatedText);
break;
// 中文校验
case 4:
question.error = validateChineseLetter(config, newValue, translatedText);
break;
// 邮箱校验
case 5:
question.error = validateEmail(newValue, translatedText);
break;
// 手机号
case 6:
question.error = validatePhone(newValue, translatedText);
break;
// 身份证号
case 7:
question.error = validateIDCard(newValue, translatedText);
break;
default:
break;
}
// 最小输入字数校验
const minStringValidateResult = validateMinLength(answer, isError, config, translatedText);
// 如果内容存在, 则说明有错误,初始化赋值数据
if (minStringValidateResult) {
const { isError: minStringError, errorMessage: minStringErrMessage } = minStringValidateResult;
minStringError && (isError = minStringError);
question.error = minStringErrMessage;
}
// 最大输入字数校验
const maxStringValidateResult = validateMaxLength(answer, isError, config, translatedText);
// 如果内容存在, 则说明有错误,初始化赋值数据
if (maxStringValidateResult) {
const { isError: maxStringError, errorMessage: maxStringErrMessage } = maxStringValidateResult;
maxStringError && (isError = maxStringError);
question.error = maxStringErrMessage;
}
// 返回错误和对应的 question
return {
question,
isError
};
}
export { validateCompletion };

View File

@@ -0,0 +1,32 @@
import type { ICompletionAnswer } from '@/views/Survey/views/Preview/components/questions/types/previewCompletion';
import { getLanguage } from '@/views/Survey/views/Preview/js/language';
export type { ITranslatedText } from '@/views/Survey/views/Preview/components/questions/types/translatedText';
/**
* 校验中文字符
* @param {ICompletionConfig} config - 相关配置
* @param {ICompletionAnswer} answer - 答案
* @param {ITranslatedText} translatedText - 语言转换文本
*/
export function validateChineseLetter(
config: ICompletionConfig,
answer: string,
translatedText = getLanguage(['zh']) as ITranslatedText
) {
let isError: boolean;
// 1. 包含标点 2. 不包含标点
// 如果包含标点,则使用正则校验,如果不包含标点,则使用
// /^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])+$/.test(newValue) || !newValue.length
// 如果包含标点,校验规则
// 如果不包含标点,校验规则
isError =
config.include_mark === 1
? !/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|[a-zA-Z·~@#¥%…&*()—\-+={}|《》?:“”【】、;‘’,。`!$^()_<>?:",./;'\\[\]])+$/.test(
answer
) || !answer.length
: !/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])+$/.test(
answer
) || !answer.length;
return isError ? translatedText.PleaseEnterChineseWords : '';
}

View File

@@ -0,0 +1,21 @@
import type { ICompletionAnswer } from '@/views/Survey/views/Preview/components/questions/types/previewCompletion';
import { getLanguage } from '@/views/Survey/views/Preview/js/language';
export type { ITranslatedText } from '@/views/Survey/views/Preview/components/questions/types/translatedText';
/**
* 验证邮箱格式
* @param answer {ICompletionAnswer} 答案
* @param translatedText {ITranslatedText} 语言转换文本
*/
export function validateEmail(
answer: string,
translatedText = getLanguage(['zh']) as ITranslatedText
) {
let isError: boolean;
isError =
!/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
answer
);
return isError ? translatedText.PleaseEnterACorrectEmail : '';
}

View File

@@ -0,0 +1,28 @@
import { getLanguage } from '@/views/Survey/views/Preview/js/language';
import type { ITranslatedText } from '@/views/Survey/views/Preview/components/questions/types/translatedText';
/**
* 验证字母
* @param {ICompletionConfig} config - 相关配置
* @param {ICompletionAnswer} answer - 答案
* @param {ITranslatedText} translatedText - 语言转换文本
*/
export function validateEnglishLetters(
config: ICompletionConfig,
answer: string,
translatedText = getLanguage(['zh']) as ITranslatedText
) {
let isError: boolean;
// 验证规则
const reg = /^[a-zA-Z·~@#¥%…&*()—\-+={}|《》?:“”【】、;‘’,。`!$^()_<>?:",./;'\\[\]]+$/;
// 是否包含标点
// 1. 包含标点 2. 不包含标点
// 如果包含标点,则使用正则校验,如果不包含标点,则使用 /^[a-zA-Z]+$/ 校验
isError =
config.include_mark === 1
? !reg.test(answer) || !answer.length
: !/^[a-zA-Z]+$/.test(answer) || !answer.length;
return isError ? translatedText.PleaseEnterEnglishLetters : '';
}

View File

@@ -0,0 +1,22 @@
import type { ICompletionAnswer } from '@/views/Survey/views/Preview/components/questions/types/previewCompletion';
import { getLanguage } from '@/views/Survey/views/Preview/js/language';
import type { ITranslatedText } from '@/views/Survey/views/Preview/components/questions/types/translatedText';
/**
* 验证手机号
* @param answer {ICompletionAnswer} 答案
* @param translatedText {ITranslatedText} 语言转换文本
*/
export function validateIDCard(
answer: string,
translatedText = getLanguage(['zh']) as ITranslatedText
) {
let isError: boolean;
isError =
!/^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/.test(
answer
);
return isError ? translatedText.PleaseEnterACorrectID : '';
}

View File

@@ -0,0 +1,18 @@
import type { ICompletionAnswer } from '@/views/Survey/views/Preview/components/questions/types/previewCompletion';
import { getLanguage } from '@/views/Survey/views/Preview/js/language';
import type { ITranslatedText } from '@/views/Survey/views/Preview/components/questions/types/translatedText';
/**
* 验证手机号
* @param answer {ICompletionAnswer} 答案
* @param translatedText {ITranslatedText} 语言转换文本
*/
export function validatePhone(
answer: string,
translatedText = getLanguage(['zh']) as ITranslatedText
) {
let isError: boolean;
isError = !/^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(answer);
return isError ? translatedText.PleaseEnterACorrectPhone : '';
}

View File

@@ -0,0 +1,60 @@
import type { ITranslatedText } from '@/views/Survey/views/Preview/components/questions/types/translatedText';
/**
* 验证最小字符串长度
* @param answer {ICompletionAnswer} 答案
* @param isError {boolean} 是否有错误
* @param config {ICompletionConfig} 配置信息
* @param translatedText {ITranslatedText} 语言转换文本
* @return {string} 错误信息
*/
export function validateMinLength(
answer: string,
isError: boolean,
config: ICompletionConfig,
translatedText: ITranslatedText
) {
// 处理异常数据,因为不给数据的时候,默认是空字符串
if (typeof config.min === 'string' && config.min.length === 0) return;
// 如果已经包含报错,不处理
if (isError) return;
// 如果包含相应的 text_type 也不处理1是整数2是小数
if ([1, 2].includes(config.text_type)) return;
if (answer.length > Number(config.min)) return;
return {
isError: true,
errorMessage: translatedText.PleaseEnterMoreCharacters(config.min)
};
}
/**
* 验证最大字符串长度
* @param answer {ICompletionAnswer} 答案
* @param isError {boolean} 是否有错误
* @param config {ICompletionConfig} 配置信息
* @param translatedText {ITranslatedText} 语言转换文本
* @return {string} 错误信息
*
*/
export function validateMaxLength(
answer: string,
isError: boolean,
config: ICompletionConfig,
translatedText: ITranslatedText
) {
// 处理异常数据,因为不给数据的时候,默认是空字符串
if (typeof config.max === 'string' && config.max.length === 0) return;
// 如果已经包含报错,不处理
if (isError) return;
// 如果包含相应的 text_type 也不处理, 1是整数2是小数
if ([1, 2].includes(config.text_type)) return;
if (answer.length < Number(config.max)) return;
return {
isError: true,
errorMessage: translatedText.PleaseEnterLessCharacters(config.max)
};
}

View File

@@ -71,7 +71,11 @@ export const language = {
en: 'Please enter a valid ID card number.',
zh: '请输入正确的身份证号。'
},
PleaseEnterMoreThanOneCharacters: {
PleaseEnterLessCharacters: {
en: (count) => `Please enter less than ${count} character${count > 1 ? 's' : ''}.`,
zh: (count) => `请输入小于${count}个字符。`
},
PleaseEnterMoreCharacters: {
en: (count) => `Please enter more than ${count} character${count > 1 ? 's' : ''}.`,
zh: (count) => `请输入大于${count}个字符。`
},
@@ -543,7 +547,24 @@ export function setLanguageTypes(types) {
languageTypes.push(...types);
}
/**
* @typedef {import('@/views/Survey/views/Preview/components/questions/types/translatedText').languageArea} LanguageArea
* @typedef {import('@/views/Survey/views/Preview/components/questions/types/translatedText').ITranslatedText} ITranslatedText I
*/
/**
*
* @alias
* @param langArr { languageArea[] } 语言区域类型
* @returns {ITranslatedText} 获取到的文本
*/
export function getLanguage(langArr = languageTypes) {
/**
* langArr: 语言数组 default: ['zh']
* 1. langArr 为空时,使用 ['zh']
* 2. langArr 是数组时,使用传入的语言
* 3. langArr 是字符串时,使用该字符串语言
*/
const l = [];
if (!langArr) {
l.push('zh');

View File

@@ -0,0 +1,7 @@
import { test, spec } from "vitest";
import { getLanguage } from "../language";
test("检测 language 列表", () => {
const res = getLanguage(['zh'])
console.log(res);
});

View File

@@ -1,4 +1,4 @@
// vite.config.ts
/// <reference types="vitest" />
import { defineConfig, loadEnv } from 'vite'; // 从 vite 导入 loadEnv
import vue from '@vitejs/plugin-vue';
import { fileURLToPath, URL } from 'node:url';