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