master
lion 3 months ago
parent f31c4d2ff7
commit da4c038d40

@ -0,0 +1,40 @@
import request from "@/utils/request";
function customParamsSerializer(params) {
let result = '';
for (let key in params) {
if (params.hasOwnProperty(key)) {
if (Array.isArray(params[key])) {
params[key].forEach((item, index) => {
if (item.key) {
result += `${key}[${index}][key]=${item.key}&${key}[${index}][op]=${item.op}&${key}[${index}][value]=${item.value}&`;
} else {
result += `${key}[${index}]=${item}&`;
}
});
} else {
result += `${key}=${params[key]}&`;
}
}
}
return result.slice(0, -1);
}
export function index(params, isLoading = false) {
return request({
method: "get",
url: "/api/admin/course-content-evaluation-form/index",
params,
paramsSerializer: customParamsSerializer,
isLoading
});
}
export function show(params, isLoading = true) {
return request({
method: "get",
url: "/api/admin/course-content-evaluation-form/show",
params,
isLoading
});
}

@ -88,8 +88,8 @@
>
{{ config.name }}
</div>
</div>
</div>
</div>
<div class="system-table">
<div class="table-header">
<div class="table-cell">课程体系</div>
@ -97,15 +97,15 @@
<div class="table-cell">培养人数(去重)</div>
</div>
<div class="table-body">
<div
<div
v-for="(item, index) in currentCourseTypes"
:key="`course-${currentYearIndex}-${index}`"
class="table-row"
>
<div class="table-cell">{{ item.name }}</div>
class="table-row"
>
<div class="table-cell">{{ item.name }}</div>
<div class="table-cell">{{ item.course_periods_total }}</div>
<div class="table-cell">{{ item.course_signs_total }}</div>
</div>
</div>
</div>
<div class="table-footer" v-if="currentCourseTypesTotal">
<div class="table-row table-row-total">
@ -197,7 +197,7 @@
<span class="ranking-name">{{ item.name }}</span>
<span class="ranking-value">{{ item.value }}</span>
</div>
</div>
</div>
</div>
</div>
@ -467,7 +467,7 @@ export default {
if (!currentConfig) return null
// 使
return {
return {
name: '累计(已去重)',
course_periods_total: currentConfig.course_periods_total || 0,
course_signs_unique_total: currentConfig.course_signs_unique_total || 0
@ -518,10 +518,10 @@ export default {
handler(newVal) {
if (newVal && newVal.length > 0) {
this.currentYearIndex = 0
this.$nextTick(() => {
this.$nextTick(() => {
this.startYearConfigRotation()
})
}
})
}
},
deep: true
},
@ -605,8 +605,8 @@ export default {
valueData = JSON.parse(firstItem.value)
if (!Array.isArray(valueData)) {
valueData = []
}
} catch (e) {
}
} catch (e) {
console.error('解析配置值失败:', e)
valueData = []
}

@ -81,7 +81,15 @@
'stats-container-half': category.layout === 'half'
}">
<div class="stats-card" :class="stat.cardClass" v-for="(stat, index) in category.stats" :key="stat.key || index">
<i class="el-icon-download stats-download" @click.stop="exportStat(stat)"></i>
<div class="stats-actions">
<i class="el-icon-download stats-download" @click.stop="exportStat(stat)"></i>
<i
v-if="hasMetadata(stat.key)"
class="el-icon-info stats-info"
@click.stop="showMetadataInfo(stat.key, stat.label)"
title="查看统计规则"
></i>
</div>
<div class="stats-icon">
<i :class="stat.icon"></i>
</div>
@ -108,7 +116,15 @@
'stats-container-half': category.layout === 'half'
}">
<div class="stats-card" :class="stat.cardClass" v-for="(stat, index) in category.stats" :key="stat.key || index">
<i class="el-icon-download stats-download" @click.stop="exportStat(stat)"></i>
<div class="stats-actions">
<i class="el-icon-download stats-download" @click.stop="exportStat(stat)"></i>
<i
v-if="hasMetadata(stat.key)"
class="el-icon-info stats-info"
@click.stop="showMetadataInfo(stat.key, stat.label)"
title="查看统计规则"
></i>
</div>
<div class="stats-icon">
<i :class="stat.icon"></i>
</div>
@ -274,13 +290,13 @@ export default {
label: '跟班学员数',
cardClass: 'student-card-3'
},
// {
// key: 'company_ganbu_total',
// icon: 'el-icon-s-check',
// value: '0',
// label: '',
// cardClass: 'student-card-4'
// },
{
key: 'company_ganbu_total',
icon: 'el-icon-s-check',
value: '0',
label: '全市干部参与企业',
cardClass: 'student-card-4'
},
{
key: 'company_join_total',
icon: 'el-icon-s-promotion',
@ -408,7 +424,9 @@ export default {
uniquePeople: 0
},
//
threeCoverageManualData: {}
threeCoverageManualData: {},
//
statistics_metadata: {}
}
},
watch: {
@ -985,9 +1003,17 @@ export default {
this.regionSummary = { totalPeople: 0, uniquePeople: 0 }
}
//
if (data && data.statistics_metadata) {
this.statistics_metadata = data.statistics_metadata
} else {
this.statistics_metadata = {}
}
console.log('统计数据已更新:', this.statsCategories)
console.log('课程分类明细数据已更新:', this.courseDetailData)
console.log('区域明细数据已更新:', this.regionData)
console.log('统计元数据:', this.statistics_metadata)
},
//
@ -1163,6 +1189,40 @@ export default {
//
getManualValue(key) {
return this.threeCoverageManualData[key] || '0'
},
//
hasMetadata(key) {
return key && this.statistics_metadata && this.statistics_metadata[key]
},
//
showMetadataInfo(key, label) {
if (!this.hasMetadata(key)) {
return
}
const metadata = this.statistics_metadata[key]
const from = metadata.from || '暂无统计规则说明'
const verify = metadata.verify || '暂无验证方式说明'
const title = label || '统计说明'
this.$alert(
`<div style="text-align: left; line-height: 1.8;">
<div style="margin-bottom: 12px;">
<strong style="color: #409EFF;">统计规则</strong><br/>
<span style="color: #606266; white-space: pre-wrap;">${from}</span>
</div>
<div>
<strong style="color: #409EFF;">验证方式</strong><br/>
<span style="color: #606266; white-space: pre-wrap;">${verify}</span>
</div>
</div>`,
title,
{
dangerouslyUseHTMLString: true,
confirmButtonText: '确定',
customClass: 'stats-metadata-dialog'
}
)
}
}
}
@ -1375,19 +1435,28 @@ export default {
}
}
.stats-download {
.stats-actions {
position: absolute;
top: 8px;
right: 8px;
display: flex;
align-items: center;
gap: 8px;
z-index: 3;
}
.stats-download,
.stats-info {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
transition: color 0.2s ease;
z-index: 3;
transition: all 0.2s ease;
}
.stats-download:hover {
.stats-download:hover,
.stats-info:hover {
color: #fff;
transform: scale(1.1);
}
.stats-icon {
@ -1634,4 +1703,15 @@ export default {
}
}
}
/* 统计元数据弹窗样式 */
::v-deep .stats-metadata-dialog {
.el-message-box__message {
padding: 10px 0;
}
.el-message-box__content {
padding: 20px;
}
}
</style>

@ -85,6 +85,7 @@
id: '',
selectCompanyVisible: false,
form: {
company_id: '',
company_name: '',
stock_date: '',
enrollment_date: '',
@ -128,6 +129,7 @@
this.selectCompanyVisible = true
},
handleCompanySelect(data) {
this.form.company_id = data.company_id
this.form.company_name = data.company_name
this.form.enrollment_date = data.enrollment_date
}
@ -141,6 +143,7 @@
} else {
this.id = ''
this.form = {
company_id: '',
company_name: '',
stock_date: '',
enrollment_date: '',

@ -109,8 +109,8 @@
v-for="(courseSign, cIdx) in user.course_signs"
:key="cIdx"
class="course-item"
:class="{ 'selected': selectedCourse && selectedCourse.company_name === scope.row.company_name && selectedCourse.userIndex === uIdx && selectedCourse.courseIndex === cIdx }"
@click="handleSelectCourse(scope.row.company_name, user, courseSign, uIdx, cIdx)">
:class="{ 'selected': selectedCourse && selectedCourse.company_id === scope.row.id && selectedCourse.userIndex === uIdx && selectedCourse.courseIndex === cIdx }"
@click="handleSelectCourse(scope.row, user, courseSign, uIdx, cIdx)">
<span class="course-name">{{ courseSign.course && courseSign.course.name ? courseSign.course.name : '-' }}</span>
<span class="course-date"> - {{ courseSign.course && courseSign.course.start_date ? courseSign.course.start_date : '-' }}</span>
</div>
@ -313,19 +313,21 @@ export default {
this.page = val
this.getList()
},
handleSelectCourse(companyName, user, courseSign, userIndex, courseIndex) {
handleSelectCourse(company, user, courseSign, userIndex, courseIndex) {
const enrollmentDate = courseSign.course && courseSign.course.start_date ? courseSign.course.start_date : ''
this.selectedCourse = {
company_name: companyName,
company_id: company.id,
company_name: company.company_name,
enrollment_date: enrollmentDate,
userIndex,
courseIndex
}
//
// ID
this.$emit('select', {
company_name: companyName,
company_id: company.id,
company_name: company.company_name,
enrollment_date: enrollmentDate
})

@ -126,6 +126,12 @@
</el-option>
</el-select>
</div>
<div style="width:250px">
<el-select v-model="select.is_company_market" placeholder="请选择是否上市企业" clearable style="width: 100%;">
<el-option label="是" :value="1"></el-option>
<el-option label="否" :value="0"></el-option>
</el-select>
</div>
<div style="width:250px">
<el-date-picker v-model="companyDates" format="yyyy-MM-dd" value-format="yyyy-MM-dd"
style="width:100%" @change="changeCompanyDates" type="daterange" range-separator="至"
@ -410,6 +416,7 @@
has_openid:'',
sign_start_date: '',
sign_end_date: '',
is_company_market: '',
page: 1,
page_size: 10,
},
@ -669,6 +676,7 @@
status: this.select.status,
course_type: this.select.course_type,
has_openid: this.select.has_openid,
is_company_market: this.select.is_company_market,
is_export: 1,
page: 1,
page_size: 99999
@ -742,6 +750,7 @@
this.select.sign_start_date = ''
this.select.sign_end_date = ''
this.signDates = []
this.select.is_company_market = ''
this.select.page = 1
this.getList()
},
@ -778,7 +787,8 @@
has_openid:this.select.has_openid,
course_type: this.select.course_type,
sign_start_date: this.select.sign_start_date,
sign_end_date: this.select.sign_end_date
sign_end_date: this.select.sign_end_date,
is_company_market: this.select.is_company_market
})
this.list = res.list.data
this.total = res.list.total

