问卷 新闻

master
lion 4 days ago
parent 44c044c777
commit 492c01d8c5

@ -0,0 +1,77 @@
<template>
<div class="survey-attachments">
<div v-for="file in files" :key="file.id" class="attachment">
<i class="el-icon-document" />
<span class="filename" :title="file.original_name">{{
file.original_name
}}</span>
<span class="filesize">{{ sizeLabel(file.size) }}</span>
<el-button
type="text"
:loading="downloading === file.id"
@click="download(file)"
>下载</el-button>
</div>
<span v-if="!files.length">未上传</span>
</div>
</template>
<script>
import axios from 'axios'
import { getToken } from '@/utils/auth'
import { saveAs } from 'file-saver'
export default {
props: { responseId: [Number, String], files: { type: Array, default: () => [] }},
data() {
return { downloading: null }
},
methods: {
sizeLabel(size) {
return Number(size) >= 1048576
? (size / 1048576).toFixed(1) + ' MB'
: Math.ceil(Number(size || 0) / 1024) + ' KB'
},
async download(file) {
this.downloading = file.id
try {
const response = await axios.get('/api/admin/course-content-evaluation-form/show', {
baseURL: process.env.VUE_APP_BASE_API,
params: { id: this.responseId, download_file_id: file.id },
headers: { Authorization: 'Bearer ' + getToken() },
responseType: 'blob'
})
if ((response.headers['content-type'] || '').includes('application/json')) {
const error = JSON.parse(await response.data.text())
throw new Error(error.errmsg || '下载失败,请重新登录后重试')
}
saveAs(response.data, file.original_name || '附件')
} catch (e) {
let message = e.message || '下载失败,请稍后重试'
if (e.response && e.response.data && typeof e.response.data.text === 'function') {
try { message = JSON.parse(await e.response.data.text()).errmsg || message } catch (_) { /* Keep the original error. */ }
}
this.$message.error(message)
} finally {
this.downloading = null
}
}
}
}
</script>
<style scoped>
.attachment {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
}
.filename {
flex: 1;
min-width: 0;
overflow-wrap: anywhere;
}
.filesize {
color: #909399;
font-size: 12px;
white-space: nowrap;
}
</style>

