This commit is contained in:
那些花儿
2025-09-17 17:52:09 +08:00
parent 9821aebd0a
commit 3f40ee599e
59 changed files with 3697 additions and 4029 deletions
@@ -0,0 +1,34 @@
const subjectBank = {
template: /*language=HTML*/ `
<el-dialog :visible.sync="visible" title="题库" append-to-body width="700px">
</el-dialog>
`,
data() {
return {
visible: false,
category: null,
list: []
}
},
methods: {
onOpen(category) {
debugger
this.visible = true
this.category = category
this.listBank()
},
listBank() {
this.$axios.post('/platform/qsv/activity/listBank', {category: this.category}).then(res => {
if (res.code === 0) {
this.list = res.data
}
})
},
},
style: /*language=CSS*/ `
`
}
@@ -1,6 +1,7 @@
<!--#include('optionImg.js'){}#-->
<!--#include('setting.js'){}#-->
<!--#include('txtImport.js'){}#-->
<!--#include('bank.js'){}#-->
const subjectForm = {
template: /*language=HTML*/ `
@@ -193,6 +194,11 @@ const subjectForm = {
<div style="border-top: 1px solid var(--border-color-lighter);padding: 12px;text-align: right;">
<!-- <el-button @click="visible = false">取消</el-button>-->
<el-button type="primary" icon="el-icon-plus" @click="openBank">
从题库选择
</el-button>
<el-button type="primary" icon="el-icon-upload" @click="openTxtImport">
从文本导入
</el-button>
@@ -207,12 +213,14 @@ const subjectForm = {
<option-img ref="optionImgRef" @confirm="onOptionImgConfirm"></option-img>
<setting ref="settingRef" @confirm="onSettingConfirm"></setting>
<txt-import ref="txtImportRef" @confirm="onTxtImportConfirm"></txt-import>
<bank ref="bankRef" @confirm="onBankConfirm"></bank>
</div>
`,
components: {
'option-img': optionImg,
'setting': setting,
'txt-import': txtImport
'txt-import': txtImport,
'bank': subjectBank
},
data() {
return {
@@ -375,6 +383,16 @@ const subjectForm = {
this.$message.success('成功导入' + importedQuestions.length + '道题目');
},
// 打开题库
openBank(){
this.$refs.bankRef.onOpen(this.activity.category)
},
// 从题库选择
onBankConfirm(subject){
},
//保存
saveQuestionnaire() {
$
@@ -0,0 +1,72 @@
const basicForm = {
template: /*language=HTML*/ `
<el-dialog :visible.sync="dialogVisible" title="基础设置" width="70%">
<div style="overflow-y: auto">
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="150px">
<el-form-item label="类型" prop="category">
<el-radio-group v-model="formData.category" size="small">
<el-radio v-for="item in dict.type.ACTIVITY_QSV_CATEGORY" :label="item.code"
:key="item.code"
border>
{{item.label}}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入标题" maxlength="50"
show-word-limit></el-input>
</el-form-item>
<el-form-item label="说明" prop="description">
<text-editor v-model="formData.description" :height="150"></text-editor>
</el-form-item>
</el-form>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="onSubmit">确 定</el-button>
</div>
</el-dialog>
`,
dicts: ["ACTIVITY_QSV_CATEGORY", "ACTIVITY_QSV_MODE", "ACTIVITY_QSV_REPEAT_MODE", "ACTIVITY_QSV_SCORE_MODE"],
data() {
return {
dialogVisible: false,
formData: {
category: ''
},
formRules: {
category: [{required: true, message: "请选择类型", trigger: "change"}],
title: [{required: true, message: "请输入标题", trigger: "change"}],
},
}
},
methods: {
onOpen(id) {
this.dialogVisible = true
if (id) {
$.post("/platform/qsv/bank/findOne", {id}).then((res) => {
if (res.code === 0) {
this.formData = res.data
}
})
} else {
this.formData = {}
}
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
$.post("/platform/qsv/bank/" + (this.formData.id ? "update" : "save"), {qsvBank: JSON.stringify(this.formData)}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.dialogVisible = false
this.$emit("refresh", null)
}
})
}
})
}
}
}
@@ -0,0 +1,100 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app">
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="标题">
<el-input placeholder="标题" v-model="pageForm.title" clearable></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="管理列表">
<el-button @click="$refs.basicFormRef.onOpen()" size="small" type="primary" class="mr5">新增</el-button>
</table-tool>
<el-table :data="tableData" ref="tableRef" @sort-change="pageOrder">
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
<el-table-column label="标题" prop="title" sortable></el-table-column>
<el-table-column label="类型" prop="category" sortable width="200">
<template scope="{row}">
<dict-tag :options="dict.type.ACTIVITY_QSV_CATEGORY" :value="row.category"></dict-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="450px">
<template scope="{row}">
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="mini" type="primary" @click="openSubject(row.id)">题目设置</el-button>
<el-button size="mini" type="danger" @click="onDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<subject-form ref="subjectFormRef" @refresh="pageData"></subject-form>
</template>
</guava>
<basic-form ref="basicFormRef" @refresh="pageData"></basic-form>
</div>
<script>
<!--#include('basicForm.js'){}#-->
<!--#include('subjectForm.js'){}#-->
new Vue({
el: "#app",
dicts: ["ACTIVITY_QSV_CATEGORY"],
mixins: [initTableMixins],
components: {
"basic-form": basicForm,
"subject-form": subjectForm
},
data() {
return {
}
},
methods: {
// 编辑
openEdit(row) {
this.$refs.basicFormRef.onOpen(row.id)
},
// 题目设置
openSubject(id) {
this.$refs.guava.edit(() => {
this.$refs.subjectFormRef.onOpen(id)
})
},
// 删除
onDelete(id) {
this.$confirm("您确认删除吗, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
$.post("/platform/qsv/activity/delete", {id}).then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.pageData()
}
})
})
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,551 @@
<!--#include('../activity/optionImg.js'){}#-->
<!--#include('../activity/setting.js'){}#-->
<!--#include('../activity/txtImport.js'){}#-->
const subjectForm = {
template: /*language=HTML*/ `
<div class="subject-form-dialog">
<el-form ref="form" :model="formData" :rules="formRules" label-width="120px">
<div style="min-height:50vh;overflow-y: auto">
<draggable v-model="subjects" handle=".drag-handler">
<transition-group>
<div v-for="(subject, subjectIndex) in subjects" :key="subject.id" class="subject-item">
<div class="subject-header">
<div style="display: flex; align-items: center;">
<i class="el-icon-rank drag-handler"></i>
<span style="font-weight: bold; margin-right: 10px;">第 {{subjectIndex + 1}} 题</span>
<el-input
v-model="subject.title"
placeholder="请输入题目"
style="flex: 1">
</el-input>
</div>
<div class="subject-meta">
<div>
题目类型:
<el-select
v-model="subject.type"
@change="subjectTypeChange(subject, subjectIndex)"
placeholder="请选择题目类型"
style="width: 200px;">
<el-option label="单选题" value="radio">
<i class="el-icon-circle-check subject-type-icon"></i>单选题
</el-option>
<el-option label="多选题" value="checkbox">
<i class="el-icon-check subject-type-icon"></i>多选题
</el-option>
<el-option label="填空题" value="text"
v-if="activity.category!=='QUIZ'">
<i class="el-icon-edit subject-type-icon"></i>填空题
</el-option>
<el-option label="判断题" value="judge">
<i class="el-icon-right subject-type-icon"></i>判断题
</el-option>
</el-select>
</div>
<div v-if="subject.type === 'checkbox'">
最大选择数:
<el-input-number v-model="subject.maxMulti"
placeholder="最多可选数量"></el-input-number>
</div>
<!-- <el-input-->
<!-- v-model="subject.hint"-->
<!-- placeholder="题目提示信息"-->
<!-- style="width: 200px;">-->
<!-- <template slot="prepend">提示</template>-->
<!-- </el-input>-->
<div v-if="activity.category==='QUIZ'">
题目分数:
<el-input-number
v-model="subject.score"
:min="0"
:max="100"
placeholder="分数">
<template slot="prepend">分数</template>
</el-input-number>
</div>
<div v-if="activity.category==='QUIZ' && activity.mode==='SCHEDULED'">
显示日期:
<el-date-picker
v-model="subject.displayDate"
type="date"
placeholder="显示日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd">
</el-date-picker>
</div>
</div>
</div>
<template>
<draggable v-model="subject.options" handle=".option-drag-handle"
v-if="subject.type !== 'text'">
<transition-group>
<div v-for="(option, optionIndex) in subject.options"
:key="option.id"
class="option-item">
<i class="el-icon-rank option-drag-handle"></i>
<div class="option-content">
<el-input
v-model="option.text"
:placeholder="'选项'+optionIndex + 1">
<template slot="prepend">选项{{optionIndex + 1}}</template>
</el-input>
<!-- <el-input-->
<!-- v-model="option.hint"-->
<!-- placeholder="选项提示"-->
<!-- style="margin-top: 5px;">-->
<!-- </el-input>-->
<div v-if="option.imageUrl" style="margin-top: 10px;">
<img :src="option.imageUrl" class="image-preview" alt="">
</div>
</div>
<div class="option-tools">
<template v-if="activity.category!=='QUIZ'">
<div class="option-img-box" v-if="option.imgUrl">
<i class="el-icon-remove"
@click="removeOptionImg(subjectIndex,optionIndex)"></i>
<img :src="option.imgUrl" alt=""
@click="openOptionImg(subjectIndex,optionIndex,option)">
</div>
<i v-if="!option.imgUrl" class="el-icon-picture-outline"
style="font-size: 40px;cursor: pointer;" title="上传图片"
@click="openOptionImg(subjectIndex,optionIndex,option)"></i>
</template>
<el-switch
v-if="activity.category==='QUIZ'"
v-model="option.isCorrect"
active-text="正确答案">
</el-switch>
<el-button
type="primary"
icon="el-icon-setting"
size="mini"
@click="openSetting(subjectIndex,optionIndex,option)">
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
@click="removeOption(subject, optionIndex)">
</el-button>
</div>
</div>
</transition-group>
</draggable>
<div class="subject-toolbar">
<div>
<el-button
v-if="subject.type !== 'text'"
type="primary"
icon="el-icon-plus"
size="small"
@click="addOption(subject)">
添加选项
</el-button>
</div>
<div>
<el-button
type="danger"
icon="el-icon-delete"
size="small"
@click="removeSubject(subjectIndex)">
删除题目
</el-button>
</div>
<!-- <div>-->
<!-- <el-switch-->
<!-- v-model="subject.required"-->
<!-- active-text="必答题">-->
<!-- </el-switch>-->
<!-- <el-switch-->
<!-- v-model="subject.showDate"-->
<!-- class="date-visible"-->
<!-- active-text="显示日期">-->
<!-- </el-switch>-->
<!-- </div>-->
</div>
</template>
<div class="meta-info">
<!-- 创建时间:{{ formatDate(subject.createTime) }}-->
<!-- <el-button-->
<!-- type="danger"-->
<!-- icon="el-icon-delete"-->
<!-- size="mini"-->
<!-- style="float: right;"-->
<!-- @click="removeSubject(index)">-->
<!-- 删除题目-->
<!-- </el-button>-->
</div>
</div>
</transition-group>
</draggable>
<el-empty description="描述文字" v-if="subjects.length===0"></el-empty>
</div>
</el-form>
<div style="border-top: 1px solid var(--border-color-lighter);padding: 12px;text-align: right;">
<!-- <el-button @click="visible = false">取消</el-button>-->
<el-button type="primary" icon="el-icon-upload" @click="openTxtImport">
从文本导入
</el-button>
<el-button type="primary" icon="el-icon-plus" @click="addSubject">
添加题目
</el-button>
<el-button type="primary" icon="el-icon-check" @click="saveQuestionnaire">
保存题目
</el-button>
</div>
<option-img ref="optionImgRef" @confirm="onOptionImgConfirm"></option-img>
<setting ref="settingRef" @confirm="onSettingConfirm"></setting>
<txt-import ref="txtImportRef" @confirm="onTxtImportConfirm"></txt-import>
</div>
`,
components: {
'option-img': optionImg,
'setting': setting,
'txt-import': txtImport
},
data() {
return {
id: null,
visible: false,
formData: {},
subjects: [],
previewDialogVisible: false,
activity: {},
formRules: {}
}
},
methods: {
onOpen(id) {
this.visible = true
if (id) {
this.id = id
$.post("/platform/qsv/bank/findOne", {id}).then((res) => {
if (res.code === 0) {
this.activity = res.data
this.subjects = res.data.subjects || []
}
})
}
},
//生成随机id
generateId() {
return Date.now() + Math.random().toString(36).substr(2, 9)
},
//添加题目
addSubject() {
this.subjects.push({
id: this.generateId(),
title: "",
hint: "",
type: "radio",
score: 0,
displayDate: "",
options: [
{
id: this.generateId(),
text: "选项1",
hint: "",
imgUrl: null,
isCorrect: false
},
{
id: this.generateId(),
text: "选项2",
hint: "",
imgUrl: null,
isCorrect: false
}
],
})
},
//题目类型切换
subjectTypeChange(subject, subjectIndex) {
if (subject.type === 'text') {
subject.options = []
}
},
//删除题目
removeSubject(index) {
this.$confirm("确认删除该题目?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.subjects.splice(index, 1)
this.$message.success("删除成功")
})
.catch(() => {
})
},
//添加选项
addOption(subject) {
subject.options.push({
id: this.generateId(),
text: "选项" + subject.options.length + 1,
hint: "",
imgUrl: null,
isCorrect: false
})
},
//删除选项
removeOption(subject, optionIndex) {
subject.options.splice(optionIndex, 1)
},
//打开选项上传图片
openOptionImg(subjectIndex, optionIndex, option) {
this.$refs.optionImgRef.onOpen(option.imgUrl, {
subjectIndex,
optionIndex
})
},
//选项图片上传成功回调
onOptionImgConfirm({img, ext}) {
this.$set(this.subjects[ext.subjectIndex].options[ext.optionIndex], "imgUrl", img)
},
//删除选项图片
removeOptionImg(subjectIndex, optionIndex) {
this.$set(this.subjects[subjectIndex].options[optionIndex], "imgUrl", null)
console.log(this.subjects[subjectIndex])
},
//打开选项设置
openSetting(subjectIndex, optionIndex, option) {
this.$refs.settingRef.onOpen(option, {
subjectIndex,
optionIndex
})
},
//选项设置确认
onSettingConfirm({option, ext}) {
this.subjects[ext.subjectIndex].options[ext.optionIndex] = option
console.log(this.subjects[ext.subjectIndex].options[ext.optionIndex])
// this.$set(this.subjects[ext.subjectIndex].options[ext.optionIndex], "isCorrect", option.isCorrect)
// this.$set(this.subjects[ext.subjectIndex].options[ext.optionIndex], "hint", option.hint)
},
//从文本导入
openTxtImport() {
this.$refs.txtImportRef.visible = true;
},
//处理导入的题目
onTxtImportConfirm(importedQuestions) {
if (!importedQuestions || importedQuestions.length === 0) {
return;
}
// 计算新题目的起始序号
const startSortNum = this.subjects.length + 1;
// 更新导入题目的序号
importedQuestions.forEach((question, index) => {
question.sortNum = startSortNum + index;
});
// 将导入的题目添加到现有题目列表
this.subjects.push(...importedQuestions);
this.$message.success('成功导入' + importedQuestions.length + '道题目');
},
//保存
saveQuestionnaire() {
this.$axios.post("/platform/qsv/bank/saveSubjects", {
id: this.activity.id,
subjects: JSON.stringify(this.subjects)
}).then((res) => {
if (res.code === 0) {
this.visible = false
this.$message.success(res.msg)
this.$emit("refresh", null)
}
})
}
},
style: /*language=CSS*/ `
/*.subject-form-dialog,.subject-form-dialog .el-dialog__body {*/
/* background: rgb(248, 249, 250);*/
/*}*/
::v-deep .questionnaire-editor {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
::v-deep .editor-header {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
}
::v-deep .subject-item {
background: white;
border-radius: 8px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
border: 1px dashed #d9d9d9;
}
::v-deep .subject-item:hover {
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.1);
/*transform: translateY(-1px);*/
}
::v-deep .subject-header {
border-bottom: 1px solid #eee;
padding-bottom: 15px;
margin-bottom: 15px;
}
::v-deep .subject-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
}
::v-deep .toolbar {
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 1000;
}
::v-deep .toolbar-btn {
margin: 5px 0;
width: 100%;
}
::v-deep .option-item {
background: #f8f9fa;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
display: flex;
align-items: center;
}
::v-deep .option-drag-handle {
cursor: move;
margin-right: 10px;
color: #909399;
}
::v-deep .option-content {
flex-grow: 1;
margin-right: 10px;
}
::v-deep .option-tools {
display: flex;
align-items: center;
gap: 10px;
}
::v-deep .preview-dialog {
max-width: 800px;
margin: 0 auto;
}
::v-deep .date-visible {
margin-left: 20px;
}
::v-deep .subject-meta {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 15px;
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
}
::v-deep .image-preview {
max-width: 200px;
max-height: 200px;
margin-top: 10px;
border-radius: 4px;
border: 1px solid #dcdfe6;
}
::v-deep .drag-handler {
cursor: move;
color: #909399;
margin-right: 10px;
}
::v-deep .meta-info {
color: #909399;
font-size: 12px;
margin-top: 5px;
}
::v-deep .subject-type-icon {
margin-right: 5px;
}
::v-deep .option-img-box {
position: relative;
width: 40px;
height: 40px;
cursor: pointer;
flex-shrink: 0
}
::v-deep .option-img-box:hover .el-icon-remove {
display: block;
}
::v-deep .option-img-box img {
width: 100%;
height: 100%;
}
::v-deep .option-img-box .el-icon-remove {
position: absolute;
top: -7px;
right: -7px;
color: red;
display: none;
}
::v-deep .option-img-box .el-icon-remove:hover {
transform: scale(1.1);
}
`
}