Files
estate/src/views/system/Questionnaire/components/QuestionnaireEdit.vue
2026-04-16 21:03:35 +08:00

330 lines
9.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="edit-container">
<el-row :gutter="20">
<!-- 左侧组件库 -->
<el-col :span="4">
<el-card shadow="hover" class="component-panel sticky-card">
<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"
class="component-list"
>
<template #item="{ element }">
<div class="component-item">
<el-icon><component :is="element.icon" /></el-icon>
<span>{{ element.label }}</span>
</div>
</template>
</draggable>
</el-card>
</el-col>
<!-- 中间设计画布 -->
<el-col :span="12">
<el-card shadow="hover" class="canvas-panel">
<template #header>
<div class="card-header">
<span>设计画布</span>
</div>
</template>
<div class="question-title-area">
<el-input v-model="form.title" placeholder="请输入问卷标题" class="title-input" />
<el-input v-model="form.description" type="textarea" placeholder="请输入问卷描述(选填)" />
</div>
<draggable v-model="form.content" group="components" item-key="cid" class="question-list" @add="handleAddQuestion">
<template #item="{ element, index }">
<div class="question-item" :class="{ 'is-active': activeIndex === index }" @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">
{{ 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>
</div>
</div>
</template>
</draggable>
<el-empty v-if="form.content.length === 0" description="请从左侧拖拽组件到此处" />
</el-card>
</el-col>
<!-- 右侧属性配置 -->
<el-col style="position: sticky; top: 60px" :span="8">
<el-card shadow="hover" class="property-panel sticky-card" v-if="activeQuestion">
<template #header>
<div class="card-header">
<span>属性配置</span>
</div>
</template>
<el-form style="position: sticky; top: 60px" label-width="80px">
<el-form-item label="题目标题">
<el-input v-model="activeQuestion.title" placeholder="请输入题目标题" />
</el-form-item>
<el-form-item label="是否必填">
<el-switch v-model="activeQuestion.required" />
</el-form-item>
<!-- 单选/多选特有属性 -->
<template v-if="activeQuestion.type === 'radio' || activeQuestion.type === 'checkbox'">
<el-divider content-position="left">选项设置</el-divider>
<div v-for="(opt, idx) in activeQuestion.options" :key="idx" class="option-item">
<el-input v-model="opt.label" placeholder="选项内容" style="width: 200px; margin-right: 10px; margin-bottom: 10px" />
<el-button link type="danger" :icon="Delete" @click="activeQuestion.options.splice(idx, 1)"> 删除 </el-button>
</div>
<el-button type="primary" @click="activeQuestion.options.push({ label: '新选项' })"> 添加选项 </el-button>
</template>
</el-form>
</el-card>
<el-empty v-else description="请点击选中一道题目" />
</el-col>
</el-row>
<!-- 底部操作栏 -->
<div class="footer-actions">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSave">保存草稿</el-button>
<el-button type="success" @click="handlePublish">保存并发布</el-button>
</div>
</div>
</template>
<script setup lang="ts" name="QuestionnaireEdit">
import draggable from 'vuedraggable';
import { Plus, Delete, Rank, Tickets, Document, Edit } from '@element-plus/icons-vue';
import { ComponentItem, QuestionComponentItem, QuestionComponentnaireForm } from '@/api/system/Questionnaire/type';
import { ElMessage } from 'element-plus'; // 👈 补充引入
// ==================== Props & Emit ====================
const props = defineProps<{
/** 初始问卷数据(编辑时传入) */
initialData?: Partial<QuestionComponentnaireForm>;
}>();
const emit = defineEmits<{
/** 取消编辑 */
cancel: [];
/** 保存草稿 */
save: [data: QuestionComponentnaireForm];
/** 发布问卷 */
publish: [data: QuestionComponentnaireForm];
}>();
import { markRaw } from 'vue'; // 👈 引入 markRaw
// ==================== 响应式数据 ====================
const componentList = ref<ComponentItem[]>([
{ type: 'radio', label: '单选题', icon: markRaw(Tickets) },
{ type: 'checkbox', label: '多选题', icon: markRaw(Tickets) },
{ type: 'input', label: '填空题', icon: markRaw(Edit) },
{ type: 'textarea', label: '多行文本', icon: markRaw(Document) }
]);
const form = ref<QuestionComponentnaireForm>({
id: null,
title: '',
description: '',
content: []
});
const activeIndex = ref<number>(-1);
// ==================== 计算属性 ====================
const activeQuestion = computed<QuestionComponentItem | null>(() => {
if (activeIndex.value === -1) return null;
return form.value.content[activeIndex.value];
});
// ==================== 辅助方法 ====================
function getComponentLabel(type: string): string {
const item = componentList.value.find((c) => c.type === type);
return item ? item.label : '';
}
// 克隆组件(用于从左侧拖入中间)
function cloneComponent(component: ComponentItem): QuestionComponentItem {
return {
cid: Date.now(), // 生成唯一ID
type: component.type,
title: 'ces',
required: true,
options: component.type === 'radio' || component.type === 'checkbox' ? [{ label: '选项1' }, { label: '选项2' }] : []
};
}
// 选中题目
function selectQuestion(index: number): void {
activeIndex.value = index;
}
// 添加题目后的处理
function handleAddQuestion(evt: any): void {
// 自动选中新添加的题目
activeIndex.value = evt.newIndex;
}
// 删除题目
function deleteQuestion(index: number): void {
form.value.content.splice(index, 1);
if (activeIndex.value === index) {
activeIndex.value = -1;
} else if (activeIndex.value > index) {
activeIndex.value--;
}
}
// ==================== 业务方法 ====================
/** 表单验证 */
function validateForm(): boolean {
if (!form.value.title.trim()) {
ElMessage.error('请输入问卷标题');
return false;
}
return true;
}
/** 取消 */
function handleCancel(): void {
emit('cancel');
}
/** 保存草稿 */
function handleSave(): void {
if (!validateForm()) return;
emit('save', { ...form.value });
}
/** 发布问卷 */
function handlePublish(): void {
if (!validateForm()) return;
emit('publish', { ...form.value });
}
/** 初始化数据(编辑时调用) */
function init(data: Partial<QuestionComponentnaireForm>): void {
form.value = {
id: null,
title: '',
description: '',
content: [],
...data
};
// 处理content字符串转对象如果是从数据库取的
if (typeof form.value.content === 'string') {
try {
form.value.content = JSON.parse(form.value.content);
} catch (e) {
form.value.content = [];
}
}
activeIndex.value = -1;
}
/** 重置表单 */
function reset(): void {
form.value = {
id: null,
title: '',
description: '',
content: []
};
activeIndex.value = -1;
}
// ==================== 生命周期 ====================
onMounted(() => {
// 如果传入了初始数据,自动初始化
if (props.initialData) {
init(props.initialData);
}
});
// ==================== 暴露方法 ====================
defineExpose({ init, reset });
</script>
<style scoped lang="scss">
.edit-container {
position: relative;
.component-list {
.component-item {
display: flex;
align-items: center;
padding: 10px;
margin-bottom: 10px;
background: #f4f4f5;
border-radius: 4px;
cursor: move;
.el-icon {
margin-right: 8px;
}
}
}
.question-title-area {
margin-bottom: 20px;
padding: 10px;
border: 1px dashed #dcdfe6;
.title-input {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}
}
.question-list {
min-height: 200px;
.question-item {
display: flex;
align-items: center;
padding: 12px;
margin-bottom: 10px;
border: 1px solid #ebeef5;
background: #fff;
border-radius: 4px;
&:hover {
border-color: #409eff;
}
&.is-active {
border-color: #409eff;
background: #ecf5ff;
}
.question-handle {
cursor: move;
padding: 0 10px;
color: #909399;
}
.question-content {
flex: 1;
.question-type-tag {
margin-right: 8px;
}
}
}
}
.canvas-panel {
}
.footer-actions {
width: 100%;
background: #fff;
padding: 10px 20px;
margin-top: 60px;
text-align: right;
border-top: 1px solid #dcdfe6;
}
}
</style>