- 新增问卷任务页面及组件(创建任务、人员选择、任务详情) - 新增问卷预览组件 - 新增答题详情对话框 - 优化问卷列表和问卷记录页面 - 优化Dashboard风险趋势图Y轴动态缩放 - 更新评估报告导出页面
819 lines
21 KiB
Vue
819 lines
21 KiB
Vue
<template>
|
||
<!-- 列表 -->
|
||
<ContentWrap>
|
||
<div class="question-header">
|
||
<div class="header-left">
|
||
<el-button
|
||
type="primary"
|
||
plain
|
||
@click="openForm('create')"
|
||
v-hasPermi="['prison:question:create']"
|
||
>
|
||
<Icon icon="ep:plus" class="mr-5px" /> 新建问题
|
||
</el-button>
|
||
<el-button
|
||
type="success"
|
||
plain
|
||
@click="openPartDialog"
|
||
v-hasPermi="['prison:question:create']"
|
||
>
|
||
<Icon icon="ep:folder" class="mr-5px" /> 分区管理
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
plain
|
||
:disabled="checkedIds.length === 0"
|
||
@click="handleDeleteBatch"
|
||
v-hasPermi="['prison:question:delete']"
|
||
>
|
||
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
|
||
</el-button>
|
||
</div>
|
||
<div class="header-right">
|
||
<span class="total-count">共 {{ list.length }} 道问题</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分区显示(可拖拽排序) -->
|
||
<div class="parts-container">
|
||
<draggable
|
||
v-model="partitions"
|
||
item-key="name"
|
||
handle=".part-drag-handle"
|
||
:animation="200"
|
||
@end="onPartitionDragEnd"
|
||
class="parts-list"
|
||
>
|
||
<template #item="{ element: partition }">
|
||
<div class="part-group" :class="{ 'is-default': !partition.name }">
|
||
<!-- 分区头部 -->
|
||
<div class="part-header">
|
||
<div class="part-drag-handle">
|
||
<Icon icon="ep:rank" class="drag-icon" />
|
||
</div>
|
||
<el-checkbox
|
||
v-model="partition.selected"
|
||
@change="(val: boolean) => togglePartQuestions(partition, val)"
|
||
:disabled="!partition.name"
|
||
>
|
||
<span class="part-title">{{ partition.name || '默认分区' }}</span>
|
||
</el-checkbox>
|
||
<span class="part-count">({{ partition.questions.length }} 道题)</span>
|
||
<div class="part-actions">
|
||
<el-button
|
||
v-if="partition.name"
|
||
type="primary"
|
||
link
|
||
size="small"
|
||
@click="editPartition(partition)"
|
||
>
|
||
<Icon icon="ep:edit" /> 编辑
|
||
</el-button>
|
||
<el-button
|
||
v-if="partition.name"
|
||
type="danger"
|
||
link
|
||
size="small"
|
||
@click="deletePartition(partition)"
|
||
>
|
||
<Icon icon="ep:delete" /> 删除
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分区内的问题列表(可拖拽排序) -->
|
||
<draggable
|
||
v-model="partition.questions"
|
||
item-key="id"
|
||
handle=".question-drag-handle"
|
||
:animation="200"
|
||
@end="onQuestionDragEnd(partition)"
|
||
class="questions-table"
|
||
ghost-class="ghost-row"
|
||
>
|
||
<template #item="{ element: question }">
|
||
<div class="question-row" :class="{ 'is-hidden': !partition.selected }">
|
||
<div class="question-drag-handle">
|
||
<Icon icon="ep:rank" class="drag-icon" />
|
||
</div>
|
||
<el-checkbox
|
||
v-model="question._checked"
|
||
@change="(val: boolean) => toggleQuestionCheck(question, val, partition)"
|
||
class="question-checkbox"
|
||
/>
|
||
<span class="question-index">{{ question._index + 1 }}</span>
|
||
<div class="question-info">
|
||
<div class="question-title-row">
|
||
<span class="type-badge" :class="'type-' + question.type">
|
||
{{ getTypeLabel(question.type) }}
|
||
</span>
|
||
<span class="title-text">{{ question.title }}</span>
|
||
<el-tag v-if="question.isRequired" type="danger" size="small">必填</el-tag>
|
||
</div>
|
||
<div v-if="question.helpText" class="help-text">
|
||
<Icon icon="ep:info-filled" />
|
||
{{ question.helpText }}
|
||
</div>
|
||
</div>
|
||
<div class="question-meta">
|
||
<span class="meta-item">
|
||
<Icon icon="ep:coin" /> {{ question.score || 0 }}分
|
||
</span>
|
||
<el-tooltip
|
||
v-if="question.autoFillType && question.autoFillType !== 'NONE'"
|
||
:content="getAutoFillContent(question)"
|
||
placement="top"
|
||
>
|
||
<el-tag :type="question.autoFillType === 'AUTO' ? 'primary' : 'warning'" size="small">
|
||
{{ getAutoFillLabel(question.autoFillType) }}
|
||
</el-tag>
|
||
</el-tooltip>
|
||
</div>
|
||
<div class="question-actions">
|
||
<el-button
|
||
type="primary"
|
||
link
|
||
size="small"
|
||
@click="openForm('update', question.id)"
|
||
v-hasPermi="['prison:question:update']"
|
||
>
|
||
<Icon icon="ep:edit" /> 修改
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
link
|
||
size="small"
|
||
@click="handleDelete(question.id)"
|
||
v-hasPermi="['prison:question:delete']"
|
||
>
|
||
<Icon icon="ep:delete" /> 删除
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</draggable>
|
||
</div>
|
||
</template>
|
||
</draggable>
|
||
</div>
|
||
</ContentWrap>
|
||
|
||
<!-- 表单弹窗:添加/修改 -->
|
||
<QuestionForm ref="formRef" @success="getList" />
|
||
|
||
<!-- 分区管理弹窗 -->
|
||
<Dialog title="分区管理" v-model="partDialogVisible" width="600px">
|
||
<el-form :model="partForm" label-width="100px">
|
||
<el-form-item label="分区列表">
|
||
<div class="part-manage-list">
|
||
<div v-for="(part, index) in allPartitions" :key="part.id || index" class="part-manage-item">
|
||
<el-icon class="drag-handle"><Rank /></el-icon>
|
||
<el-input
|
||
v-model="part.name"
|
||
:placeholder="part.isDefault ? '默认分区' : '请输入分区名称'"
|
||
:disabled="part.isDefault"
|
||
style="flex: 1; margin: 0 10px"
|
||
/>
|
||
<el-input-number
|
||
v-model="part.sort"
|
||
:min="0"
|
||
:max="999"
|
||
controls-position="right"
|
||
style="width: 100px"
|
||
:disabled="part.isDefault"
|
||
/>
|
||
<el-button
|
||
v-if="!part.isDefault"
|
||
type="danger"
|
||
:icon="Delete"
|
||
circle
|
||
@click="removePartition(index)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" plain :icon="Plus" @click="addPartition" v-hasPermi="['prison:question:create']">添加分区</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="savePartitions" type="primary" v-hasPermi="['prison:question:update']">保存设置</el-button>
|
||
<el-button @click="partDialogVisible = false">取消</el-button>
|
||
</template>
|
||
</Dialog>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||
import { QuestionApi, Question } from '@/api/prison/question'
|
||
import QuestionForm from './QuestionForm.vue'
|
||
import { Folder, InfoFilled, Rank, Plus, Delete } from '@element-plus/icons-vue'
|
||
import draggable from 'vuedraggable'
|
||
|
||
defineOptions({ name: 'PrisonQuestionList' })
|
||
|
||
const message = useMessage()
|
||
const { t } = useI18n()
|
||
|
||
const props = defineProps<{
|
||
questionnaireId?: number
|
||
}>()
|
||
|
||
const loading = ref(false)
|
||
const list = ref<Question[]>([])
|
||
const formRef = ref()
|
||
const partDialogVisible = ref(false)
|
||
|
||
// 分区列表(带问题)
|
||
const partitions = ref<Array<{
|
||
name: string
|
||
sort: number
|
||
selected: boolean
|
||
questions: Array<Question & { _index: number; _checked: boolean }>
|
||
}>>([])
|
||
|
||
// 所有分区(用于管理弹窗)
|
||
const allPartList = ref<Array<{ id: string; name: string; sort: number; isDefault: boolean }>>([])
|
||
const allPartitions = computed({
|
||
get: () => allPartList.value,
|
||
set: (val) => { allPartList.value = val }
|
||
})
|
||
|
||
// 分区表单
|
||
const partForm = ref({
|
||
name: '',
|
||
sort: 0
|
||
})
|
||
|
||
// 选中的问题ID
|
||
const checkedIds = ref<number[]>([])
|
||
|
||
/** 问题类型标签 */
|
||
const getTypeLabel = (type: number) => {
|
||
const labels: Record<number, string> = { 1: '单选', 2: '多选', 3: '填空', 4: '评分', 5: '日期', 6: '数字' }
|
||
return labels[type] || '未知'
|
||
}
|
||
|
||
/** 自动填充标签 */
|
||
const getAutoFillLabel = (type: string) => {
|
||
return { 'NONE': '-', 'AUTO': '自动', 'MANUAL': '手动' }[type] || type
|
||
}
|
||
|
||
/** 自动填充内容 */
|
||
const getAutoFillContent = (row: Question) => {
|
||
if (row.autoFillType === 'AUTO') {
|
||
return `自动填充来源:${row.autoFillSource || '-'}`
|
||
}
|
||
return '手动输入'
|
||
}
|
||
|
||
/** 从问题列表提取所有分区 */
|
||
const extractPartitions = (questions: Question[]) => {
|
||
const partMap = new Map<string, Question[]>()
|
||
const defaultPart: Question[] = []
|
||
|
||
questions.forEach(q => {
|
||
const partName = q.partName || ''
|
||
if (partName) {
|
||
if (!partMap.has(partName)) {
|
||
partMap.set(partName, [])
|
||
}
|
||
partMap.get(partName)!.push(q)
|
||
} else {
|
||
defaultPart.push(q)
|
||
}
|
||
})
|
||
|
||
// 按分区排序
|
||
const sortedParts = Array.from(partMap.entries())
|
||
.sort((a, b) => {
|
||
const partA = partitions.value.find(p => p.name === a[0])
|
||
const partB = partitions.value.find(p => p.name === b[0])
|
||
const sortA = partA?.sort ?? 0
|
||
const sortB = partB?.sort ?? 0
|
||
return sortA - sortB
|
||
})
|
||
|
||
// 构建分区列表
|
||
const result: typeof partitions.value = []
|
||
|
||
// 添加默认分区
|
||
result.push({
|
||
name: '',
|
||
sort: 0,
|
||
selected: true,
|
||
questions: defaultPart.map((q, i) => ({ ...q, _index: i, _checked: false }))
|
||
})
|
||
|
||
// 添加其他分区
|
||
sortedParts.forEach(([name, qs]) => {
|
||
result.push({
|
||
name,
|
||
sort: partitions.value.find(p => p.name === name)?.sort ?? 0,
|
||
selected: true,
|
||
questions: qs.map((q, i) => ({ ...q, _index: i, _checked: false }))
|
||
})
|
||
})
|
||
|
||
return result
|
||
}
|
||
|
||
/** 加载问题列表 */
|
||
const getList = async () => {
|
||
if (!props.questionnaireId) return
|
||
loading.value = true
|
||
try {
|
||
const data = await QuestionApi.getQuestionPage({
|
||
pageNo: 1,
|
||
pageSize: 200,
|
||
questionnaireId: props.questionnaireId
|
||
})
|
||
list.value = data.list
|
||
partitions.value = extractPartitions(data.list)
|
||
|
||
// 每次都重新初始化分区管理列表(切换问卷时需要重载)
|
||
allPartList.value = []
|
||
|
||
// 添加默认分区
|
||
allPartList.value.push({
|
||
id: 'default',
|
||
name: '',
|
||
sort: 0,
|
||
isDefault: true
|
||
})
|
||
|
||
// 添加已存在的分区
|
||
const existingNames = new Set<string>()
|
||
partitions.value.forEach(p => {
|
||
if (p.name && !existingNames.has(p.name)) {
|
||
existingNames.add(p.name)
|
||
allPartList.value.push({
|
||
id: `part_${p.name}`,
|
||
name: p.name,
|
||
sort: p.sort,
|
||
isDefault: false
|
||
})
|
||
}
|
||
})
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/** 监听问卷ID变化 */
|
||
watch(
|
||
() => props.questionnaireId,
|
||
(val) => {
|
||
if (!val) {
|
||
list.value = []
|
||
partitions.value = []
|
||
return
|
||
}
|
||
getList()
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
/** 打开表单 */
|
||
const openForm = (type: string, id?: number) => {
|
||
if (!props.questionnaireId) {
|
||
message.error('请选择一个问卷')
|
||
return
|
||
}
|
||
// 传入可用分区列表
|
||
formRef.value.open(type, id, props.questionnaireId, getPartitionNames())
|
||
}
|
||
|
||
/** 获取分区名称列表 */
|
||
const getPartitionNames = () => {
|
||
return allPartList.value
|
||
.filter(p => !p.isDefault && p.name)
|
||
.map((p, index) => ({
|
||
label: p.name,
|
||
value: p.name,
|
||
sort: index
|
||
}))
|
||
.sort((a, b) => a.sort - b.sort)
|
||
}
|
||
|
||
/** 打开分区管理弹窗 */
|
||
const openPartDialog = () => {
|
||
// 确保默认分区存在且在第一位
|
||
if (allPartList.value.length === 0 || !allPartList.value[0]?.isDefault) {
|
||
// 添加默认分区(第一个)
|
||
allPartList.value.unshift({
|
||
id: 'default',
|
||
name: '',
|
||
sort: 0,
|
||
isDefault: true
|
||
})
|
||
}
|
||
// 确保其他分区不是默认分区
|
||
for (let i = 1; i < allPartList.value.length; i++) {
|
||
allPartList.value[i].isDefault = false
|
||
}
|
||
partDialogVisible.value = true
|
||
}
|
||
|
||
/** 添加分区 */
|
||
const addPartition = () => {
|
||
const newSort = allPartList.value.length > 0
|
||
? Math.max(...allPartList.value.map(p => p.sort || 0)) + 1
|
||
: 0
|
||
allPartList.value.push({
|
||
id: `part_${Date.now()}`,
|
||
name: '',
|
||
sort: newSort,
|
||
isDefault: false
|
||
})
|
||
}
|
||
|
||
/** 删除分区 */
|
||
const removePartition = (index: number) => {
|
||
allPartList.value.splice(index, 1)
|
||
}
|
||
|
||
/** 编辑分区名称 */
|
||
const editPartition = (partition: typeof partitions.value[0]) => {
|
||
// 打开分区管理弹窗
|
||
openPartDialog()
|
||
// 延迟滚动到对应分区并聚焦输入框
|
||
nextTick(() => {
|
||
const partIndex = allPartList.value.findIndex(p => p.name === partition.name)
|
||
if (partIndex > 0) { // 跳过默认分区(index=0)
|
||
const element = document.querySelector(`.part-manage-item:nth-child(${partIndex + 1}) .el-input__wrapper`)
|
||
if (element) {
|
||
(element as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||
// 尝试聚焦输入框
|
||
const input = element.querySelector('input')
|
||
if (input) {
|
||
input.focus()
|
||
}
|
||
}
|
||
}
|
||
})
|
||
// 提示用户
|
||
message.info(`请在弹窗中编辑分区"${partition.name}"的名称`)
|
||
}
|
||
|
||
/** 删除分区 */
|
||
const deletePartition = async (partition: typeof partitions.value[0]) => {
|
||
try {
|
||
await message.delConfirm(`确定删除分区"${partition.name}"吗?该分区下的问题将移到默认分区`)
|
||
// 更新问题的分区为默认分区
|
||
for (const q of partition.questions) {
|
||
await QuestionApi.updateQuestion({ ...q, id: q.id, partName: '' })
|
||
}
|
||
await getList()
|
||
message.success('删除成功')
|
||
} catch {}
|
||
}
|
||
|
||
/** 保存分区设置 */
|
||
const savePartitions = async () => {
|
||
try {
|
||
// 验证分区名称唯一性(非默认分区)
|
||
const names = allPartList.value.filter(p => !p.isDefault).map(p => p.name).filter(n => n)
|
||
if (new Set(names).size !== names.length) {
|
||
message.error('分区名称不能重复')
|
||
return
|
||
}
|
||
|
||
// 验证非默认分区必须有名称
|
||
const emptyParts = allPartList.value.filter(p => !p.isDefault && !p.name)
|
||
if (emptyParts.length > 0) {
|
||
message.error('请为所有分区输入名称')
|
||
return
|
||
}
|
||
|
||
// 收集所有需要更新的问题
|
||
const updates: Array<{ id: number; partName?: string; partSort?: number; sort?: number }> = []
|
||
for (let i = 0; i < allPartList.value.length; i++) {
|
||
const part = allPartList.value[i]
|
||
if (!part.isDefault && part.name) {
|
||
for (const p of partitions.value) {
|
||
if (p.name === part.name) {
|
||
p.questions.forEach((q, sortIndex) => {
|
||
updates.push({
|
||
id: q.id!,
|
||
partName: part.name,
|
||
partSort: i,
|
||
sort: sortIndex
|
||
})
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 批量更新
|
||
await QuestionApi.batchUpdate({ questions: updates })
|
||
await getList()
|
||
partDialogVisible.value = false
|
||
message.success('保存成功')
|
||
} catch (e) {
|
||
message.error('保存失败')
|
||
}
|
||
}
|
||
|
||
/** 分区拖拽排序完成 */
|
||
const onPartitionDragEnd = async () => {
|
||
// 收集所有需要更新的问题
|
||
const updates: Array<{ id: number; partName?: string; partSort?: number; sort?: number }> = []
|
||
for (let i = 0; i < partitions.value.length; i++) {
|
||
const part = partitions.value[i]
|
||
if (part.name) {
|
||
part.questions.forEach((q, sortIndex) => {
|
||
updates.push({
|
||
id: q.id!,
|
||
partName: part.name,
|
||
partSort: i,
|
||
sort: sortIndex
|
||
})
|
||
})
|
||
}
|
||
}
|
||
// 批量更新
|
||
await QuestionApi.batchUpdate({ questions: updates })
|
||
message.success('分区排序已更新')
|
||
}
|
||
|
||
/** 问题拖拽排序完成 */
|
||
const onQuestionDragEnd = async (partition: typeof partitions.value[0]) => {
|
||
// 收集当前分区内需要更新的问题
|
||
const updates: Array<{ id: number; partName?: string; partSort?: number; sort?: number }> = []
|
||
partition.questions.forEach((q, i) => {
|
||
updates.push({
|
||
id: q.id!,
|
||
partName: partition.name || undefined,
|
||
partSort: partition.sort,
|
||
sort: i
|
||
})
|
||
})
|
||
// 批量更新
|
||
await QuestionApi.batchUpdate({ questions: updates })
|
||
message.success('问题排序已更新')
|
||
}
|
||
|
||
/** 切换分区显示/隐藏 */
|
||
const togglePartQuestions = (partition: typeof partitions.value[0], selected: boolean) => {
|
||
partition.questions.forEach(q => {
|
||
q._checked = selected
|
||
})
|
||
}
|
||
|
||
/** 切换问题选中状态 */
|
||
const toggleQuestionCheck = (question: Question & { _index: number; _checked: boolean }, val: boolean, partition: typeof partitions.value[0]) => {
|
||
if (val) {
|
||
if (!checkedIds.value.includes(question.id!)) {
|
||
checkedIds.value.push(question.id!)
|
||
}
|
||
} else {
|
||
checkedIds.value = checkedIds.value.filter(id => id !== question.id)
|
||
}
|
||
}
|
||
|
||
/** 删除问题 */
|
||
const handleDelete = async (id: number) => {
|
||
try {
|
||
await message.delConfirm()
|
||
await QuestionApi.deleteQuestion(id)
|
||
message.success(t('common.delSuccess'))
|
||
await getList()
|
||
} catch {}
|
||
}
|
||
|
||
/** 批量删除 */
|
||
const handleDeleteBatch = async () => {
|
||
try {
|
||
await message.delConfirm()
|
||
await QuestionApi.deleteQuestionList(checkedIds.value)
|
||
checkedIds.value = []
|
||
message.success(t('common.delSuccess'))
|
||
await getList()
|
||
} catch {}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.question-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.header-left {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.header-right {
|
||
color: #909399;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.total-count {
|
||
padding: 4px 12px;
|
||
background: #f4f4f5;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.parts-container {
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.parts-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.part-group {
|
||
border-bottom: 1px solid #e4e7ed;
|
||
}
|
||
|
||
.part-group:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.part-group.is-default {
|
||
background: #fafafa;
|
||
}
|
||
|
||
.part-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px 16px;
|
||
background: linear-gradient(135deg, #f0f7ff 0%, #e8f4fd 100%);
|
||
border-bottom: 1px solid #e4e7ed;
|
||
}
|
||
|
||
.part-drag-handle {
|
||
cursor: grab;
|
||
color: #c0c4cc;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.part-drag-handle:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.drag-icon {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.part-title {
|
||
font-weight: 600;
|
||
color: #1a5cb8;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.part-count {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.part-actions {
|
||
margin-left: auto;
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.questions-table {
|
||
min-height: 50px;
|
||
}
|
||
|
||
.question-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.question-row:hover {
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
.question-row.is-hidden {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.question-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.ghost-row {
|
||
background: #f0f9eb;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.question-drag-handle {
|
||
cursor: grab;
|
||
color: #c0c4cc;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.question-drag-handle:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.question-checkbox {
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.question-index {
|
||
width: 30px;
|
||
text-align: center;
|
||
color: #909399;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.question-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.question-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.title-text {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.type-badge {
|
||
font-size: 12px;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.type-1 { background: #e6f7ff; color: #1890ff; }
|
||
.type-2 { background: #f6ffed; color: #52c41a; }
|
||
.type-3 { background: #fff7e6; color: #fa8c16; }
|
||
.type-4 { background: #f9f0ff; color: #722ed1; }
|
||
.type-5 { background: #fff1f0; color: #f5222d; }
|
||
.type-6 { background: #e6fffb; color: #13c2c2; }
|
||
|
||
.help-text {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-top: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.question-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
min-width: 150px;
|
||
}
|
||
|
||
.meta-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
color: #606266;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.question-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.part-manage-list {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.part-manage-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px;
|
||
margin-bottom: 8px;
|
||
background: #f5f7fa;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.drag-handle {
|
||
cursor: move;
|
||
color: #c0c4cc;
|
||
}
|
||
</style>
|