From 9e8048be971071635668352bba222effc63cd7a2 Mon Sep 17 00:00:00 2001 From: lion <120344285@qq.com> Date: Mon, 2 Nov 2026 14:50:21 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E6=8A=A5=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/courseForm.js | 108 ++++++++++++++++ src/utils/downloadRequest.js | 9 +- src/views/course/apply_list.vue | 6 +- .../course/components/CourseAnswerField.vue | 39 ++++++ .../course/components/CourseFieldRules.vue | 74 +++++++++++ src/views/course/components/addCourse.vue | 23 +++- src/views/course/components/applyForm.vue | 39 +++--- src/views/course/components/editPane.vue | 63 ++++++++-- src/views/course/components/editSign.vue | 49 ++++---- src/views/student/components/detail.vue | 25 +++- .../survey/components/SurveyFieldSettings.vue | 14 ++- .../survey/components/SurveyQuestion.vue | 4 +- tests/unit/components/CourseForm.spec.js | 115 ++++++++++++++++++ 13 files changed, 506 insertions(+), 62 deletions(-) create mode 100644 src/utils/courseForm.js create mode 100644 src/views/course/components/CourseAnswerField.vue create mode 100644 src/views/course/components/CourseFieldRules.vue create mode 100644 tests/unit/components/CourseForm.spec.js diff --git a/src/utils/courseForm.js b/src/utils/courseForm.js new file mode 100644 index 0000000..9d6c750 --- /dev/null +++ b/src/utils/courseForm.js @@ -0,0 +1,108 @@ +// Legacy value remains a readable string; selected_keys and notes carry new semantics. +export function courseArray(value, fallback = []) { + if (typeof value !== 'string') return value || fallback + try { return JSON.parse(value) || fallback } catch (e) { return fallback } +} +export function courseBlank(value) { + return value == null || (typeof value === 'string' && !value.trim()) || (Array.isArray(value) && !value.length) +} +export function courseSelected(field) { + if (Array.isArray(field.selected_keys)) return field.selected_keys.map(String) + const values = Array.isArray(field.value) ? field.value : field.edit_input === 'checkbox' ? String(field.value || '').split(',') : [field.value] + return (field.select_item || []).filter(o => values.map(String).includes(String(o.value))).map(o => String(o.key)) +} +export function prepareCourseFields(fields, user = {}) { + return (fields || []).map(source => { + const field = { ...source, config: courseArray(source.config, {}), select_item: courseArray(source.select_item), option_texts: courseArray(source.option_texts, {}), other_text: source.other_text || '' } + if (!Object.prototype.hasOwnProperty.call(source, 'value') && (field.belong_user === true || Number(field.belong_user) === 1)) { + const key = field.belong_user_table || field.field + field.value = user[key] == null && key === 'username' ? user.name : user[key] + } + if (field.value == null) field.value = '' + if (Array.isArray(field.value)) field.value = field.value.join(',') + field.selected_keys = courseSelected(field) + field.valueArr = field.selected_keys.length ? field.select_item.filter(o => field.selected_keys.includes(String(o.key))).map(o => o.value) : (field.edit_input === 'checkbox' && field.value ? String(field.value).split(',') : []) + field.fileList = courseArray(field.fileList) + field.fileListArr = courseArray(field.fileListArr || field.fileList) + return field + }).sort((a, b) => Number(a.sort || 0) - Number(b.sort || 0) || Number(a.id || 0) - Number(b.id || 0)) +} +export function visibleCourseFields(fields) { + const shown = [] + return (fields || []).filter(field => { + const condition = (field.config || {}).visible_when + if (condition) { + const parent = shown.find(item => item.field === condition.field) + const values = (condition.values || [condition.value]).map(String) + if (!parent || !courseSelected(parent).some(key => values.includes(key))) return false + } + shown.push(field) + return true + }) +} +export function requiredCourseField(field, fields) { + if (fields && !visibleCourseFields(fields).includes(field)) return false + const condition = (field.config || {}).visible_when + if (condition && Array.isArray(condition.required_values) && fields) { + const parent = fields.find(item => item.field === condition.field) + return !!parent && courseSelected(parent).some(key => condition.required_values.map(String).includes(key)) + } + return (field.rule || '').split(',').includes('required') +} +export function changeCourseChoice(field, values) { + const selected = values === undefined ? (field.edit_input === 'checkbox' ? field.valueArr : [field.value]) : values + const labels = (Array.isArray(selected) ? selected : [selected]).map(String) + field.selected_keys = field.select_item.filter(o => labels.includes(String(o.value))).map(o => String(o.key)) + field.value = field.edit_input === 'checkbox' ? labels.join(',') : (labels[0] || '') + field.valueArr = field.edit_input === 'checkbox' ? labels : [] + field.option_texts = Object.keys(field.option_texts || {}).reduce((result, key) => { + if (field.selected_keys.includes(String(key))) result[key] = field.option_texts[key] + return result + }, {}) + if (!field.selected_keys.includes(String((field.config || {}).other_key || ''))) field.other_text = '' +} +export function courseNotes(field) { + const selected = courseSelected(field) + const config = field.config || {} + return (field.select_item || []).filter(option => selected.includes(String(option.key)) && config.option_notes && config.option_notes[option.key]) +} +export function courseChoiceHint(field) { + const config = field.config || {} + return '最少选择' + (config.min_choices || 1) + '项' + (config.max_choices > 0 ? ',最多选择' + config.max_choices + '项' : '') + ',已选择' + courseSelected(field).length + '项' +} +export function courseFormError(fields) { + for (const field of visibleCourseFields(fields)) { + const fail = message => field.name + ':' + message + const value = field.value + const selected = courseSelected(field) + const empty = ['radio', 'checkbox'].includes(field.edit_input) ? !selected.length : courseBlank(value) + if (empty) { if (requiredCourseField(field, fields)) return fail('请填写此项'); continue } + const config = field.config || {} + if (field.edit_input === 'checkbox') { + if (selected.length < Number(config.min_choices || 1)) return fail('至少选择' + (config.min_choices || 1) + '项') + if (config.max_choices && selected.length > Number(config.max_choices)) return fail('最多选择' + config.max_choices + '项') + } + const rules = (field.rule || '').split(',') + if (rules.includes('mobile') && !/^1[3-9]\d{9}$/.test(String(value))) return fail('手机号格式不正确') + if (rules.includes('email') && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))) return fail('邮箱格式不正确') + if (rules.includes('integer') && !/^-?\d+$/.test(String(value))) return fail('请输入整数') + if ((rules.includes('numeric') || field.edit_input === 'number') && !Number.isFinite(Number(value))) return fail('请输入有效数字') + if (rules.includes('idcard') && !/^(\d{15}|\d{17}[\dXx])$/.test(String(value))) return fail('身份证格式不正确') + for (const option of courseNotes(field)) { + if (config.option_notes[option.key].required && courseBlank((field.option_texts || {})[option.key])) return fail('请填写补充说明') + } + if (selected.includes(String(config.other_key)) && config.other_required && courseBlank(field.other_text)) return fail('请填写补充说明') + } + return '' +} +export function courseSubmission(fields) { + const visible = visibleCourseFields(fields) + return fields.map(field => { + const shown = visible.includes(field) + return { ...field, hidden: !shown, value: shown ? field.value : null, + selected_keys: shown ? courseSelected(field) : [], + option_texts: shown ? courseNotes(field).reduce((notes, option) => { notes[option.key] = (field.option_texts || {})[option.key] || ''; return notes }, {}) : {}, + other_text: shown && courseSelected(field).includes(String((field.config || {}).other_key)) ? field.other_text : '', + fileList: shown ? field.fileList : [], fileListArr: shown ? field.fileListArr : [] } + }) +} diff --git a/src/utils/downloadRequest.js b/src/utils/downloadRequest.js index c05251b..29621a2 100644 --- a/src/utils/downloadRequest.js +++ b/src/utils/downloadRequest.js @@ -36,7 +36,7 @@ function customParamsSerializer(params) { return result.slice(0, -1); } -export async function download(url, method = "get", info, filename) { +export async function download(url, method = "get", info, filename, downloadOptions = {}) { loading = Loading.service({ lock: true, background: "rgba(0,0,0,0.4)", @@ -48,7 +48,7 @@ export async function download(url, method = "get", info, filename) { url, method, responseType: "blob", - timeout: 30000, + timeout: downloadOptions.timeout || 30000, headers: { Accept: "application/json", "Content-Type": "application/json; charset=utf-8", @@ -88,6 +88,11 @@ export async function download(url, method = "get", info, filename) { ""; } + // Registration exports include attachments in a ZIP when present. + if ((response.headers["content-type"] || "").includes("application/zip")) { + filename = (filename || "报名信息.xlsx").replace(/\.(xlsx|xls|zip)$/i, "") + ".zip"; + } + // 将二进制流转为blob const blob = new Blob([response.data], { type: "application/octet-stream", diff --git a/src/views/course/apply_list.vue b/src/views/course/apply_list.vue index f58b41b..3abac07 100644 --- a/src/views/course/apply_list.vue +++ b/src/views/course/apply_list.vue @@ -93,7 +93,7 @@ 重置 导入 - 导出 + 导出报名信息及附件 批量审核 @@ -243,7 +243,7 @@ selectids: [], list: [], total: 0, - table_item: [{ + table_item: [{ prop: 'replacement_text', label: '报名更换记录', width: 260, align: 'left' }, { type: 'selection', width: 50, fixed: 'left', @@ -614,7 +614,7 @@ page: 1, page_size: 999 }, - `${this.subjectObj.title}报名信息.xlsx`) + `${this.subjectObj.title}报名信息.xlsx`, { timeout: 120000 }) } } } diff --git a/src/views/course/components/CourseAnswerField.vue b/src/views/course/components/CourseAnswerField.vue new file mode 100644 index 0000000..92d52ac --- /dev/null +++ b/src/views/course/components/CourseAnswerField.vue @@ -0,0 +1,39 @@ + + + diff --git a/src/views/course/components/CourseFieldRules.vue b/src/views/course/components/CourseFieldRules.vue new file mode 100644 index 0000000..7e786fa --- /dev/null +++ b/src/views/course/components/CourseFieldRules.vue @@ -0,0 +1,74 @@ + + + diff --git a/src/views/course/components/addCourse.vue b/src/views/course/components/addCourse.vue index 4067a71..05236fb 100644 --- a/src/views/course/components/addCourse.vue +++ b/src/views/course/components/addCourse.vue @@ -131,6 +131,21 @@ + @@ -417,6 +432,7 @@ imgList: [], qunList: [], publicizeList: [], + companyLimitDuplicates: [], form: { step: '', // 控制课程 课表 type: '', @@ -429,6 +445,7 @@ total: '', sign_start_date: '', sign_end_date: '', + one_person_per_company: 0, status: 0, address_detail: '', @@ -708,6 +725,7 @@ duration: 2000 }) this.id = res.id + this.companyLimitDuplicates = res.company_limit_duplicates || [] this.$emit('refresh') this.active = 1 // this.active = 1 @@ -771,7 +789,9 @@ id: this.id, show_relation: ['image', 'qun_image'] }).then(res => { + this.companyLimitDuplicates = res.company_limit_duplicates || [] this.form = this.base.requestToForm(res, this.form) + this.form.one_person_per_company = Number(res.one_person_per_company || 0) if (res.image) { this.imgList.push(res.image) } @@ -876,6 +896,7 @@ total: '', sign_start_date: '', sign_end_date: '', + one_person_per_company: 0, status: 0, applylabel: '', address_detail: '', diff --git a/src/views/course/components/applyForm.vue b/src/views/course/components/applyForm.vue index a3cef6e..920077e 100644 --- a/src/views/course/components/applyForm.vue +++ b/src/views/course/components/applyForm.vue @@ -12,6 +12,7 @@
+
@@ -38,20 +39,21 @@ label-position="top" size="small"> - + previewMode ? isRequired(i) : (i.rule ? !!i.rule.includes('required') : false) + " v-for="(i, index) in formList" :key="i.id || i.field || index" v-show="!previewMode || visibleForms.includes(i)" @click.native="selectPick(i, index)"> + +
+ @@ -69,6 +72,8 @@