@ -351,6 +351,12 @@
align: 'center',
width: 80,
fixed: 'left'
},{
prop: 'is_schoolmate_company',
label: '是否校友企业',
align: 'center',
width: 120,
fixed: 'left'
},{
prop: 'company_name',
label: '企业名称',

@ -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>

@ -197,6 +197,7 @@ import SurveyFillDialog from "./components/SurveyFillDialog.vue";
import { index as courseApi } from "@/api/course/index.js";
import { index, destroy, save, qrCode} from "@/api/survey/evaluations.js";
import { index as formIndex } from "@/api/survey/index.js";
export default {
components: {
@ -297,36 +298,36 @@ export default {
this.$refs.SurveyCreateWizard.isShow = true;
},
viewResults(survey) {
this.resultsSurveyData = {
id: 1,
title: "问卷1",
description: "请对本次智能制造专题课程进行评价和反馈",
status: "published",
type: "feedback",
bindType: "course",
bindCourse: "2025产业加速营 | 智能制造专题",
createTime: "2025-06-01 10:00",
deadline: "2025-06-10 23:59",
responses: 1,
questions: 6,
avgScore: 4.6,
questions: [
{
id: 101,
type: "single",
title: "单选框",
options: ["单选1", "单选2"],
},
// {
// id: 102,
// type: 'rate',
// title: '',
// rateMax: 5
// }
],
};
this.resultsDialogVisible = true;
async viewResults(survey) {
try {
//
const questionsRes = await formIndex({
page: 1,
page_size: 999,
sort_name: 'sort',
sort_type: 'ASC',
show_relation: ['courseContent.teacher'],
filter: [{
key: 'course_content_evaluation_id',
op: 'eq',
value: survey.id
}],
});
//
this.resultsSurveyData = {
...survey,
questions: questionsRes.data || [],
// survey
responses: survey.course_content_evaluation_forms_count || 0,
questionsCount: survey.course_content_evaluation_asks_count || (questionsRes.data && questionsRes.data.length) || 0,
};
this.resultsDialogVisible = true;
} catch (error) {
console.error('获取问卷结果失败:', error);
this.$message.error('获取问卷结果失败,请重试');
}
},
async previewSurvey(survey) {
try {

Loading…
Cancel
Save