|
|
|
|
@ -1,91 +1,831 @@
|
|
|
|
|
<template>
|
|
|
|
|
<el-dialog :visible.sync="visible" fullscreen :show-close="false" class="survey-dialog">
|
|
|
|
|
<el-dialog :visible.sync="dialogVisible" fullscreen :show-close="false" class="survey-dialog">
|
|
|
|
|
<div class="results-header">
|
|
|
|
|
<div class="header-content">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="survey-title">{{ surveyData.title }}</div>
|
|
|
|
|
<div class="survey-title">{{ surveyData ? surveyData.title : '' }}</div>
|
|
|
|
|
<div class="survey-meta">
|
|
|
|
|
<span class="meta-item"><i class="el-icon-date"></i> 创建时间:{{ surveyData.createTime }}</span>
|
|
|
|
|
<span class="meta-item"><i class="el-icon-time"></i> 截止时间:{{ surveyData.deadline }}</span>
|
|
|
|
|
<span class="meta-item" v-if="surveyData && surveyData.start_time">
|
|
|
|
|
<i class="el-icon-date"></i> 开始时间:{{ surveyData.start_time }}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="meta-item" v-if="surveyData && surveyData.end_time">
|
|
|
|
|
<i class="el-icon-time"></i> 截止时间:{{ surveyData.end_time }}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="meta-item" v-if="surveyData && surveyData.course">
|
|
|
|
|
<i class="el-icon-link"></i> 关联课程:{{ surveyData.course.name }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-button @click="$emit('close')">关闭</el-button>
|
|
|
|
|
<el-button @click="handleClose" icon="el-icon-close">关闭</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 标签页 -->
|
|
|
|
|
<div class="tabs-container">
|
|
|
|
|
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
|
|
|
|
<el-tab-pane label="统计分析" name="statistics"></el-tab-pane>
|
|
|
|
|
<el-tab-pane label="回复列表" name="responses"></el-tab-pane>
|
|
|
|
|
</el-tabs>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="results-main">
|
|
|
|
|
<div class="stats-overview">
|
|
|
|
|
<div class="results-main" v-if="surveyData">
|
|
|
|
|
<!-- 统计概览 -->
|
|
|
|
|
<div class="stats-overview" v-if="activeTab === 'statistics'">
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
<div class="stat-icon" style="background:#3498db"><i class="el-icon-user"></i></div>
|
|
|
|
|
<div class="stat-number">{{ surveyData.responses }}</div>
|
|
|
|
|
<div class="stat-icon" style="background:#3498db">
|
|
|
|
|
<i class="el-icon-user"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-number">{{ surveyData.responses || surveyData.course_content_evaluation_forms_count || 0 }}</div>
|
|
|
|
|
<div class="stat-label">回复数</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
<div class="stat-icon" style="background:#f1c40f"><i class="el-icon-edit"></i></div>
|
|
|
|
|
<div class="stat-number">{{ surveyData.questions.length }}</div>
|
|
|
|
|
<div class="stat-icon" style="background:#f1c40f">
|
|
|
|
|
<i class="el-icon-edit"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-number">{{ surveyData.questionsCount || surveyData.course_content_evaluation_asks_count || (questions && questions.length) || 0 }}</div>
|
|
|
|
|
<div class="stat-label">题目数</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
<div class="stat-icon" style="background:#2ecc71"><i class="el-icon-star-on"></i></div>
|
|
|
|
|
<div class="stat-number">{{ surveyData.avgScore || '-' }}</div>
|
|
|
|
|
<div class="stat-label">平均分</div>
|
|
|
|
|
<div class="stat-card" v-if="avgRateScore !== null">
|
|
|
|
|
<div class="stat-icon" style="background:#2ecc71">
|
|
|
|
|
<i class="el-icon-star-on"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-number">{{ avgRateScore.toFixed(1) }}</div>
|
|
|
|
|
<div class="stat-label">平均评分</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-for="(q, idx) in surveyData.questions" :key="q.id" class="question-analysis">
|
|
|
|
|
<div class="question-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="question-title">{{ idx+1 }}. {{ q.title }}</div>
|
|
|
|
|
<div class="question-meta">
|
|
|
|
|
<span>题型:{{ typeText(q.type) }}</span>
|
|
|
|
|
|
|
|
|
|
<!-- 统计分析内容 -->
|
|
|
|
|
<div v-if="activeTab === 'statistics'">
|
|
|
|
|
<!-- 题目分析 -->
|
|
|
|
|
<div v-if="questions && questions.length > 0">
|
|
|
|
|
<div v-for="(q, idx) in questions" :key="q.id" class="question-analysis">
|
|
|
|
|
<div class="question-header">
|
|
|
|
|
<div class="question-title-wrapper">
|
|
|
|
|
<div class="question-title">
|
|
|
|
|
<span class="question-number">{{ idx + 1 }}.</span>
|
|
|
|
|
{{ q.name }}
|
|
|
|
|
<span v-if="q.rule && q.rule.includes('required')" class="required-mark">*</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="question-meta">
|
|
|
|
|
<span class="meta-tag">题型:{{ getQuestionTypeText(q.edit_input) }}</span>
|
|
|
|
|
<span v-if="q.course_content" class="meta-tag">
|
|
|
|
|
<i class="el-icon-document"></i> {{ q.course_content.theme }}
|
|
|
|
|
</span>
|
|
|
|
|
<span v-if="q.course_content && q.course_content.teacher" class="meta-tag">
|
|
|
|
|
<i class="el-icon-user"></i> {{ q.course_content.teacher.name }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 单选题/多选题统计 -->
|
|
|
|
|
<div v-if="q.edit_input === 'radio' || q.edit_input === 'checkbox'" class="question-result">
|
|
|
|
|
<div v-if="q.select_item && q.select_item.length > 0">
|
|
|
|
|
<el-table :data="getOptionStats(q)" style="width: 100%;" border>
|
|
|
|
|
<el-table-column prop="option" label="选项" width="300"></el-table-column>
|
|
|
|
|
<el-table-column label="选择人数" width="120">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
{{ scope.row.count }}
|
|
|
|
|
<span v-if="totalResponses > 0" class="percentage">
|
|
|
|
|
({{ ((scope.row.count / totalResponses) * 100).toFixed(1) }}%)
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="占比" width="200">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
<el-progress
|
|
|
|
|
:percentage="totalResponses > 0 ? (scope.row.count / totalResponses) * 100 : 0"
|
|
|
|
|
:stroke-width="20"
|
|
|
|
|
:show-text="false"
|
|
|
|
|
></el-progress>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
<el-empty v-else description="暂无选项数据" :image-size="100"></el-empty>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 多维度题目统计 -->
|
|
|
|
|
<div v-else-if="q.edit_input === 'multi_dimension'" class="question-result">
|
|
|
|
|
<div v-if="q.dimensions && q.dimensions.length > 0">
|
|
|
|
|
<div v-for="(dim, dimIdx) in q.dimensions" :key="dimIdx" class="dimension-stats">
|
|
|
|
|
<div class="dimension-title">
|
|
|
|
|
<span v-if="dim.need_fill" class="required-mark">*</span>
|
|
|
|
|
{{ dim.name || dim.field }}
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="dim.select_item && dim.select_item.length > 0">
|
|
|
|
|
<el-table :data="getDimensionOptionStats(q, dim)" style="width: 100%;" border size="small">
|
|
|
|
|
<el-table-column prop="option" label="选项" width="250"></el-table-column>
|
|
|
|
|
<el-table-column label="选择人数" width="100">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
{{ scope.row.count }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="占比" width="150">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
<el-progress
|
|
|
|
|
:percentage="totalResponses > 0 ? (scope.row.count / totalResponses) * 100 : 0"
|
|
|
|
|
:stroke-width="16"
|
|
|
|
|
:show-text="false"
|
|
|
|
|
></el-progress>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
<el-empty v-else description="暂无选项" :image-size="80"></el-empty>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 文本题/文本域题 -->
|
|
|
|
|
<div v-else-if="q.edit_input === 'text' || q.edit_input === 'textarea'" class="question-result">
|
|
|
|
|
<el-alert
|
|
|
|
|
:title="`共收到 ${totalResponses} 条文本回答`"
|
|
|
|
|
type="info"
|
|
|
|
|
:closable="false"
|
|
|
|
|
show-icon>
|
|
|
|
|
</el-alert>
|
|
|
|
|
<!-- <div class="text-answers-preview" v-if="totalResponses > 0">
|
|
|
|
|
<p class="text-hint">文本答案预览功能开发中,当前仅显示回答数量</p>
|
|
|
|
|
</div> -->
|
|
|
|
|
<!-- <el-empty v-else description="暂无回答" :image-size="100"></el-empty> -->
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 日期/日期时间 -->
|
|
|
|
|
<div v-else-if="q.edit_input === 'date' || q.edit_input === 'datetime'" class="question-result">
|
|
|
|
|
<el-alert
|
|
|
|
|
:title="`共收到 ${totalResponses} 条日期回答`"
|
|
|
|
|
type="info"
|
|
|
|
|
:closable="false"
|
|
|
|
|
show-icon>
|
|
|
|
|
</el-alert>
|
|
|
|
|
<el-empty v-if="totalResponses === 0" description="暂无回答" :image-size="100"></el-empty>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 未知类型 -->
|
|
|
|
|
<div v-else class="question-result">
|
|
|
|
|
<el-alert
|
|
|
|
|
title="未知的题目类型"
|
|
|
|
|
type="warning"
|
|
|
|
|
:closable="false"
|
|
|
|
|
show-icon>
|
|
|
|
|
</el-alert>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="q.type==='single' || q.type==='multi'">
|
|
|
|
|
<el-table :data="q.options.map((opt,i)=>({option:opt,count:Math.floor(Math.random()*20+1)}))" style="width: 100%;">
|
|
|
|
|
<el-table-column prop="option" label="选项" />
|
|
|
|
|
<el-table-column prop="count" label="选择人数" />
|
|
|
|
|
|
|
|
|
|
<!-- 无题目 -->
|
|
|
|
|
<el-empty v-else description="该问卷暂无题目" :image-size="150"></el-empty>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 回复列表内容 -->
|
|
|
|
|
<div v-if="activeTab === 'responses'" class="responses-container">
|
|
|
|
|
<div v-loading="responsesLoading">
|
|
|
|
|
<el-table
|
|
|
|
|
:data="responseList"
|
|
|
|
|
style="width: 100%;"
|
|
|
|
|
border
|
|
|
|
|
v-if="responseList && responseList.length > 0">
|
|
|
|
|
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
|
|
|
|
|
<el-table-column prop="user_name" label="姓名" width="150" align="center">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
{{ scope.row.user && scope.row.user.name ? scope.row.user.name : '-' }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="user_no" label="学号" width="150" align="center">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
{{ scope.row.user && scope.row.user.no ? scope.row.user.no : '-' }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="user_company" label="公司" min-width="200" show-overflow-tooltip>
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
{{ scope.row.user && scope.row.user.company_name ? scope.row.user.company_name : '-' }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="user_position" label="职务" width="150" show-overflow-tooltip>
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
{{ scope.row.user && scope.row.user.company_position ? scope.row.user.company_position : '-' }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="created_at" label="提交时间" width="180" align="center">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
{{ scope.row.created_at || '-' }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="操作" width="120" align="center" fixed="right">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
<el-button type="primary" size="mini" @click="viewResponseDetail(scope.row)">查看</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
<el-empty v-else description="暂无回复数据" :image-size="150"></el-empty>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else-if="q.type==='rate'">
|
|
|
|
|
<el-rate v-model="q.rateMax" :max="10" show-text text-color="#ff9900" disabled />
|
|
|
|
|
<div style="font-size:12px;color:#888;">评分题,用户可打分</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 回复详情对话框 -->
|
|
|
|
|
<el-dialog
|
|
|
|
|
title="答题详情"
|
|
|
|
|
:visible.sync="detailDialogVisible"
|
|
|
|
|
width="80%"
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
append-to-body
|
|
|
|
|
:z-index="3000"
|
|
|
|
|
class="response-detail-dialog">
|
|
|
|
|
<div v-if="currentResponse" class="response-detail-content">
|
|
|
|
|
<div class="respondent-info">
|
|
|
|
|
<el-descriptions :column="3" border>
|
|
|
|
|
<el-descriptions-item label="姓名">
|
|
|
|
|
{{ currentResponse.user && currentResponse.user.name ? currentResponse.user.name : '-' }}
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="学号">
|
|
|
|
|
{{ currentResponse.user && currentResponse.user.no ? currentResponse.user.no : '-' }}
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="提交时间">
|
|
|
|
|
{{ currentResponse.created_at || '-' }}
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="公司" :span="2">
|
|
|
|
|
{{ currentResponse.user && currentResponse.user.company_name ? currentResponse.user.company_name : '-' }}
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="职务">
|
|
|
|
|
{{ currentResponse.user && currentResponse.user.company_position ? currentResponse.user.company_position : '-' }}
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else-if="q.type==='text'">
|
|
|
|
|
<el-empty description="文本题答案统计略" />
|
|
|
|
|
|
|
|
|
|
<div class="answers-list">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(q, idx) in questions"
|
|
|
|
|
:key="q.id"
|
|
|
|
|
class="answer-item">
|
|
|
|
|
<div class="answer-question">
|
|
|
|
|
<span class="question-number">{{ idx + 1 }}.</span>
|
|
|
|
|
<span class="question-text">{{ q.name }}</span>
|
|
|
|
|
<span v-if="q.rule && q.rule.includes('required')" class="required-mark">*</span>
|
|
|
|
|
<span class="question-type">({{ getQuestionTypeText(q.edit_input) }})</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="answer-content">
|
|
|
|
|
<div v-if="getAnswerForQuestion(q.id) !== null && getAnswerForQuestion(q.id) !== undefined">
|
|
|
|
|
<!-- 单选题 -->
|
|
|
|
|
<div v-if="q.edit_input === 'radio'">
|
|
|
|
|
<span class="answer-value">{{ getAnswerForQuestion(q.id) || '未填写' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 多选题 -->
|
|
|
|
|
<div v-else-if="q.edit_input === 'checkbox'">
|
|
|
|
|
<span v-if="Array.isArray(getAnswerForQuestion(q.id))" class="answer-value">
|
|
|
|
|
{{ getAnswerForQuestion(q.id).join('、') || '未填写' }}
|
|
|
|
|
</span>
|
|
|
|
|
<span v-else class="answer-value">{{ getAnswerForQuestion(q.id) || '未填写' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 多维度题 -->
|
|
|
|
|
<div v-else-if="q.edit_input === 'multi_dimension'">
|
|
|
|
|
<div v-if="typeof getAnswerForQuestion(q.id) === 'object' && getAnswerForQuestion(q.id)">
|
|
|
|
|
<div v-for="dim in q.dimensions" :key="dim.field || dim.name" class="dimension-answer">
|
|
|
|
|
<span class="dimension-label">{{ dim.name || dim.field }}:</span>
|
|
|
|
|
<span class="dimension-value">{{ getAnswerForQuestion(q.id)[dim.field] || '未填写' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-else class="answer-value">未填写</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 文本题、文本域、日期等 -->
|
|
|
|
|
<div v-else>
|
|
|
|
|
<span class="answer-value">{{ getAnswerForQuestion(q.id) || '未填写' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-else class="answer-empty">未填写</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div slot="footer">
|
|
|
|
|
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
import { index as formResponseIndex, show as formResponseShow } from '@/api/survey/evaluationForm.js'
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
name: 'SurveyResultsDialog',
|
|
|
|
|
props: {
|
|
|
|
|
visible: Boolean,
|
|
|
|
|
surveyData: Object
|
|
|
|
|
visible: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: false
|
|
|
|
|
},
|
|
|
|
|
surveyData: {
|
|
|
|
|
type: Object,
|
|
|
|
|
default: null
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
activeTab: 'statistics',
|
|
|
|
|
responseList: [],
|
|
|
|
|
responsesLoading: false,
|
|
|
|
|
detailDialogVisible: false,
|
|
|
|
|
currentResponse: null,
|
|
|
|
|
responseAnswers: {} // 存储解析后的答案数据
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
dialogVisible: {
|
|
|
|
|
get() {
|
|
|
|
|
return this.visible;
|
|
|
|
|
},
|
|
|
|
|
set(val) {
|
|
|
|
|
this.$emit('update:visible', val);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
questions() {
|
|
|
|
|
return this.surveyData && this.surveyData.questions ? this.surveyData.questions : [];
|
|
|
|
|
},
|
|
|
|
|
totalResponses() {
|
|
|
|
|
return this.surveyData ? (this.surveyData.responses || this.surveyData.course_content_evaluation_forms_count || 0) : 0;
|
|
|
|
|
},
|
|
|
|
|
avgRateScore() {
|
|
|
|
|
// 计算所有评分题的平均分(如果有评分题的话)
|
|
|
|
|
// 这里需要根据实际数据结构来计算
|
|
|
|
|
// 暂时返回null,表示暂无评分数据
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
watch: {
|
|
|
|
|
visible(newVal) {
|
|
|
|
|
if (newVal && this.surveyData) {
|
|
|
|
|
// 对话框打开时,如果切换到回复列表,自动加载数据
|
|
|
|
|
if (this.activeTab === 'responses') {
|
|
|
|
|
this.loadResponseList()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 关闭时重置
|
|
|
|
|
this.activeTab = 'statistics'
|
|
|
|
|
this.responseList = []
|
|
|
|
|
this.currentResponse = null
|
|
|
|
|
this.responseAnswers = {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
typeText(type) {
|
|
|
|
|
return { single: '单选题', multi: '多选题', text: '文本题', rate: '评分题' }[type] || type;
|
|
|
|
|
handleClose() {
|
|
|
|
|
this.dialogVisible = false;
|
|
|
|
|
},
|
|
|
|
|
handleTabClick(tab) {
|
|
|
|
|
if (tab.name === 'responses' && this.responseList.length === 0 && !this.responsesLoading) {
|
|
|
|
|
this.loadResponseList()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
async loadResponseList() {
|
|
|
|
|
if (!this.surveyData || !this.surveyData.id) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.responsesLoading = true
|
|
|
|
|
try {
|
|
|
|
|
const res = await formResponseIndex({
|
|
|
|
|
page: 1,
|
|
|
|
|
page_size: 999,
|
|
|
|
|
show_relation: ['user'],
|
|
|
|
|
filter: [{
|
|
|
|
|
key: 'course_content_evaluation_id',
|
|
|
|
|
op: 'eq',
|
|
|
|
|
value: this.surveyData.id
|
|
|
|
|
}],
|
|
|
|
|
sort_name: 'created_at',
|
|
|
|
|
sort_type: 'DESC'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.responseList = res.data || []
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取回复列表失败:', error)
|
|
|
|
|
this.$message.error('获取回复列表失败,请重试')
|
|
|
|
|
this.responseList = []
|
|
|
|
|
} finally {
|
|
|
|
|
this.responsesLoading = false
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
async viewResponseDetail(response) {
|
|
|
|
|
this.responsesLoading = true
|
|
|
|
|
try {
|
|
|
|
|
// 获取详情
|
|
|
|
|
const res = await formResponseShow({
|
|
|
|
|
id: response.id,
|
|
|
|
|
show_relation: ['user']
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.currentResponse = res || response
|
|
|
|
|
// 解析答案数据
|
|
|
|
|
this.parseResponseAnswers(this.currentResponse)
|
|
|
|
|
this.detailDialogVisible = true
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取回复详情失败:', error)
|
|
|
|
|
this.$message.error('获取回复详情失败,请重试')
|
|
|
|
|
} finally {
|
|
|
|
|
this.responsesLoading = false
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
parseResponseAnswers(response) {
|
|
|
|
|
// 解析 data 字段(可能是 JSON 字符串)
|
|
|
|
|
this.responseAnswers = {}
|
|
|
|
|
|
|
|
|
|
if (!response || !response.data) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let answerData = null
|
|
|
|
|
try {
|
|
|
|
|
// 尝试解析 JSON 字符串
|
|
|
|
|
if (typeof response.data === 'string') {
|
|
|
|
|
answerData = JSON.parse(response.data)
|
|
|
|
|
} else {
|
|
|
|
|
answerData = response.data
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('解析答案数据失败:', e)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!answerData || typeof answerData !== 'object') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 将答案数据转换为以题目ID为键的对象
|
|
|
|
|
this.questions.forEach(q => {
|
|
|
|
|
if (answerData[q.id] !== undefined && answerData[q.id] !== null) {
|
|
|
|
|
// 如果是多维度题目,需要特殊处理
|
|
|
|
|
if (q.edit_input === 'multi_dimension') {
|
|
|
|
|
let dimensionValue = answerData[q.id]
|
|
|
|
|
if (typeof dimensionValue === 'string') {
|
|
|
|
|
try {
|
|
|
|
|
dimensionValue = JSON.parse(dimensionValue)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('解析多维度答案失败:', e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.$set(this.responseAnswers, q.id, dimensionValue || {})
|
|
|
|
|
} else {
|
|
|
|
|
this.$set(this.responseAnswers, q.id, answerData[q.id])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
getAnswerForQuestion(questionId) {
|
|
|
|
|
return this.responseAnswers[questionId] !== undefined ? this.responseAnswers[questionId] : null
|
|
|
|
|
},
|
|
|
|
|
getQuestionTypeText(type) {
|
|
|
|
|
const typeMap = {
|
|
|
|
|
'radio': '单选题',
|
|
|
|
|
'checkbox': '多选题',
|
|
|
|
|
'text': '文本题',
|
|
|
|
|
'textarea': '文本域',
|
|
|
|
|
'date': '日期',
|
|
|
|
|
'datetime': '日期时间',
|
|
|
|
|
'multi_dimension': '多维度题'
|
|
|
|
|
};
|
|
|
|
|
return typeMap[type] || type || '未知类型';
|
|
|
|
|
},
|
|
|
|
|
getOptionStats(question) {
|
|
|
|
|
// 统计每个选项被选择的数量
|
|
|
|
|
// 这里需要从实际的填写结果数据中统计
|
|
|
|
|
// 暂时返回模拟数据
|
|
|
|
|
if (!question.select_item || !Array.isArray(question.select_item)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return question.select_item.map(item => {
|
|
|
|
|
// TODO: 从实际的填写结果中统计每个选项的选择次数
|
|
|
|
|
// 这里先返回0,表示暂无统计数据
|
|
|
|
|
return {
|
|
|
|
|
option: item.value || item.label || item.name || '',
|
|
|
|
|
count: 0
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
getDimensionOptionStats(question, dimension) {
|
|
|
|
|
// 统计多维度题目每个维度的选项选择数量
|
|
|
|
|
if (!dimension.select_item || !Array.isArray(dimension.select_item)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dimension.select_item.map(item => {
|
|
|
|
|
// TODO: 从实际的填写结果中统计每个选项的选择次数
|
|
|
|
|
return {
|
|
|
|
|
option: item.value || item.label || item.name || '',
|
|
|
|
|
count: 0
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.survey-dialog >>> .el-dialog__body { padding:0; height: 70vh;
|
|
|
|
|
overflow: scroll;}
|
|
|
|
|
.results-header { background: #fff; padding: 30px; border-bottom: 1px solid #e9ecef; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
|
|
|
.header-content { max-width: 1400px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; }
|
|
|
|
|
.survey-title { font-size: 28px; font-weight: 700; color: #2c3e50; margin-bottom: 12px; }
|
|
|
|
|
.survey-meta { display: flex; gap: 32px; flex-wrap: wrap; margin-bottom: 20px; }
|
|
|
|
|
.meta-item { display: flex; align-items: center; gap: 8px; font-size: 14px; color: #6c757d; }
|
|
|
|
|
.results-main { max-width: 1400px; margin: 0 auto; padding: 30px; }
|
|
|
|
|
.stats-overview { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
|
|
|
|
.stat-card { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); text-align: center; border: 1px solid #e9ecef; }
|
|
|
|
|
.stat-icon { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px; font-size: 20px; color: #fff; }
|
|
|
|
|
.stat-number { font-size: 32px; font-weight: 700; color: #2c3e50; margin-bottom: 4px; }
|
|
|
|
|
.stat-label { font-size: 14px; color: #6c757d; }
|
|
|
|
|
.question-analysis { background: #fff; border-radius: 12px; padding: 30px; margin-bottom: 30px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
|
|
|
.question-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; padding-bottom: 16px; border-bottom: 1px solid #e9ecef; }
|
|
|
|
|
.question-title { font-size: 18px; font-weight: 600; color: #2c3e50; margin-bottom: 8px; }
|
|
|
|
|
.question-meta { display: flex; gap: 16px; font-size: 14px; color: #6c757d; }
|
|
|
|
|
.survey-dialog >>> .el-dialog__body {
|
|
|
|
|
padding: 0;
|
|
|
|
|
height: calc(100vh - 55px);
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.results-header {
|
|
|
|
|
background: #fff;
|
|
|
|
|
padding: 30px;
|
|
|
|
|
border-bottom: 1px solid #e9ecef;
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
|
|
position: sticky;
|
|
|
|
|
top: 0;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-content {
|
|
|
|
|
max-width: 1400px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tabs-container {
|
|
|
|
|
max-width: 1400px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
border-bottom: 1px solid #e9ecef;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.survey-title {
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.survey-meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 32px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.meta-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #6c757d;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.results-main {
|
|
|
|
|
max-width: 1400px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
padding: 30px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stats-overview {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
|
|
gap: 20px;
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 24px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
text-align: center;
|
|
|
|
|
border: 1px solid #e9ecef;
|
|
|
|
|
transition: transform 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-card:hover {
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-icon {
|
|
|
|
|
width: 48px;
|
|
|
|
|
height: 48px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
margin: 0 auto 12px;
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
color: #fff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-number {
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #6c757d;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-analysis {
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 30px;
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
border: 1px solid #e9ecef;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-header {
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
padding-bottom: 16px;
|
|
|
|
|
border-bottom: 1px solid #e9ecef;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-title-wrapper {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-title {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-number {
|
|
|
|
|
color: #3498db;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
min-width: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.required-mark {
|
|
|
|
|
color: #f56c6c;
|
|
|
|
|
margin-left: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #6c757d;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.meta-tag {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-result {
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.percentage {
|
|
|
|
|
color: #909399;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
margin-left: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dimension-stats {
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dimension-stats:last-child {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dimension-title {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.text-answers-preview {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.text-hint {
|
|
|
|
|
color: #909399;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
margin: 0;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 回复列表样式 */
|
|
|
|
|
.responses-container {
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 24px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 回复详情样式 */
|
|
|
|
|
.response-detail-content {
|
|
|
|
|
max-height: 70vh;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.respondent-info {
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answers-list {
|
|
|
|
|
border-top: 1px solid #e9ecef;
|
|
|
|
|
padding-top: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-item {
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border-left: 4px solid #3498db;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-question {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-question .question-number {
|
|
|
|
|
color: #3498db;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
min-width: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-question .question-text {
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-question .question-type {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #909399;
|
|
|
|
|
font-weight: normal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-content {
|
|
|
|
|
padding-left: 32px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #606266;
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-value {
|
|
|
|
|
color: #303133;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.answer-empty {
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
font-style: italic;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dimension-answer {
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dimension-answer:last-child {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dimension-label {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #606266;
|
|
|
|
|
min-width: 100px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dimension-value {
|
|
|
|
|
color: #303133;
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 答题详情弹窗层级样式 */
|
|
|
|
|
::v-deep .response-detail-dialog {
|
|
|
|
|
z-index: 3000 !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
::v-deep .response-detail-dialog .el-dialog__wrapper {
|
|
|
|
|
z-index: 3000 !important;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|