diff --git a/frontend/package.json b/frontend/package.json index 791d5fb7..48091054 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,8 @@ "dev": "vue-cli-service serve", "build:prod": "vue-cli-service build", "build:stage": "vue-cli-service build --mode staging", - "preview": "node build/index.js --preview" + "preview": "node build/index.js --preview", + "dict:init-course-type": "node scripts/init-course-type-dict.js" }, "keywords": [ "vue", diff --git a/frontend/scripts/init-course-type-dict.js b/frontend/scripts/init-course-type-dict.js new file mode 100644 index 00000000..63ec6244 --- /dev/null +++ b/frontend/scripts/init-course-type-dict.js @@ -0,0 +1,193 @@ +/** + * 版权所有,内部使用。 + * + * @description 初始化教学大纲使用的课类型字典,可重复执行且不会重复插入。 + * @author 软件研发部 + * @since 2026-08-25 + */ + +const DICT_TYPE = 'course_type' +const DICT_NAME = '课类型' +const DEFAULT_API_BASE_URL = 'http://localhost/api' +const DEFAULT_ADMIN_USERNAME = 'admin' +const DICT_ITEMS = [ + { label: '必修', value: '必修' }, + { label: '选修', value: '选修' }, + { label: '实践', value: '实践' } +] + +let authToken = '' + +function readHiddenInput(prompt) { + if (!process.stdin.isTTY || !process.stdin.setRawMode) { + throw new Error('当前终端不支持隐藏输入,请在 PowerShell 中直接运行该脚本') + } + + return new Promise((resolve, reject) => { + let input = '' + + function cleanup() { + process.stdin.removeListener('data', handleInput) + process.stdin.setRawMode(false) + process.stdin.pause() + process.stdout.write('\n') + } + + function handleInput(characters) { + for (const character of characters) { + if (character === '\u0003') { + cleanup() + reject(new Error('用户取消执行')) + return + } + if (character === '\r' || character === '\n') { + cleanup() + resolve(input) + return + } + if (character === '\u0008' || character === '\u007f') { + input = input.slice(0, -1) + continue + } + if (character >= ' ') { + input += character + } + } + } + + process.stdout.write(prompt) + process.stdin.setEncoding('utf8') + process.stdin.setRawMode(true) + process.stdin.resume() + process.stdin.on('data', handleInput) + }) +} + +async function authenticate() { + const environmentToken = process.env.EDUCATION_API_TOKEN + if (environmentToken) { + authToken = environmentToken.replace(/^Bearer\s+/i, '') + return + } + + const username = process.env.EDUCATION_ADMIN_USERNAME || DEFAULT_ADMIN_USERNAME + let password = await readHiddenInput(`请输入管理员 ${username} 的密码:`) + const response = await fetch(getApiBaseUrl() + '/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json;charset=utf-8' }, + body: JSON.stringify({ username, password, code: '', uuid: '' }) + }) + password = '' + const result = await response.json() + + if (!response.ok || result.code !== 200 || !result.token) { + throw new Error(result.msg || `登录失败:HTTP ${response.status}`) + } + + authToken = result.token +} + +function getApiBaseUrl() { + const configuredUrl = process.env.EDUCATION_API_BASE_URL || DEFAULT_API_BASE_URL + + return configuredUrl.replace(/\/+$/, '') +} + +async function request(path, options = {}) { + if (!authToken) { + throw new Error('尚未完成管理员认证') + } + + const response = await fetch(getApiBaseUrl() + path, { + ...options, + headers: { + Authorization: 'Bearer ' + authToken, + 'Content-Type': 'application/json;charset=utf-8', + ...(options.headers || {}) + } + }) + const result = await response.json() + + if (!response.ok || result.code !== 200) { + throw new Error(result.msg || `请求失败:HTTP ${response.status}`) + } + + return result +} + +async function ensureDictType() { + const query = new URLSearchParams({ + pageNum: '1', + pageSize: '10', + dictType: DICT_TYPE + }) + const result = await request('/system/dict/type/list?' + query.toString()) + const hasDictType = (result.rows || []).some(item => item.dictType === DICT_TYPE) + + if (hasDictType) { + console.log(`字典类型 ${DICT_TYPE} 已存在,跳过新增`) + return + } + + await request('/system/dict/type', { + method: 'POST', + body: JSON.stringify({ + dictName: DICT_NAME, + dictType: DICT_TYPE, + status: '0', + remark: '教学大纲课类型列表' + }) + }) + console.log(`已新增字典类型 ${DICT_TYPE}`) +} + +async function ensureDictItems() { + const query = new URLSearchParams({ + pageNum: '1', + pageSize: '100', + dictType: DICT_TYPE + }) + const result = await request('/system/dict/data/list?' + query.toString()) + const existingValues = new Set((result.rows || []).map(item => item.dictValue)) + + for (const [index, item] of DICT_ITEMS.entries()) { + if (existingValues.has(item.value)) { + console.log(`字典项 ${item.label} 已存在,跳过新增`) + continue + } + + await request('/system/dict/data', { + method: 'POST', + body: JSON.stringify({ + dictSort: index + 1, + dictLabel: item.label, + dictValue: item.value, + dictType: DICT_TYPE, + cssClass: '', + listClass: 'default', + isDefault: 'N', + status: '0', + remark: '教学大纲课类型' + }) + }) + console.log(`已新增字典项 ${item.label}`) + } +} + +async function refreshDictCache() { + await request('/system/dict/type/refreshCache', { method: 'DELETE' }) + console.log('字典缓存已刷新') +} + +async function main() { + await authenticate() + await ensureDictType() + await ensureDictItems() + await refreshDictCache() + console.log('课类型字典初始化完成') +} + +main().catch(error => { + console.error('课类型字典初始化失败:' + error.message) + process.exitCode = 1 +}) diff --git a/frontend/src/views/teachBusiness/syllabus/index.vue b/frontend/src/views/teachBusiness/syllabus/index.vue index b2cf102d..11d4c5a2 100644 --- a/frontend/src/views/teachBusiness/syllabus/index.vue +++ b/frontend/src/views/teachBusiness/syllabus/index.vue @@ -100,13 +100,41 @@ - - + + + + - - + + + + @@ -116,7 +144,21 @@ - + + + @@ -158,8 +200,22 @@ - - + + + + @@ -225,6 +281,13 @@ import { listSyllabus, listSyllabusByZydhAndTy } from '@/api/teachBusiness/syllabus' +import { listMajor } from '@/api/subjectMajor/major' +import { listKb } from '@/api/teachOffice/kb' +import { listOffice } from '@/api/teachOffice/office' +import { getDicts } from '@/api/system/dict/data' + +const REFERENCE_DATA_PAGE_SIZE = 10000 +const COURSE_TYPE_DICT_CODE = 'course_type' export default { name: 'SyllabusIndex', @@ -240,6 +303,11 @@ export default { pageNum: 1, pageSize: 20, selection: [], + referenceDataLoading: false, + majorOptions: [], + subjectOptions: [], + courseTypeOptions: [], + officeOptions: [], dialog: { visible: false, title: '', @@ -247,10 +315,10 @@ export default { form: this.createEmptyForm() }, rules: { - zydh: [{ required: true, message: '请输入专业代号', trigger: 'blur' }], - kbh: [{ required: true, message: '请输入课编号', trigger: 'blur' }], + zydh: [{ required: true, message: '请选择专业', trigger: 'change' }], + kbh: [{ required: true, message: '请选择课程', trigger: 'change' }], xqdc: [{ required: true, message: '请输入学期第次', trigger: 'blur' }], - klx: [{ required: true, message: '请输入课类型', trigger: 'blur' }], + klx: [{ required: true, message: '请选择课类型', trigger: 'change' }], xs: [{ required: true, message: '请输入学时', trigger: 'blur' }], ty: [{ required: true, message: '请选择停用标识', trigger: 'change' }] } @@ -264,8 +332,51 @@ export default { }, created() { this.fetchList() + this.loadReferenceData() }, methods: { + loadReferenceData() { + this.referenceDataLoading = true + const query = { pageNum: 1, pageSize: REFERENCE_DATA_PAGE_SIZE } + const requests = [ + listMajor(query), + listKb(query), + listOffice(query), + getDicts(COURSE_TYPE_DICT_CODE) + ].map(request => { + return request + .then(response => this.getResponseRecords(response)) + .catch(() => []) + }) + + Promise.all(requests).then(([majors, subjects, offices, courseTypes]) => { + this.majorOptions = this.getUniqueOptions(majors, 'zydh', 'zymc') + this.subjectOptions = this.getUniqueOptions(subjects, 'kbh', 'kmc') + this.officeOptions = this.getUniqueOptions(offices, 'jysdh', 'jysmc') + this.courseTypeOptions = this.getUniqueOptions(courseTypes, 'dictValue', 'dictLabel') + }).finally(() => { + this.referenceDataLoading = false + }) + }, + getResponseRecords(response) { + const data = response.data || {} + + return Array.isArray(data) ? data : data.records || [] + }, + getUniqueOptions(items, valueKey, labelKey) { + const values = new Set() + + return items.filter(item => { + const value = item[valueKey] + const label = item[labelKey] + if (!value || !label || values.has(value)) { + return false + } + values.add(value) + + return true + }) + }, /* ---------- 列表加载 ---------- */ fetchList() { this.loading = true