|
@@ -11,14 +11,92 @@
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
- <div class="content">
|
|
|
- <span class="subtitle">输入对话:</span>
|
|
|
- <el-input v-model="data.dialogue" :autosize="{ minRows: 3 }" type="textarea" placeholder="输入对话" />
|
|
|
+ <div class="content-wrapper">
|
|
|
+ <div class="content">
|
|
|
+ <div v-for="({ role, type, text, file_id }, i) in data.option_list" :key="i" class="option-list">
|
|
|
+ <span
|
|
|
+ class="avatar"
|
|
|
+ :style="{ backgroundColor: data.property.role_list.find((item) => item.mark === role).color }"
|
|
|
+ >
|
|
|
+ {{ data.property.role_list.find((item) => item.mark === role).name }}
|
|
|
+ </span>
|
|
|
+
|
|
|
+ <div v-if="type === 'text'" class="text">{{ text }}</div>
|
|
|
+
|
|
|
+ <div v-else-if="type === 'image'" class="image">
|
|
|
+ <img :src="file_map_list[file_id]" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else-if="type === 'audio'" class="audio">
|
|
|
+ <AudioPlay
|
|
|
+ :file-id="file_id"
|
|
|
+ :show-slider="true"
|
|
|
+ :show-progress="false"
|
|
|
+ :background-color="data.property.role_list.find((item) => item.mark === role).color"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="content-operation">
|
|
|
+ <div class="up-down">
|
|
|
+ <span :style="{ borderBottomColor: i === 0 ? '#c2c2c4' : '#000' }" @click="moveOption('up', i)"></span>
|
|
|
+ <span
|
|
|
+ :style="{ borderTopColor: i < data.option_list.length - 1 ? '#000' : '#c2c2c4' }"
|
|
|
+ @click="moveOption('down', i)"
|
|
|
+ ></span>
|
|
|
+ </div>
|
|
|
+ <SvgIcon icon-class="delete" @click="deleteOption(i)" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="operation">
|
|
|
+ <div class="operation-left">
|
|
|
+ <el-select v-model="curRole" placeholder="请选择">
|
|
|
+ <el-option
|
|
|
+ v-for="(item, i) in data.property.role_list"
|
|
|
+ :key="i"
|
|
|
+ :label="item.name.length === 0 ? ' ' : item.name"
|
|
|
+ :value="item.mark"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ <SoundRecord @saveWav="saveWav" />
|
|
|
+ </div>
|
|
|
+ <div class="operation-right">
|
|
|
+ <el-upload
|
|
|
+ action="no"
|
|
|
+ accept="audio/*"
|
|
|
+ :show-file-list="false"
|
|
|
+ :http-request="handleAudio"
|
|
|
+ :before-upload="handleBeforeAudio"
|
|
|
+ >
|
|
|
+ <SvgIcon icon-class="music" />
|
|
|
+ <span>上传音频</span>
|
|
|
+ </el-upload>
|
|
|
+ <el-upload
|
|
|
+ action="no"
|
|
|
+ accept="image/*"
|
|
|
+ :show-file-list="false"
|
|
|
+ :http-request="handleImage"
|
|
|
+ :before-upload="handleBeforeImage"
|
|
|
+ >
|
|
|
+ <SvgIcon icon-class="picture" />
|
|
|
+ <span>上传图片</span>
|
|
|
+ </el-upload>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-input
|
|
|
+ v-model="textInput"
|
|
|
+ placeholder="输入"
|
|
|
+ type="textarea"
|
|
|
+ resize="none"
|
|
|
+ :autosize="{ minRows: 3 }"
|
|
|
+ @keyup.enter.native="handleText"
|
|
|
+ />
|
|
|
</div>
|
|
|
|
|
|
<el-button @click="identifyText">识别</el-button>
|
|
|
<div v-if="data.answer.answer_list.length > 0" class="correct-answer">
|
|
|
- <div class="subtitle">正确答案</div>
|
|
|
<el-input v-for="(item, i) in data.answer.answer_list" :key="item.mark" v-model="item.value">
|
|
|
<span slot="prefix">{{ i + 1 }}.</span>
|
|
|
</el-input>
|
|
@@ -77,104 +155,208 @@
|
|
|
{{ label }}
|
|
|
</el-radio>
|
|
|
</el-form-item>
|
|
|
+ <el-form-item label="角色数">
|
|
|
+ <el-select v-model="data.property.role_number" placeholder="请选择">
|
|
|
+ <el-option v-for="item in [2, 3, 4, 5]" :key="item" :label="item" :value="item" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item v-for="(item, i) in data.property.role_list" :key="i" :label="`角色 ${i + 1}`" class="role">
|
|
|
+ <el-input v-model="item.name" />
|
|
|
+ <el-color-picker v-model="item.color" />
|
|
|
+ </el-form-item>
|
|
|
</el-form>
|
|
|
</template>
|
|
|
</QuestionBase>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import RichText from '@/components/common/RichText.vue';
|
|
|
import QuestionMixin from '../common/QuestionMixin.js';
|
|
|
+import AudioPlay from '../common/AudioPlay.vue';
|
|
|
+import SoundRecord from '@/components/common/SoundRecord.vue';
|
|
|
|
|
|
import { getRandomNumber } from '@/utils';
|
|
|
-import { dialogueData } from '@/views/exercise_questions/data/dialogue';
|
|
|
+import { fileUpload, GetFileURLMap } from '@/api/app';
|
|
|
+import { getDialogueData, getRole } from '@/views/exercise_questions/data/dialogue';
|
|
|
|
|
|
export default {
|
|
|
name: 'DialogueQuestion',
|
|
|
- components: { RichText },
|
|
|
+ components: {
|
|
|
+ AudioPlay,
|
|
|
+ SoundRecord,
|
|
|
+ },
|
|
|
mixins: [QuestionMixin],
|
|
|
data() {
|
|
|
return {
|
|
|
- data: JSON.parse(JSON.stringify(dialogueData)),
|
|
|
+ curRole: '',
|
|
|
+ textInput: '',
|
|
|
+ file_id_list: [],
|
|
|
+ file_map_list: [],
|
|
|
+ data: getDialogueData(),
|
|
|
};
|
|
|
},
|
|
|
watch: {
|
|
|
- 'data.dialogue': {
|
|
|
+ 'data.property.role_number'(val) {
|
|
|
+ if (this.data.property.role_list.length > val) {
|
|
|
+ this.data.property.role_list = this.data.property.role_list.slice(0, val);
|
|
|
+ } else {
|
|
|
+ for (let i = this.data.property.role_list.length; i < val; i++) {
|
|
|
+ this.data.property.role_list.push(getRole(i));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ 'data.property.role_list': {
|
|
|
+ handler(val) {
|
|
|
+ if (val.find((item) => item.mark === this.curRole) === undefined && val.length > 0) {
|
|
|
+ this.curRole = val[0].mark;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ immediate: true,
|
|
|
+ },
|
|
|
+ 'data.option_list': {
|
|
|
handler(val) {
|
|
|
- if (!val) {
|
|
|
+ let file_id_list = [];
|
|
|
+ val.forEach(({ type, file_id }) => {
|
|
|
+ if (type === 'image' || type === 'audio') {
|
|
|
+ file_id_list.push(file_id);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ // 判断 this.file_id_list 和 file_id_list 两个数组中的内容是否相等,位置可能不同
|
|
|
+ if (
|
|
|
+ this.file_id_list.length === file_id_list.length &&
|
|
|
+ this.file_id_list.every((item) => file_id_list.includes(item))
|
|
|
+ ) {
|
|
|
return;
|
|
|
}
|
|
|
- let direction = 'row';
|
|
|
- let preName = '';
|
|
|
- this.data.option_list = val
|
|
|
- // 用换行分割 data.dialogue
|
|
|
- .split('\n')
|
|
|
- // 过滤掉空行
|
|
|
- .filter((item) => item.trim() !== '')
|
|
|
- .map((item) => {
|
|
|
- // 分割对话中的人物和对话内容
|
|
|
- const match = item.match(/(\S+)[::](.+)/);
|
|
|
- if (match) {
|
|
|
- let name = match[1];
|
|
|
- if (preName.length === 0 || preName !== name) {
|
|
|
- direction = preName.length === 0 ? 'row' : direction === 'row' ? 'row-reverse' : 'row';
|
|
|
- }
|
|
|
- preName = name;
|
|
|
- let content = match[2];
|
|
|
- let reg = /_{3,}/;
|
|
|
- let isFill = reg.test(content);
|
|
|
- let fillList = [];
|
|
|
- if (isFill) {
|
|
|
- content = content.replace(/(_{3,})/g, '###$1###');
|
|
|
- fillList = content
|
|
|
- .split('###')
|
|
|
- .filter((item) => item)
|
|
|
- .map((content) => {
|
|
|
- let isInput = reg.test(content);
|
|
|
- return {
|
|
|
- content: isInput ? '' : content,
|
|
|
- mark: getRandomNumber(),
|
|
|
- type: isInput ? 'input' : 'text',
|
|
|
- };
|
|
|
- });
|
|
|
- }
|
|
|
- return {
|
|
|
- name,
|
|
|
- content: isFill ? fillList : content,
|
|
|
- direction,
|
|
|
- audio_file_id: '',
|
|
|
- mark: isFill ? getRandomNumber() : '',
|
|
|
- type: isFill ? 'input' : 'text',
|
|
|
- };
|
|
|
- }
|
|
|
- })
|
|
|
- .filter((item) => item);
|
|
|
+ this.file_id_list = file_id_list;
|
|
|
+ GetFileURLMap({ file_id_list }).then(({ url_map }) => {
|
|
|
+ this.file_map_list = url_map;
|
|
|
+ });
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
methods: {
|
|
|
+ // 识别
|
|
|
identifyText() {
|
|
|
- this.data.answer.answer_list = this.data.option_list
|
|
|
- .filter(({ type }) => type === 'input')
|
|
|
- .map(({ content }) => {
|
|
|
- return content
|
|
|
- .filter(({ type }) => type === 'input')
|
|
|
- .map(({ content, mark }) => {
|
|
|
- return {
|
|
|
- value: content,
|
|
|
- mark,
|
|
|
- type: 'only_one',
|
|
|
- };
|
|
|
- });
|
|
|
- })
|
|
|
- .flat();
|
|
|
+ let answer_list = [];
|
|
|
+ this.data.option_list.forEach(({ type, content_list }) => {
|
|
|
+ if (type !== 'text') return;
|
|
|
+ content_list.forEach(({ type, mark }) => {
|
|
|
+ if (type !== 'input') return;
|
|
|
+ answer_list.push({
|
|
|
+ mark,
|
|
|
+ value: this.data.answer.answer_list.find((item) => item.mark === mark)?.value || '',
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+ this.data.answer.answer_list = answer_list;
|
|
|
+ },
|
|
|
+ handleBeforeAudio(file) {
|
|
|
+ if (this.curRole.length <= 0) {
|
|
|
+ this.$message.error('请先选择角色');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // 判断文件是否为音频
|
|
|
+ if (!file.type.includes('audio')) {
|
|
|
+ this.$message.error('请选择音频文件');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleBeforeImage(file) {
|
|
|
+ if (this.curRole.length <= 0) {
|
|
|
+ this.$message.error('请先选择角色');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // 判断文件是否为图片
|
|
|
+ if (!file.type.includes('image')) {
|
|
|
+ this.$message.error('请选择图片文件');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleAudio(file) {
|
|
|
+ this.upload('audio', file);
|
|
|
+ },
|
|
|
+ handleImage(file) {
|
|
|
+ this.upload('image', file);
|
|
|
+ },
|
|
|
+ upload(type, file) {
|
|
|
+ fileUpload('Mid', file, { isGlobalprogress: true }).then(({ file_info_list }) => {
|
|
|
+ if (file_info_list.length > 0) {
|
|
|
+ const { file_id } = file_info_list[0];
|
|
|
+ this.data.option_list.push({
|
|
|
+ role: this.curRole,
|
|
|
+ file_id,
|
|
|
+ type,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ handleText() {
|
|
|
+ if (this.curRole.length <= 0) {
|
|
|
+ return this.$message.error('请先选择角色');
|
|
|
+ }
|
|
|
+ let text = this.textInput.replace(/\n/, '');
|
|
|
+ let reg = /_{3,}/;
|
|
|
+ let hasFill = reg.test(text); // 是否有填空
|
|
|
+ let content_list = [];
|
|
|
+ if (hasFill) {
|
|
|
+ text = text.replace(/(_{3,})/g, '###$1###');
|
|
|
+ content_list = text
|
|
|
+ .split('###')
|
|
|
+ .filter((item) => item)
|
|
|
+ .map((content) => {
|
|
|
+ let isInput = reg.test(content);
|
|
|
+ return {
|
|
|
+ content: isInput ? '' : content,
|
|
|
+ mark: isInput ? getRandomNumber() : '',
|
|
|
+ type: isInput ? 'input' : 'text',
|
|
|
+ };
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ content_list = [
|
|
|
+ {
|
|
|
+ content: text,
|
|
|
+ mark: '',
|
|
|
+ type: 'text',
|
|
|
+ },
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ this.data.option_list.push({
|
|
|
+ role: this.curRole,
|
|
|
+ text: this.textInput.replace(/\n/, ''),
|
|
|
+ mark: getRandomNumber(),
|
|
|
+ file_id: '',
|
|
|
+ content_list,
|
|
|
+ type: 'text',
|
|
|
+ });
|
|
|
+ this.textInput = '';
|
|
|
+ },
|
|
|
+ moveOption(type, i) {
|
|
|
+ if ((type === 'up' && i === 0) || (type === 'down' && i === this.data.option_list.length - 1)) return;
|
|
|
+ const item = this.data.option_list[i];
|
|
|
+ this.data.option_list.splice(i, 1);
|
|
|
+ this.data.option_list.splice(type === 'up' ? i - 1 : i + 1, 0, item);
|
|
|
+ },
|
|
|
+ deleteOption(i) {
|
|
|
+ let type = this.data.option_list[i].type;
|
|
|
+ this.data.option_list.splice(i, 1);
|
|
|
+ // 如果删除的是文本,需要重新识别
|
|
|
+ if (type === 'text') {
|
|
|
+ this.identifyText();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ saveWav(file_id) {
|
|
|
+ this.data.option_list.push({
|
|
|
+ role: this.curRole,
|
|
|
+ file_id,
|
|
|
+ type: 'audio',
|
|
|
+ });
|
|
|
},
|
|
|
},
|
|
|
};
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
-.content {
|
|
|
+.content-wrapper {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
row-gap: 8px;
|
|
@@ -182,23 +364,115 @@ export default {
|
|
|
margin-bottom: 8px;
|
|
|
border-bottom: $border;
|
|
|
|
|
|
- .subtitle {
|
|
|
- font-size: 14px;
|
|
|
+ .content {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ row-gap: 16px;
|
|
|
+ padding: 16px;
|
|
|
+ background-color: $fill-color;
|
|
|
+
|
|
|
+ .option-list {
|
|
|
+ display: flex;
|
|
|
+ column-gap: 8px;
|
|
|
+
|
|
|
+ .avatar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 40px;
|
|
|
+ min-width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ min-height: 40px;
|
|
|
+ margin-right: 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 50%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .content-operation {
|
|
|
+ display: flex;
|
|
|
+ column-gap: 8px;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ .up-down {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ row-gap: 12px;
|
|
|
+
|
|
|
+ // span 三角形
|
|
|
+ :first-child {
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ cursor: pointer;
|
|
|
+ border-color: transparent;
|
|
|
+ border-style: solid;
|
|
|
+ border-width: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :last-child {
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ cursor: pointer;
|
|
|
+ border-color: transparent;
|
|
|
+ border-style: solid;
|
|
|
+ border-width: 4px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .svg-icon {
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .text {
|
|
|
+ padding: 8px 12px;
|
|
|
+ font-size: 14px;
|
|
|
+ word-break: break-all;
|
|
|
+ background-color: #fff;
|
|
|
+ border-radius: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .image img {
|
|
|
+ border-radius: 8px;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- + .content {
|
|
|
- padding-top: 8px;
|
|
|
- margin-top: 8px;
|
|
|
- border-top: $border;
|
|
|
+ .operation {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-size: 14px;
|
|
|
+
|
|
|
+ &-left {
|
|
|
+ display: flex;
|
|
|
+ column-gap: 8px;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ .el-select {
|
|
|
+ width: 130px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &-right {
|
|
|
+ display: flex;
|
|
|
+ column-gap: 36px;
|
|
|
+ align-items: center;
|
|
|
+ color: $main-color;
|
|
|
+
|
|
|
+ :deep .el-upload {
|
|
|
+ display: flex;
|
|
|
+ column-gap: 8px;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.correct-answer {
|
|
|
- .subtitle {
|
|
|
- margin: 8px 0;
|
|
|
- font-size: 14px;
|
|
|
- color: #4e5969;
|
|
|
- }
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px 8px;
|
|
|
+ margin-top: 8px;
|
|
|
|
|
|
.el-input {
|
|
|
width: 180px;
|
|
@@ -208,9 +482,40 @@ export default {
|
|
|
align-items: center;
|
|
|
color: $text-color;
|
|
|
}
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.el-form {
|
|
|
+ .role {
|
|
|
+ :deep .el-form-item__content {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+
|
|
|
+ .el-input {
|
|
|
+ width: 100px;
|
|
|
+ margin-right: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-color-picker {
|
|
|
+ flex: 1;
|
|
|
+
|
|
|
+ &__trigger {
|
|
|
+ width: 100%;
|
|
|
+ background-color: $fill-color;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__color {
|
|
|
+ width: 22px;
|
|
|
+ height: 22px;
|
|
|
+ border-width: 0;
|
|
|
+ }
|
|
|
|
|
|
- + .el-input {
|
|
|
- margin-left: 8px;
|
|
|
+ &__icon {
|
|
|
+ left: 88%;
|
|
|
+ width: 20px;
|
|
|
+ color: $font-color;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|