@ -1,5 +1,22 @@
<template>
<div class="field-settings">
<template v-if="field.edit_input === 'file'">
<el-form-item
label="最多上传文件数"
><el-input-number
:value="field.config.max_files || 1"
:min="1"
:max="10"
:precision="0"
@change="$set(field.config, 'max_files', $event)"
/></el-form-item>
<el-form-item label="允许上传的格式">
<el-select :value="field.config.allowed_extensions || fileExtensions" multiple collapse-tags placeholder="请至少选择一种格式" @change="$set(field.config, 'allowed_extensions', $event)">
<el-option v-for="ext in fileExtensions" :key="ext" :value="ext" :label="ext.toUpperCase()" />
</el-select>
</el-form-item>
<p class="hint">仅允许所选后缀格式,单文件小于 50MB。</p>
</template>
<template
v-if="field.edit_input === 'radio' || field.edit_input === 'checkbox'"
>
@ -15,6 +32,10 @@
可自由添加、删除选项并编辑每项分值(支持 0、负数和小数);5~1
分仅为快捷预设。
</p>
<el-form-item v-if="field.edit_input === 'checkbox'" label="最多选择几项">
<el-input-number :value="field.config.max_choices || 0" :min="0" :max="field.select_item.length || 1" :precision="0" @change="$set(field.config, 'max_choices', $event || 0)" />
<span class="hint">0 表示不限</span>
</el-form-item>
<el-form-item label="选项">
<div
v-for="(option, index) in field.select_item"
@ -43,6 +64,17 @@
</div>
<el-button size="small" @click="addOption">添加选项</el-button>
</el-form-item>
<template v-if="!dimension && field.edit_input === 'checkbox'">
<el-form-item label="选项附加输入框">
<div v-for="option in field.select_item" :key="option.key" class="note-settings">
<div>{{ option.value || '未命名选项' }} <el-switch :value="!!noteFor(option.key)" active-text="选中后填写" @change="toggleNote(option.key, $event)" /></div>
<template v-if="noteFor(option.key)">
<el-input v-model="field.config.option_notes[option.key].placeholder" maxlength="255" placeholder="输入框提示文字,如:可初步填写您想咨询的问题" />
<el-checkbox v-model="field.config.option_notes[option.key].required">选中后必须填写</el-checkbox>
</template>
</div>
</el-form-item>
</template>
<template v-if="!dimension">
<el-form-item label="选择后填写补充说明">
<el-select
@ -55,9 +87,13 @@
:key="option.key"
:value="option.key"
:label="option.value || option.key"
:disabled="!!noteFor(option.key)"
/>
</el-select>
</el-form-item>
<el-form-item v-if="field.config.other_key || field.allow_input" label="补充输入框提示">
<el-input v-model="field.config.other_placeholder" maxlength="255" placeholder="请填写补充说明" />
</el-form-item>
<el-form-item v-if="field.config.other_key" label="补充必填">
<el-switch v-model="field.config.other_required" />
</el-form-item>
@ -96,7 +132,16 @@
<script>
export default {
props: { field: { type: Object, required: true }, dimension: Boolean },
data() { return { fileExtensions: ['zip', 'rar', 'ppt', 'pptx', 'xls', 'xlsx', 'doc', 'docx', 'png', 'gif', 'jpg', 'jpeg', 'pdf', 'mp4'] } },
methods: {
noteFor(key) { return (this.field.config.option_notes || {})[key] },
toggleNote(key, enabled) {
if (!this.field.config.option_notes) this.$set(this.field.config, 'option_notes', {})
if (enabled) {
this.$set(this.field.config.option_notes, key, { placeholder: '', required: false })
if (this.field.config.other_key === key) this.field.config.other_key = ''
} else this.$delete(this.field.config.option_notes, key)
},
addOption() {
this.field.select_item.push({
key:
@ -110,7 +155,9 @@ export default {
if (this.field.config.other_key === this.field.select_item[index].key) {
this.field.config.other_key = ''
}
if (this.field.config.option_notes) this.$delete(this.field.config.option_notes, this.field.select_item[index].key)
this.field.select_item.splice(index, 1)
if (this.field.config.max_choices > this.field.select_item.length) this.field.config.max_choices = this.field.select_item.length
},
ratingOptions() {
this.field.select_item = [5, 4, 3, 2, 1].map((score, i) => ({
@ -124,6 +171,8 @@ export default {
}
</script>
<style scoped>
.note-settings { padding: 10px 0; border-bottom: 1px solid #eee; }
.note-settings .el-input { margin: 8px 0; }
.option-row {
display: flex;
gap: 8px;

@ -1,7 +1,14 @@
<template>
<div class="preview-field">
<div v-if="field.edit_input === 'file'" class="file-preview">
<el-button icon="el-icon-upload2" disabled>上传文件</el-button>
<p>
最多 {{ (field.config || {}).max_files || 1 }} 个文件,单文件小于
50MB(请在小程序中上传)。允许格式:{{ ((field.config || {}).allowed_extensions || ['zip', 'rar', 'ppt', 'pptx', 'xls', 'xlsx', 'doc', 'docx', 'png', 'gif', 'jpg', 'jpeg', 'pdf', 'mp4']).join("、").toUpperCase() }}
</p>
</div>
<el-radio-group
v-if="field.edit_input === 'radio' || !field.edit_input"
v-else-if="field.edit_input === 'radio' || !field.edit_input"
:value="value"
:disabled="disabled"
class="choices"

@ -59,7 +59,11 @@
:value="selectedValue(question)"
disabled
/>
<div class="answer">
<SurveyAttachments
v-if="question.edit_input === 'file'"
:files="question.value || []" :response-id="responseId"
/>
<div v-else class="answer">
{{ hasOptions(question) ? "学员选择:" : ""
}}{{ question.display_value }}
</div>
@ -90,7 +94,7 @@
<el-input
v-model="previewNoteValues[opt.key]"
type="textarea"
:placeholder="question.config.option_notes[opt.key].placeholder"
:placeholder="question.config.option_notes[opt.key].placeholder || '请填写补充说明'"
/>
</div>
<div v-if="showSupplement" class="supplement-label">
@ -114,12 +118,14 @@
</section>
</template>
<script>
import SurveyAttachments from './SurveyAttachments.vue'
import SurveyPreviewField from './SurveyPreviewField.vue'
export default {
components: { SurveyPreviewField },
components: { SurveyPreviewField, SurveyAttachments },
props: {
question: { type: Object, required: true },
answered: Boolean,
responseId: [Number, String],
number: { type: Number, default: 0 }
},
data() {

@ -27,7 +27,11 @@
:class="['stat-card', { chapter: q.edit_input === 'section' }]"
>
<h3>{{ q.name }}</h3>
<p v-if="q.edit_input === 'section'" class="description" v-text="q.help" />
<p
v-if="q.edit_input === 'section'"
class="description"
v-text="q.help"
/>
<template v-else-if="q.edit_input === 'multi_dimension'">
<p>
有效作答 {{ q.stats.valid_count }} 人
@ -50,10 +54,10 @@
label="均分 / 数量合计"
min-width="130"
><template slot-scope="scope">{{
scope.row.edit_input === 'number'
scope.row.edit_input === "number"
? scope.row.stats.sum
: scope.row.stats.average === null
? '—'
? "—"
: scope.row.stats.average
}}</template></el-table-column></el-table>
<el-collapse><el-collapse-item
@ -82,22 +86,24 @@
><el-table-column
label="姓名"
><template slot-scope="scope">{{
(scope.row.user || {}).name || '—'
(scope.row.user || {}).name || "—"
}}</template></el-table-column><el-table-column
label="学号"
><template slot-scope="scope">{{
(scope.row.user || {}).no || '—'
(scope.row.user || {}).no || "—"
}}</template></el-table-column><el-table-column
label="公司"
><template slot-scope="scope">{{
(scope.row.user || {}).company_name || '—'
(scope.row.user || {}).company_name || "—"
}}</template></el-table-column><el-table-column
label="职务"
><template slot-scope="scope">{{
(scope.row.user || {}).company_position || '—'
(scope.row.user || {}).company_position || "—"
}}</template></el-table-column><el-table-column
label="最近提交时间"
><template slot-scope="scope">{{ scope.row.updated_at || scope.row.created_at }}</template></el-table-column><el-table-column
><template slot-scope="scope">{{
scope.row.updated_at || scope.row.created_at
}}</template></el-table-column><el-table-column
label="操作"
width="90"
><template
@ -123,23 +129,27 @@
>
<div v-if="reply" class="reply">
<h2>{{ reply.snapshot ? reply.snapshot.title : surveyData.title }}</h2>
<SurveyDescription class="description" :value="reply.snapshot ? reply.snapshot.desc : surveyData.desc" />
<SurveyDescription
class="description"
:value="reply.snapshot ? reply.snapshot.desc : surveyData.desc"
/>
<el-descriptions
border
:column="2"
><el-descriptions-item label="姓名">{{
(reply.user || {}).name || '—'
(reply.user || {}).name || "—"
}}</el-descriptions-item><el-descriptions-item label="最近提交时间">{{
reply.updated_at || reply.created_at
}}</el-descriptions-item><el-descriptions-item label="公司">{{
(reply.user || {}).company_name || '—'
(reply.user || {}).company_name || "—"
}}</el-descriptions-item><el-descriptions-item label="学号">{{
(reply.user || {}).no || '—'
(reply.user || {}).no || "—"
}}</el-descriptions-item></el-descriptions>
<SurveyQuestion
v-for="q in reply.questions"
:key="q.id"
:question="q"
:response-id="reply.id"
answered
/>
</div>
@ -153,7 +163,11 @@
<div v-loading="textsLoading">
<div v-for="item in texts" :key="item.id" class="text-answer">
<small>{{ item.created_at }}</small>
<p>{{ item.text }}</p>
<SurveyAttachments
v-if="item.files && item.files.length"
:files="item.files" :response-id="item.id"
/>
<p v-else>{{ item.text }}</p>
<el-button
type="text"
@click="openReply(item)"
@ -180,11 +194,17 @@ import {
statistics,
textAnswers
} from '@/api/survey/evaluationForm'
import SurveyAttachments from './SurveyAttachments.vue'
import SurveyDescription from './SurveyDescription.vue'
import SurveyQuestion from './SurveyQuestion.vue'
import SurveyStatsField from './SurveyStatsField.vue'
export default {
components: { SurveyQuestion, SurveyStatsField, SurveyDescription },
components: {
SurveyAttachments,
SurveyQuestion,
SurveyStatsField,
SurveyDescription
},
props: { visible: Boolean, surveyData: { type: Object, default: null }},
data() {
return {

@ -27,10 +27,14 @@
slot-scope="scope"
>{{ scope.row.percentage }}%</template></el-table-column></el-table>
<el-button
v-if="['text', 'textarea', 'date', 'datetime'].includes(field.edit_input)"
v-if="
['text', 'textarea', 'date', 'datetime', 'file'].includes(
field.edit_input
)
"
type="text"
@click="$emit('texts', 'value')"
>查看回答</el-button>
>{{ field.edit_input === "file" ? "查看附件" : "查看回答" }}</el-button>
<el-button
v-if="field.stats.other_count"
type="text"

@ -101,6 +101,7 @@ export default {
{ value: 'checkbox', label: '多选' },
{ value: 'multi_dimension', label: '多维度 / 组合' },
{ value: 'number', label: '数字' },
{ value: 'file', label: '文件上传' },
{ value: 'text', label: '短文本' },
{ value: 'textarea', label: '长文本' },
{ value: 'date', label: '日期' },

@ -1,3 +1,7 @@
import axios from 'axios'
import Attachments from '@/views/survey/components/SurveyAttachments.vue'
import { saveAs } from 'file-saver'
jest.mock('file-saver', () => ({ saveAs: jest.fn() }))
import { mount, shallowMount, createLocalVue } from '@vue/test-utils'
import ElementUI from 'element-ui'
import FieldSettings from '@/views/survey/components/SurveyFieldSettings.vue'
@ -28,8 +32,10 @@ describe('Survey editing and results', () => {
jest.clearAllMocks()
})
it('keeps unrestricted and multiple audience courses mutually exclusive', () => {
const state = { form: { audience_course_ids: [] } }
Object.defineProperty(state, 'audienceSelection', { get: () => Wizard.computed.audienceSelection.call(state) })
const state = { form: { audience_course_ids: [] }}
Object.defineProperty(state, 'audienceSelection', {
get: () => Wizard.computed.audienceSelection.call(state)
})
expect(state.audienceSelection).toEqual(['all'])
Wizard.methods.changeAudience.call(state, ['all', 11])
expect(state.form.audience_course_ids).toEqual([11])
@ -437,4 +443,46 @@ describe('Survey editing and results', () => {
expect(wrapper.vm.reply.questions[0].display_value).toBe('0')
wrapper.destroy()
})
it('shows file names and downloads with original names', async() => {
const file = { id: 81, original_name: '材料.pdf', url: '/storage/files/a.pdf', size: 2048 }
const answer = mount(Question, { localVue, propsData: { answered: true, question: { edit_input: 'file', name: '附件', value: [file] } } })
expect(answer.text()).toContain('材料.pdf')
expect(answer.text()).toContain('下载')
const blob = new Blob(['example'])
const get = jest.spyOn(axios, 'get').mockResolvedValue({ headers: { 'content-type': 'application/pdf' }, data: blob })
const oldFetch = global.fetch
global.fetch = jest.fn().mockResolvedValue({ ok: true, blob: () => Promise.resolve(blob) })
await answer.find(Attachments).vm.download(file)
expect(saveAs).toHaveBeenCalledWith(blob, '材料.pdf')
expect(get).toHaveBeenCalledWith('/api/admin/course-content-evaluation-form/show', expect.objectContaining({ params: expect.objectContaining({ download_file_id: 81 }) }))
get.mockRestore()
global.fetch = oldFetch
answer.destroy()
})
it('edits allowed upload formats and shows them in preview', async() => {
const field = { edit_input: 'file', config: { max_files: 2, allowed_extensions: ['pdf'] } }
const wrapper = shallowMount(FieldSettings, { localVue, propsData: { field } })
wrapper.find('el-select-stub').vm.$emit('change', ['pdf', 'docx'])
await wrapper.vm.$nextTick()
expect(field.config.allowed_extensions).toEqual(['pdf', 'docx'])
const preview = mount(PreviewField, { localVue, propsData: { field } })
expect(preview.text()).toContain('PDF、DOCX')
wrapper.destroy()
preview.destroy()
})
it('configures per-option notes, placeholders and selection limits without a new type', async() => {
const field = { edit_input: 'checkbox', select_item: [{ key: 'tax', value: '财税' }, { key: 'legal', value: '法律' }], config: { max_choices: 1, other_key: 'tax', other_placeholder: '补充说明提示' } }
const settings = shallowMount(FieldSettings, { localVue, propsData: { field } })
settings.vm.toggleNote('tax', true)
expect(field.config.other_key).toBe('')
field.config.option_notes.tax.placeholder = '可初步填写您想咨询的问题'
field.config.option_notes.tax.required = true
const editor = shallowMount(Editor, { localVue, propsData: { question: { ...field, name: '会场', field: 'venues' } } })
await editor.vm.save()
expect(save.mock.calls[0][0].config).toMatchObject({ max_choices: 1, other_placeholder: '补充说明提示', option_notes: { tax: { placeholder: '可初步填写您想咨询的问题', required: true } } })
settings.vm.removeOption(0)
expect(field.config.option_notes.tax).toBeUndefined()
settings.destroy()
editor.destroy()
})
})

Loading…
Cancel
Save