9/2 禅道问题
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<div class="AddBuildingBox">
|
||||
<PageHeader :title="userid ? '编辑问卷' : '添加问卷'"></PageHeader>
|
||||
<div class="AddBuildingBody">
|
||||
<el-form :rules="rules" class="formBody" label-position="right" label-width="130px">
|
||||
<el-form class="formBody" label-position="right" label-width="130px">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>基本信息</span>
|
||||
@@ -25,7 +25,7 @@
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="时间范围" prop="startTime">
|
||||
<el-form-item label="时间范围" required>
|
||||
<el-date-picker
|
||||
v-model="timeData"
|
||||
:disabled-date="handleDate"
|
||||
@@ -82,12 +82,9 @@ const form = ref({
|
||||
startTime: '',
|
||||
endTime: ''
|
||||
});
|
||||
const rules = ref({
|
||||
startTime: [{ required: true, message: '请选择日期范围', trigger: 'blur' }]
|
||||
});
|
||||
const timeData = ref([]);
|
||||
function handleDate(data: string) {
|
||||
const date = new Date();
|
||||
const date = new Date(new Date().toLocaleDateString());
|
||||
return date.getTime() > new Date(data).getTime();
|
||||
}
|
||||
function handleModelDate(date: string) {
|
||||
@@ -157,12 +154,16 @@ async function handleSave(data: QuestionComponentnaireForm) {
|
||||
if (timeData.value.length <= 0) {
|
||||
return ElMessage.warning('请选择问卷时间范围');
|
||||
}
|
||||
const list = data.content ?? [];
|
||||
if (list.length <= 0) {
|
||||
return ElMessage.warning('请添加问卷题目');
|
||||
}
|
||||
try {
|
||||
// 父组件自己调用接口提交
|
||||
if (userid.value !== null) {
|
||||
await updateQuestionnaire({
|
||||
...data,
|
||||
content: JSON.stringify(data.content),
|
||||
content: JSON.stringify(list),
|
||||
status: '0', // 草稿状态
|
||||
startTime: timeData.value[0],
|
||||
endTime: timeData.value[1],
|
||||
@@ -171,7 +172,7 @@ async function handleSave(data: QuestionComponentnaireForm) {
|
||||
} else {
|
||||
await addQuestionnaire({
|
||||
...data,
|
||||
content: JSON.stringify(data.content),
|
||||
content: JSON.stringify(list),
|
||||
status: '0', // 草稿状态
|
||||
startTime: timeData.value[0],
|
||||
endTime: timeData.value[1],
|
||||
@@ -191,11 +192,15 @@ async function handlePublish(data: QuestionComponentnaireForm) {
|
||||
if (timeData.value.length <= 0) {
|
||||
return ElMessage.warning('请选择问卷时间范围');
|
||||
}
|
||||
const list = data.content ?? [];
|
||||
if (list.length <= 0) {
|
||||
return ElMessage.warning('请添加问卷题目');
|
||||
}
|
||||
try {
|
||||
if (userid.value !== null) {
|
||||
await updateQuestionnaire({
|
||||
...data,
|
||||
content: JSON.stringify(data.content),
|
||||
content: JSON.stringify(list),
|
||||
status: '1', // 发布状态
|
||||
startTime: timeData.value[0],
|
||||
endTime: timeData.value[1],
|
||||
@@ -204,7 +209,7 @@ async function handlePublish(data: QuestionComponentnaireForm) {
|
||||
} else {
|
||||
await addQuestionnaire({
|
||||
...data,
|
||||
content: JSON.stringify(data.content),
|
||||
content: JSON.stringify(list),
|
||||
status: '1', // 发布状态
|
||||
endTime: timeData.value[1],
|
||||
startTime: timeData.value[0],
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
<el-row :gutter="20">
|
||||
<!-- 左侧:组件库 -->
|
||||
<el-col :span="4">
|
||||
<el-card shadow="hover" class="component-panel sticky-card">
|
||||
<el-card class="component-panel sticky-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>组件库</span>
|
||||
</div>
|
||||
</template>
|
||||
<draggable
|
||||
:list="componentList"
|
||||
:group="{ name: 'components', pull: 'clone', put: false }"
|
||||
:clone="cloneComponent"
|
||||
item-key="type"
|
||||
:group="{ name: 'components', pull: 'clone', put: false }"
|
||||
:list="componentList"
|
||||
class="component-list"
|
||||
item-key="type"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div class="component-item">
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
<!-- 中间:设计画布 -->
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" class="canvas-panel">
|
||||
<el-card class="canvas-panel" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>设计画布</span>
|
||||
@@ -36,24 +36,24 @@
|
||||
</template>
|
||||
|
||||
<div class="question-title-area">
|
||||
<el-input maxlength="20" show-word-limit v-model.trim="form.title" placeholder="请输入问卷标题" class="title-input" />
|
||||
<el-input maxlength="200" show-word-limit v-model.trim="form.description" type="textarea" placeholder="请输入问卷描述(选填)" />
|
||||
<el-input v-model.trim="form.title" class="title-input" maxlength="20" placeholder="请输入问卷标题" show-word-limit />
|
||||
<el-input v-model.trim="form.description" maxlength="200" placeholder="请输入问卷描述(选填)" show-word-limit type="textarea" />
|
||||
</div>
|
||||
|
||||
<draggable v-model="form.content" group="components" item-key="cid" class="question-list" @add="handleAddQuestion">
|
||||
<draggable v-model="form.content" class="question-list" group="components" item-key="cid" @add="handleAddQuestion">
|
||||
<template #item="{ element, index }">
|
||||
<div class="question-item" :class="{ 'is-active': activeIndex === index }" @click="selectQuestion(index)">
|
||||
<div :class="{ 'is-active': activeIndex === index }" class="question-item" @click="selectQuestion(index)">
|
||||
<div class="question-handle">
|
||||
<el-icon><Rank /></el-icon>
|
||||
</div>
|
||||
<div class="question-content">
|
||||
<el-tag size="small" type="success" class="question-type-tag">
|
||||
<el-tag class="question-type-tag" size="small" type="success">
|
||||
{{ getComponentLabel(element.type) }}
|
||||
</el-tag>
|
||||
<span>Q{{ index + 1 }}. {{ element.title || '未命名题目' }}</span>
|
||||
</div>
|
||||
<div class="question-actions">
|
||||
<el-button link type="danger" :icon="Delete" @click.stop="deleteQuestion(index)"> 删除 </el-button>
|
||||
<el-button :icon="Delete" link type="danger" @click.stop="deleteQuestion(index)"> 删除 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -63,16 +63,16 @@
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧:属性配置 -->
|
||||
<el-col style="position: sticky; top: 60px" :span="8">
|
||||
<el-card shadow="hover" class="property-panel sticky-card" v-if="activeQuestion">
|
||||
<el-col :span="8" style="position: sticky; top: 60px">
|
||||
<el-card v-if="activeQuestion" class="property-panel sticky-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>属性配置</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :rules="rules" :model="activeQuestion" style="position: sticky; top: 60px" label-width="80px">
|
||||
<el-form :model="activeQuestion" :rules="rules" label-width="80px" style="position: sticky; top: 60px">
|
||||
<el-form-item label="题目标题" prop="title">
|
||||
<el-input maxlength="50" v-model.trim="activeQuestion.title" placeholder="请输入题目标题" />
|
||||
<el-input v-model.trim="activeQuestion.title" maxlength="50" placeholder="请输入题目标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否必填">
|
||||
<el-switch v-model.trim="activeQuestion.required" />
|
||||
@@ -82,18 +82,19 @@
|
||||
<el-divider content-position="left">选项设置</el-divider>
|
||||
<div v-for="(opt, idx) in activeQuestion.options" :key="idx" class="option-item">
|
||||
<el-input
|
||||
maxlength="20"
|
||||
v-model.trim="opt.label"
|
||||
:class="{ 'is-duplicate': isOptionDuplicate(idx) }"
|
||||
maxlength="20"
|
||||
placeholder="选项内容"
|
||||
style="width: 200px; margin-right: 10px; margin-bottom: 10px"
|
||||
@blur="handleOptionBlur(idx)"
|
||||
/>
|
||||
<el-button link type="danger" v-if="activeQuestion.options.length > 2" :icon="Delete" @click="activeQuestion.options.splice(idx, 1)">
|
||||
<span v-if="isOptionDuplicate(idx)" class="duplicate-tip">选项重复</span>
|
||||
<el-button v-if="activeQuestion.options.length > 2" :icon="Delete" link type="danger" @click="activeQuestion.options.splice(idx, 1)">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button type="primary" @click="activeQuestion.options.push({ label: '选项' + (activeQuestion.options.length + 1) })">
|
||||
添加选项
|
||||
</el-button>
|
||||
<el-button type="primary" @click="addOption">添加选项</el-button>
|
||||
</template>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -110,11 +111,13 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="QuestionnaireEdit">
|
||||
<script lang="ts" name="QuestionnaireEdit" setup>
|
||||
import draggable from 'vuedraggable';
|
||||
import { Delete, Rank, Tickets, Document, Edit } from '@element-plus/icons-vue';
|
||||
import { ComponentItem, QuestionComponentItem, QuestionComponentnaireForm } from '@/api/system/Questionnaire/type';
|
||||
import { ElMessage } from 'element-plus'; // 👈 补充引入
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
// ==================== Props & Emit ====================
|
||||
const props = defineProps<{
|
||||
/** 初始问卷数据(编辑时传入) */
|
||||
@@ -129,7 +132,7 @@ const emit = defineEmits<{
|
||||
/** 发布问卷 */
|
||||
publish: [data: QuestionComponentnaireForm];
|
||||
}>();
|
||||
import { markRaw } from 'vue'; // 👈 引入 markRaw
|
||||
|
||||
// ==================== 响应式数据 ====================
|
||||
const componentList = ref<ComponentItem[]>([
|
||||
{ type: 'radio', label: '单选题', icon: markRaw(Tickets) },
|
||||
@@ -157,6 +160,46 @@ const activeQuestion = computed<QuestionComponentItem | null>(() => {
|
||||
return form.value.content[activeIndex.value];
|
||||
});
|
||||
|
||||
// ==================== 选项重复校验 ====================
|
||||
/** 判断指定选项是否与其他选项内容重复(同一题内,trim 后比对) */
|
||||
function isOptionDuplicate(idx: number): boolean {
|
||||
const options = activeQuestion.value?.options;
|
||||
if (!options) return false;
|
||||
const label = options[idx]?.label?.trim();
|
||||
if (!label) return false;
|
||||
return options.some((opt, i) => i !== idx && opt.label?.trim() === label);
|
||||
}
|
||||
|
||||
/** 选项失焦时弹提示 */
|
||||
function handleOptionBlur(idx: number): void {
|
||||
if (isOptionDuplicate(idx)) {
|
||||
ElMessage.warning('选项内容不能重复');
|
||||
}
|
||||
}
|
||||
|
||||
/** 添加选项:智能生成不冲突的默认名称(避免重名) */
|
||||
function addOption(): void {
|
||||
const options = activeQuestion.value!.options;
|
||||
let nextIndex = options.length + 1;
|
||||
while (options.some((opt) => opt.label === '选项' + nextIndex)) {
|
||||
nextIndex++;
|
||||
}
|
||||
options.push({ label: '选项' + nextIndex });
|
||||
}
|
||||
|
||||
/** 遍历所有题目,校验选项是否重复;有重复返回错误信息,否则返回空串 */
|
||||
function validateAllOptions(): string {
|
||||
for (const q of form.value.content) {
|
||||
if (q.type === 'radio' || q.type === 'checkbox') {
|
||||
const labels = q.options.map((o) => o.label?.trim()).filter(Boolean);
|
||||
if (new Set(labels).size !== labels.length) {
|
||||
return `题目「${q.title || '未命名题目'}」存在重复选项`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
function getComponentLabel(type: string): string {
|
||||
const item = componentList.value.find((c) => c.type === type);
|
||||
@@ -166,7 +209,7 @@ function getComponentLabel(type: string): string {
|
||||
// 克隆组件(用于从左侧拖入中间)
|
||||
function cloneComponent(component: ComponentItem): QuestionComponentItem {
|
||||
return {
|
||||
cid: Date.now(), // 生成唯一ID
|
||||
cid: Date.now(),
|
||||
type: component.type,
|
||||
title: '',
|
||||
required: true,
|
||||
@@ -181,7 +224,6 @@ function selectQuestion(index: number): void {
|
||||
|
||||
// 添加题目后的处理
|
||||
function handleAddQuestion(evt: any): void {
|
||||
// 自动选中新添加的题目
|
||||
activeIndex.value = evt.newIndex;
|
||||
}
|
||||
|
||||
@@ -198,15 +240,19 @@ function deleteQuestion(index: number): void {
|
||||
// ==================== 业务方法 ====================
|
||||
/** 表单验证 */
|
||||
function validateForm(): boolean {
|
||||
// 验证问卷标题
|
||||
if (!form.value.title.trim()) {
|
||||
ElMessage.error('请输入问卷标题');
|
||||
return false;
|
||||
}
|
||||
if (form.value.content.some((item) => item.title === '')) {
|
||||
if (form.value.content.some((item) => !item.title?.trim())) {
|
||||
ElMessage.error('请填写所有题目标题');
|
||||
return false;
|
||||
}
|
||||
const duplicateMsg = validateAllOptions();
|
||||
if (duplicateMsg) {
|
||||
ElMessage.error(duplicateMsg);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -237,7 +283,6 @@ function init(data: Partial<QuestionComponentnaireForm>): void {
|
||||
...data
|
||||
};
|
||||
|
||||
// 处理content字符串转对象(如果是从数据库取的)
|
||||
if (typeof form.value.content === 'string') {
|
||||
try {
|
||||
form.value.content = JSON.parse(form.value.content);
|
||||
@@ -262,7 +307,6 @@ function reset(): void {
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
onMounted(() => {
|
||||
// 如果传入了初始数据,自动初始化
|
||||
if (props.initialData) {
|
||||
init(props.initialData);
|
||||
}
|
||||
@@ -272,7 +316,7 @@ onMounted(() => {
|
||||
defineExpose({ init, reset });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.edit-container {
|
||||
position: relative;
|
||||
|
||||
@@ -333,8 +377,27 @@ defineExpose({ init, reset });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.canvas-panel {
|
||||
}
|
||||
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.is-duplicate {
|
||||
:deep(.el-input__wrapper) {
|
||||
box-shadow: 0 0 0 1px #f56c6c inset;
|
||||
}
|
||||
}
|
||||
|
||||
.duplicate-tip {
|
||||
color: #f56c6c;
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
|
||||
Reference in New Issue
Block a user