This commit is contained in:
zwt13703 2026-03-12 12:59:01 +08:00
parent cc9271c344
commit 4023e9ab37
33 changed files with 3782 additions and 1747 deletions

View File

@ -34,7 +34,7 @@
"/docs": true,
"**/dist/**": true,
"**/node_modules": true,
"node_modules/**": true,
"node_modules/**": true
},
"cSpell.words": ["Axios", "tinymce"]
}

View File

@ -302,6 +302,8 @@ const local: App.I18n.Schema = {
tool: 'System Tools',
tool_gen: 'Code Generation',
art: 'Art',
'art_history-score-control-line': 'History Score Control Line',
art_major: 'Art Major Library',
art_school: 'School',
art_school_modules: 'School Modules',
'art_school_modules_school-campus': 'School Campus',
@ -314,6 +316,8 @@ const local: App.I18n.Schema = {
'art_school_modules_school-media': 'School Media',
'art_school_modules_school-name': 'School Name',
'art_school_modules_school-tag': 'School Tag',
'art_school-recruit-major': 'School Recruit Major',
'art_school-recruit-major-history': 'School Recruit Major History',
about: 'About'
},
menu: {

View File

@ -298,6 +298,8 @@ const local: App.I18n.Schema = {
tool: '系统工具',
tool_gen: '代码生成',
art: '艺术院校',
'art_history-score-control-line': '历年省控线',
art_major: '艺术专业库',
art_school: '院校管理',
art_school_modules: '院校子模块',
'art_school_modules_school-campus': '校区管理',
@ -309,6 +311,9 @@ const local: App.I18n.Schema = {
'art_school_modules_school-major-tag': '专业标签',
'art_school_modules_school-media': '媒体管理',
'art_school_modules_school-name': '院校名称',
'art_school_modules_school-tag': '学校标签',
'art_school-recruit-major': '院校招录专业',
'art_school-recruit-major-history': '院校招录专业历年录取',
about: '关于'
},
menu: {

View File

@ -1,7 +1,7 @@
import { request } from '@/service/request';
/** 获取历年省控线列表 */
export function fetchGetHistoryScoreControlLineList (params?: Api.Art.HistoryScoreControlLineSearchParams) {
export function fetchGetHistoryScoreControlLineList(params?: Api.Art.HistoryScoreControlLineSearchParams) {
return request<Api.Art.HistoryScoreControlLineList>({
url: '/art/historyScoreControlLine/list',
method: 'get',
@ -9,7 +9,7 @@ export function fetchGetHistoryScoreControlLineList (params?: Api.Art.HistorySco
});
}
/** 新增历年省控线 */
export function fetchCreateHistoryScoreControlLine (data: Api.Art.HistoryScoreControlLineOperateParams) {
export function fetchCreateHistoryScoreControlLine(data: Api.Art.HistoryScoreControlLineOperateParams) {
return request<boolean>({
url: '/art/historyScoreControlLine',
method: 'post',
@ -18,7 +18,7 @@ export function fetchCreateHistoryScoreControlLine (data: Api.Art.HistoryScoreCo
}
/** 修改历年省控线 */
export function fetchUpdateHistoryScoreControlLine (data: Api.Art.HistoryScoreControlLineOperateParams) {
export function fetchUpdateHistoryScoreControlLine(data: Api.Art.HistoryScoreControlLineOperateParams) {
return request<boolean>({
url: '/art/historyScoreControlLine',
method: 'put',
@ -27,7 +27,7 @@ export function fetchUpdateHistoryScoreControlLine (data: Api.Art.HistoryScoreCo
}
/** 批量删除历年省控线 */
export function fetchBatchDeleteHistoryScoreControlLine (controlIds: CommonType.IdType[]) {
export function fetchBatchDeleteHistoryScoreControlLine(controlIds: CommonType.IdType[]) {
return request<boolean>({
url: `/art/historyScoreControlLine/${controlIds.join(',')}`,
method: 'delete'

View File

@ -1,7 +1,7 @@
import { request } from '@/service/request';
/** 获取艺术专业库列表 */
export function fetchGetMajorList (params?: Api.Art.MajorSearchParams) {
export function fetchGetMajorList(params?: Api.Art.MajorSearchParams) {
return request<Api.Art.MajorList>({
url: '/art/major/list',
method: 'get',
@ -9,7 +9,7 @@ export function fetchGetMajorList (params?: Api.Art.MajorSearchParams) {
});
}
/** 新增艺术专业库 */
export function fetchCreateMajor (data: Api.Art.MajorOperateParams) {
export function fetchCreateMajor(data: Api.Art.MajorOperateParams) {
return request<boolean>({
url: '/art/major',
method: 'post',
@ -18,7 +18,7 @@ export function fetchCreateMajor (data: Api.Art.MajorOperateParams) {
}
/** 修改艺术专业库 */
export function fetchUpdateMajor (data: Api.Art.MajorOperateParams) {
export function fetchUpdateMajor(data: Api.Art.MajorOperateParams) {
return request<boolean>({
url: '/art/major',
method: 'put',
@ -27,7 +27,7 @@ export function fetchUpdateMajor (data: Api.Art.MajorOperateParams) {
}
/** 批量删除艺术专业库 */
export function fetchBatchDeleteMajor (majorIds: CommonType.IdType[]) {
export function fetchBatchDeleteMajor(majorIds: CommonType.IdType[]) {
return request<boolean>({
url: `/art/major/${majorIds.join(',')}`,
method: 'delete'

View File

@ -1,7 +1,7 @@
import { request } from '@/service/request';
/** 获取院校招录专业历年录取数据列表 */
export function fetchGetSchoolRecruitMajorHistoryList (params?: Api.Art.SchoolRecruitMajorHistorySearchParams) {
export function fetchGetSchoolRecruitMajorHistoryList(params?: Api.Art.SchoolRecruitMajorHistorySearchParams) {
return request<Api.Art.SchoolRecruitMajorHistoryList>({
url: '/art/schoolRecruitMajorHistory/list',
method: 'get',
@ -9,7 +9,7 @@ export function fetchGetSchoolRecruitMajorHistoryList (params?: Api.Art.SchoolRe
});
}
/** 新增院校招录专业历年录取数据 */
export function fetchCreateSchoolRecruitMajorHistory (data: Api.Art.SchoolRecruitMajorHistoryOperateParams) {
export function fetchCreateSchoolRecruitMajorHistory(data: Api.Art.SchoolRecruitMajorHistoryOperateParams) {
return request<boolean>({
url: '/art/schoolRecruitMajorHistory',
method: 'post',
@ -18,7 +18,7 @@ export function fetchCreateSchoolRecruitMajorHistory (data: Api.Art.SchoolRecrui
}
/** 修改院校招录专业历年录取数据 */
export function fetchUpdateSchoolRecruitMajorHistory (data: Api.Art.SchoolRecruitMajorHistoryOperateParams) {
export function fetchUpdateSchoolRecruitMajorHistory(data: Api.Art.SchoolRecruitMajorHistoryOperateParams) {
return request<boolean>({
url: '/art/schoolRecruitMajorHistory',
method: 'put',
@ -27,7 +27,7 @@ export function fetchUpdateSchoolRecruitMajorHistory (data: Api.Art.SchoolRecrui
}
/** 批量删除院校招录专业历年录取数据 */
export function fetchBatchDeleteSchoolRecruitMajorHistory (historyIds: CommonType.IdType[]) {
export function fetchBatchDeleteSchoolRecruitMajorHistory(historyIds: CommonType.IdType[]) {
return request<boolean>({
url: `/art/schoolRecruitMajorHistory/${historyIds.join(',')}`,
method: 'delete'

View File

@ -1,7 +1,7 @@
import { request } from '@/service/request';
/** 获取院校招录专业列表 */
export function fetchGetSchoolRecruitMajorList (params?: Api.Art.SchoolRecruitMajorSearchParams) {
export function fetchGetSchoolRecruitMajorList(params?: Api.Art.SchoolRecruitMajorSearchParams) {
return request<Api.Art.SchoolRecruitMajorList>({
url: '/art/schoolRecruitMajor/list',
method: 'get',
@ -9,7 +9,7 @@ export function fetchGetSchoolRecruitMajorList (params?: Api.Art.SchoolRecruitMa
});
}
/** 新增院校招录专业 */
export function fetchCreateSchoolRecruitMajor (data: Api.Art.SchoolRecruitMajorOperateParams) {
export function fetchCreateSchoolRecruitMajor(data: Api.Art.SchoolRecruitMajorOperateParams) {
return request<boolean>({
url: '/art/schoolRecruitMajor',
method: 'post',
@ -18,7 +18,7 @@ export function fetchCreateSchoolRecruitMajor (data: Api.Art.SchoolRecruitMajorO
}
/** 修改院校招录专业 */
export function fetchUpdateSchoolRecruitMajor (data: Api.Art.SchoolRecruitMajorOperateParams) {
export function fetchUpdateSchoolRecruitMajor(data: Api.Art.SchoolRecruitMajorOperateParams) {
return request<boolean>({
url: '/art/schoolRecruitMajor',
method: 'put',
@ -27,7 +27,7 @@ export function fetchUpdateSchoolRecruitMajor (data: Api.Art.SchoolRecruitMajorO
}
/** 批量删除院校招录专业 */
export function fetchBatchDeleteSchoolRecruitMajor (recruitMajorIds: CommonType.IdType[]) {
export function fetchBatchDeleteSchoolRecruitMajor(recruitMajorIds: CommonType.IdType[]) {
return request<boolean>({
url: `/art/schoolRecruitMajor/${recruitMajorIds.join(',')}`,
method: 'delete'

View File

@ -20,6 +20,8 @@ declare namespace Api {
schoolId: CommonType.IdType;
/** 学校详细介绍(大文本) */
introduction: string;
/** 院校图标 */
schoolIcon: string;
/** 学校地址 */
address: string;
/** 联系电话 */
@ -28,6 +30,44 @@ declare namespace Api {
email: string;
/** 官网地址 */
website: string;
/** 邮编 */
postcode: string;
/** 建校时间(年) */
establishYear: number;
/** 占地面积(亩) */
campusAreaMu: number;
/** 图书馆藏书量 */
libraryCollection: number;
/** 男生比例(%) */
maleRatio: number;
/** 女生比例(%) */
femaleRatio: number;
/** 是否985(0/1) */
is985: number;
/** 是否211(0/1) */
is211: number;
/** 是否双一流(0/1) */
isDoubleFirstClass: number;
/** 是否重点大学(0/1) */
isKeyUniversity: number;
/** 标签 */
tags: string[];
/** 学生人数 */
studentCount: number;
/** 教师人数 */
teacherCount: number;
/** 硕士点数量 */
masterPoint: number;
/** 博士点数量 */
doctorPoint: number;
/** 重点专业数量 */
keyMajorCount: number;
/** 就业率(%) */
employmentRate: number;
/** 满意度(%) */
satisfactionRate: number;
/** 外部学校ID */
univId: number;
/** 删除标志0代表存在 1代表删除 */
delFlag: string;
/** 备注 */
@ -36,7 +76,36 @@ declare namespace Api {
/** school detail search params */
type SchoolDetailSearchParams = CommonType.RecordNullable<
Pick<Api.Art.SchoolDetail, 'schoolId' | 'introduction' | 'address' | 'contact' | 'email' | 'website'> &
Pick<
Api.Art.SchoolDetail,
| 'detailId'
| 'schoolId'
| 'introduction'
| 'schoolIcon'
| 'address'
| 'contact'
| 'email'
| 'website'
| 'postcode'
| 'establishYear'
| 'campusAreaMu'
| 'libraryCollection'
| 'maleRatio'
| 'femaleRatio'
| 'is985'
| 'is211'
| 'isDoubleFirstClass'
| 'isKeyUniversity'
| 'tags'
| 'studentCount'
| 'teacherCount'
| 'masterPoint'
| 'doctorPoint'
| 'keyMajorCount'
| 'employmentRate'
| 'satisfactionRate'
| 'univId'
> &
Api.Common.CommonSearchParams
>;
@ -44,7 +113,34 @@ declare namespace Api {
type SchoolDetailOperateParams = CommonType.RecordNullable<
Pick<
Api.Art.SchoolDetail,
'detailId' | 'schoolId' | 'introduction' | 'address' | 'contact' | 'email' | 'website' | 'remark'
| 'detailId'
| 'schoolId'
| 'introduction'
| 'schoolIcon'
| 'address'
| 'contact'
| 'email'
| 'website'
| 'postcode'
| 'establishYear'
| 'campusAreaMu'
| 'libraryCollection'
| 'maleRatio'
| 'femaleRatio'
| 'is985'
| 'is211'
| 'isDoubleFirstClass'
| 'isKeyUniversity'
| 'tags'
| 'studentCount'
| 'teacherCount'
| 'masterPoint'
| 'doctorPoint'
| 'keyMajorCount'
| 'employmentRate'
| 'satisfactionRate'
| 'univId'
| 'remark'
>
>;

View File

@ -26,8 +26,6 @@ declare namespace Api {
enrollCodes: string[];
/** 学校标签列表 */
schoolTags: string[];
/** 院校图标 */
schoolIcon: string;
/** 省份 */
province: string;
/** 城市 */
@ -42,14 +40,6 @@ declare namespace Api {
schoolNature: string;
/** 主管部门:教育部/工信部/民委... */
supervisorDept: string;
/** 建校时间(年) */
establishYear: number;
/** 占地面积(亩) */
campusAreaMu: number;
/** 男生比例(%) */
maleRatio: number;
/** 女生比例(%) */
femaleRatio: number;
/** 删除标志0代表存在 1代表删除 */
delFlag: string;
/** 备注 */
@ -64,7 +54,6 @@ declare namespace Api {
| 'mainCode'
| 'mainName'
| 'shortName'
| 'schoolIcon'
| 'province'
| 'city'
| 'district'
@ -72,10 +61,6 @@ declare namespace Api {
| 'educationLevel'
| 'schoolNature'
| 'supervisorDept'
| 'establishYear'
| 'campusAreaMu'
| 'maleRatio'
| 'femaleRatio'
> &
Api.Common.CommonSearchParams
>;
@ -88,7 +73,6 @@ declare namespace Api {
| 'mainCode'
| 'mainName'
| 'shortName'
| 'schoolIcon'
| 'province'
| 'city'
| 'district'
@ -96,10 +80,6 @@ declare namespace Api {
| 'educationLevel'
| 'schoolNature'
| 'supervisorDept'
| 'establishYear'
| 'campusAreaMu'
| 'maleRatio'
| 'femaleRatio'
| 'remark'
>
>;
@ -108,7 +88,34 @@ declare namespace Api {
type SchoolDetailInSchoolOperateParams = CommonType.RecordNullable<
Pick<
Api.Art.SchoolDetail,
'detailId' | 'schoolId' | 'introduction' | 'address' | 'contact' | 'email' | 'website' | 'remark'
| 'detailId'
| 'schoolId'
| 'introduction'
| 'schoolIcon'
| 'address'
| 'contact'
| 'email'
| 'website'
| 'postcode'
| 'establishYear'
| 'campusAreaMu'
| 'libraryCollection'
| 'maleRatio'
| 'femaleRatio'
| 'is985'
| 'is211'
| 'isDoubleFirstClass'
| 'isKeyUniversity'
| 'tags'
| 'studentCount'
| 'teacherCount'
| 'masterPoint'
| 'doctorPoint'
| 'keyMajorCount'
| 'employmentRate'
| 'satisfactionRate'
| 'univId'
| 'remark'
>
>;

View File

@ -16,7 +16,6 @@ declare module 'vue' {
BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
BooleanTag: typeof import('./../components/custom/boolean-tag.vue')['default']
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
copy: typeof import('./../components/custom/dict-radio copy.vue')['default']
CountTo: typeof import('./../components/custom/count-to.vue')['default']
DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default']
DataTable: typeof import('./../components/common/data-table.vue')['default']
@ -37,6 +36,7 @@ declare module 'vue' {
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
IconHugeiconsConfiguration01: typeof import('~icons/hugeicons/configuration01')['default']
IconIcRoundOpenInFull: typeof import('~icons/ic/round-open-in-full')['default']
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
IconLocalBanner: typeof import('~icons/local/banner')['default']
@ -62,6 +62,7 @@ declare module 'vue' {
IconSimpleIconsGitee: typeof import('~icons/simple-icons/gitee')['default']
IconTooltip: typeof import('./../components/common/icon-tooltip.vue')['default']
IconUilSearch: typeof import('~icons/uil/search')['default']
InlineExpandTextarea: typeof import('./../components/custom/inline-expand-textarea.vue')['default']
JsonPreview: typeof import('./../components/custom/json-preview.vue')['default']
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
LookForward: typeof import('./../components/custom/look-forward.vue')['default']
@ -168,7 +169,6 @@ declare global {
const BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
const BooleanTag: typeof import('./../components/custom/boolean-tag.vue')['default']
const ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
const copy: typeof import('./../components/custom/dict-radio copy.vue')['default']
const CountTo: typeof import('./../components/custom/count-to.vue')['default']
const DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default']
const DataTable: typeof import('./../components/common/data-table.vue')['default']
@ -189,6 +189,7 @@ declare global {
const IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
const IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
const IconHugeiconsConfiguration01: typeof import('~icons/hugeicons/configuration01')['default']
const IconIcRoundOpenInFull: typeof import('~icons/ic/round-open-in-full')['default']
const IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
const IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
const IconLocalBanner: typeof import('~icons/local/banner')['default']
@ -214,6 +215,7 @@ declare global {
const IconSimpleIconsGitee: typeof import('~icons/simple-icons/gitee')['default']
const IconTooltip: typeof import('./../components/common/icon-tooltip.vue')['default']
const IconUilSearch: typeof import('~icons/uil/search')['default']
const InlineExpandTextarea: typeof import('./../components/custom/inline-expand-textarea.vue')['default']
const JsonPreview: typeof import('./../components/custom/json-preview.vue')['default']
const LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
const LookForward: typeof import('./../components/custom/look-forward.vue')['default']

View File

@ -1,21 +1,26 @@
<script setup lang="tsx">
import { ref } from 'vue';
import { NDivider } from 'naive-ui';
import { fetchBatchDeleteHistoryScoreControlLine, fetchGetHistoryScoreControlLineList } from '@/service/api/art/history-score-control-line';
import { computed, ref } from 'vue';
import { NButton, NDivider, NInput, NSpace } from 'naive-ui';
import { jsonClone } from '@sa/utils';
import {
fetchBatchDeleteHistoryScoreControlLine,
fetchCreateHistoryScoreControlLine,
fetchGetHistoryScoreControlLineList,
fetchUpdateHistoryScoreControlLine
} from '@/service/api/art/history-score-control-line';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
import { useDownload } from '@/hooks/business/download';
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
import { defaultTransform, useNaivePaginatedTable } from '@/hooks/common/table';
import { $t } from '@/locales';
import ButtonIcon from '@/components/custom/button-icon.vue';
import HistoryScoreControlLineOperateDrawer from './modules/history-score-control-line-operate-drawer.vue';
import InlineExpandTextarea from '@/components/custom/inline-expand-textarea.vue';
import HistoryScoreControlLineSearch from './modules/history-score-control-line-search.vue';
defineOptions({
name: 'HistoryScoreControlLineList'
});
const appStore = useAppStore();
const { download } = useDownload();
const { hasAuth } = useAuth();
@ -36,6 +41,32 @@ const searchParams = ref<Api.Art.HistoryScoreControlLineSearchParams>({
params: {}
});
type TableRow = Api.Art.HistoryScoreControlLine & { tempKey?: string };
type Model = Api.Art.HistoryScoreControlLineOperateParams;
type EditableField = Extract<keyof Model, keyof TableRow>;
const editingModel = ref<Model>(createDefaultModel());
const editingSnapshot = ref<TableRow | null>(null);
const editingMode = ref<NaiveUI.TableOperateType | null>(null);
const editingRowKey = ref<string | null>(null);
const savingRowKey = ref<string | null>(null);
const tempRow = ref<TableRow | null>(null);
const checkedRowKeys = ref<CommonType.IdType[]>([]);
const editableColumns: Array<{ key: EditableField; title: string; textarea?: boolean }> = [
{ key: 'provinceCode', title: '省份行政区划代码' },
{ key: 'provinceName', title: '省份名称' },
{ key: 'year', title: '年份' },
{ key: 'majorCategory', title: '专业类别' },
{ key: 'batchName', title: '批次' },
{ key: 'subjectType', title: '科类(文/理)' },
{ key: 'cultureScore', title: '文化成绩分数' },
{ key: 'majorScore', title: '专业成绩分数' },
{ key: 'cultureScoreExam', title: '文化成绩校考分数' },
{ key: 'majorScoreExam', title: '专业成绩校考分数' },
{ key: 'remark', title: '备注', textarea: true }
];
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination, scrollX } =
useNaivePaginatedTable({
api: () => fetchGetHistoryScoreControlLineList(searchParams.value),
@ -44,7 +75,16 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
searchParams.value.pageNum = params.page;
searchParams.value.pageSize = params.pageSize;
},
columns: () => [
columns: () => createColumns()
});
const tableData = computed<TableRow[]>(() => {
const rows = data.value as TableRow[];
return tempRow.value ? [tempRow.value, ...rows] : rows;
});
function createColumns(): NaiveUI.TableColumn<TableRow>[] {
return [
{
type: 'selection',
align: 'center',
@ -63,105 +103,58 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
align: 'center',
minWidth: 120
},
{
key: 'provinceCode',
title: '省份行政区划代码',
...editableColumns.map(column => createEditableColumn(column)),
createOperateColumn()
];
}
function createEditableColumn(column: { key: EditableField; title: string; textarea?: boolean }) {
return {
key: column.key,
title: column.title,
align: 'center',
minWidth: 120
},
{
key: 'provinceName',
title: '省份名称',
align: 'center',
minWidth: 120
},
{
key: 'year',
title: '年份',
align: 'center',
minWidth: 120
},
{
key: 'majorCategory',
title: '专业类别',
align: 'center',
minWidth: 120
},
{
key: 'batchName',
title: '批次',
align: 'center',
minWidth: 120
},
{
key: 'subjectType',
title: '科类(文/理)',
align: 'center',
minWidth: 120
},
{
key: 'cultureScore',
title: '文化成绩分数',
align: 'center',
minWidth: 120
},
{
key: 'majorScore',
title: '专业成绩分数',
align: 'center',
minWidth: 120
},
{
key: 'cultureScoreExam',
title: '文化成绩校考分数',
align: 'center',
minWidth: 120
},
{
key: 'majorScoreExam',
title: '专业成绩校考分数',
align: 'center',
minWidth: 120
},
{
key: 'remark',
title: '备注',
align: 'center',
minWidth: 120
},
{
minWidth: 140,
render: (row: TableRow) => renderEditableCell(row, column.key, column)
} satisfies NaiveUI.TableColumn<TableRow>;
}
function createOperateColumn(): NaiveUI.TableColumn<TableRow> {
return {
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => {
const divider = () => {
if (!hasAuth('art:historyScoreControlLine:edit') || !hasAuth('art:historyScoreControlLine:remove')) {
return null;
}
return <NDivider vertical />;
};
fixed: 'right',
width: 180,
render: (row: TableRow) => {
const rowKey = resolveRowKey(row);
const editing = editingRowKey.value === rowKey;
const saving = savingRowKey.value === rowKey;
const editBtn = () => {
if (!hasAuth('art:historyScoreControlLine:edit')) {
return null;
}
if (editing) {
return (
<ButtonIcon
text
type="primary"
icon="material-symbols:drive-file-rename-outline-outline"
tooltipContent={$t('common.edit')}
onClick={() => edit(row.controlId)}
/>
<NSpace size={8} justify="center">
<NButton size="tiny" quaternary disabled={saving} onClick={handleCancelEdit}>
{$t('common.cancel')}
</NButton>
<NButton size="tiny" type="primary" loading={saving} onClick={handleSaveRow}>
{$t('common.save')}
</NButton>
</NSpace>
);
};
const deleteBtn = () => {
if (!hasAuth('art:historyScoreControlLine:remove')) {
return null;
}
const showEdit = hasAuth('art:historyScoreControlLine:edit');
const showDelete = hasAuth('art:historyScoreControlLine:remove');
return (
<div class="flex-center gap-8px">
{showEdit ? (
<NButton size="tiny" text type="primary" onClick={() => handleEditRow(row)}>
{$t('common.edit')}
</NButton>
) : null}
{showEdit && showDelete ? <NDivider vertical /> : null}
{showDelete ? (
<ButtonIcon
text
type="error"
@ -170,50 +163,281 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
popconfirmContent={$t('common.confirmDelete')}
onPositiveClick={() => handleDelete(row.controlId)}
/>
);
};
return (
<div class="flex-center gap-8px">
{editBtn()}
{divider()}
{deleteBtn()}
) : null}
</div>
);
}
}
]
});
};
}
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
useTableOperate(data, 'controlId', getData);
function getRowFieldValue(row: TableRow, field: EditableField) {
return row[field as keyof TableRow];
}
function formatDisplayValue(value: unknown) {
if (value === null || value === undefined || value === '') {
return '-';
}
return typeof value === 'number' ? value : String(value);
}
function renderEditableCell(row: TableRow, field: EditableField, options?: { textarea?: boolean }) {
if (!isEditingRow(row)) {
return formatDisplayValue(getRowFieldValue(row, field));
}
const inputValue = editingModel.value[field];
const resolvedValue = inputValue === null || inputValue === undefined ? '' : String(inputValue);
if (options?.textarea) {
return renderTextareaEditor(row, field, resolvedValue);
}
return (
<NInput
size="small"
type={options?.textarea ? 'textarea' : 'text'}
autosize={options?.textarea ? { minRows: 1, maxRows: 4 } : undefined}
value={resolvedValue}
onUpdateValue={value => updateEditingField(field, value)}
/>
);
}
function renderTextareaEditor(row: TableRow, field: EditableField, value: string) {
return <InlineExpandTextarea value={value} onUpdateValue={val => updateEditingField(field, val)} />;
}
function updateEditingField(field: EditableField, value: string | number | null) {
(editingModel.value as Record<string, string | number | null>)[field] = value;
}
function resolveRowKey(row: TableRow) {
if (row.controlId !== null && row.controlId !== undefined && row.controlId !== '') {
return String(row.controlId);
}
return row.tempKey ?? '';
}
function isEditingRow(row: TableRow) {
return editingRowKey.value !== null && editingRowKey.value === resolveRowKey(row);
}
function createDefaultModel(): Model {
return {
controlId: null,
provinceCode: '',
provinceName: '',
year: null,
majorCategory: '',
batchName: '',
subjectType: '',
cultureScore: null,
majorScore: null,
cultureScoreExam: null,
majorScoreExam: null,
remark: ''
};
}
function createTempKey() {
return `temp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
function ensureEditingGuard(action: () => void | Promise<void>) {
const executeAction = () => Promise.resolve(action());
if (!editingRowKey.value) {
return executeAction();
}
if (!window.$dialog) {
resetEditingState();
return executeAction();
}
return new Promise<void>((resolve, reject) => {
window.$dialog?.warning({
title: '提示',
content: '当前行尚未保存,确定放弃修改吗?',
positiveText: '放弃',
negativeText: '继续编辑',
onPositiveClick: async () => {
resetEditingState();
try {
await executeAction();
resolve();
} catch (error) {
reject(error);
}
},
onNegativeClick: () => resolve()
});
});
}
function resetEditingState() {
editingModel.value = createDefaultModel();
editingSnapshot.value = null;
editingMode.value = null;
editingRowKey.value = null;
savingRowKey.value = null;
tempRow.value = null;
}
async function handleAddRow() {
await ensureEditingGuard(() => {
editingMode.value = 'add';
editingModel.value = createDefaultModel();
editingSnapshot.value = null;
const key = createTempKey();
tempRow.value = { ...(editingModel.value as TableRow), tempKey: key };
editingRowKey.value = key;
checkedRowKeys.value = [];
});
}
async function handleEditRow(row: TableRow) {
await ensureEditingGuard(() => {
editingMode.value = 'edit';
tempRow.value = null;
editingRowKey.value = resolveRowKey(row);
editingSnapshot.value = jsonClone(row);
editingModel.value = Object.assign(createDefaultModel(), jsonClone(row));
});
}
function handleCancelEdit() {
resetEditingState();
}
const requiredFields: Array<{ key: EditableField; label: string }> = [
{ key: 'provinceCode', label: '省份行政区划代码' },
{ key: 'provinceName', label: '省份名称' },
{ key: 'year', label: '年份' },
{ key: 'majorCategory', label: '专业类别' },
{ key: 'batchName', label: '批次' },
{ key: 'subjectType', label: '科类(文/理)' }
];
function validateModel() {
for (const item of requiredFields) {
const value = editingModel.value[item.key];
if (value === null || value === undefined || value === '') {
window.$message?.warning(`${item.label}不能为空`);
return false;
}
}
return true;
}
async function handleSaveRow() {
if (!editingMode.value) return;
if (!validateModel()) return;
const {
controlId,
provinceCode,
provinceName,
year,
majorCategory,
batchName,
subjectType,
cultureScore,
majorScore,
cultureScoreExam,
majorScoreExam,
remark
} = editingModel.value;
savingRowKey.value = editingRowKey.value;
const requestResult =
editingMode.value === 'add'
? await fetchCreateHistoryScoreControlLine({
provinceCode,
provinceName,
year,
majorCategory,
batchName,
subjectType,
cultureScore,
majorScore,
cultureScoreExam,
majorScoreExam,
remark
})
: await fetchUpdateHistoryScoreControlLine({
controlId,
provinceCode,
provinceName,
year,
majorCategory,
batchName,
subjectType,
cultureScore,
majorScore,
cultureScoreExam,
majorScoreExam,
remark
});
savingRowKey.value = null;
if (requestResult.error) {
window.$message?.error(requestResult.error.message || '保存失败');
if (editingMode.value === 'edit' && editingSnapshot.value) {
editingModel.value = Object.assign(createDefaultModel(), jsonClone(editingSnapshot.value));
}
return;
}
window.$message?.success($t('common.updateSuccess'));
resetEditingState();
await getData();
}
async function handleBatchDelete() {
// request
await ensureEditingGuard(async () => {
const { error } = await fetchBatchDeleteHistoryScoreControlLine(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
window.$message?.success($t('common.deleteSuccess'));
checkedRowKeys.value = [];
await getData();
});
}
async function handleDelete(controlId: CommonType.IdType) {
// request
await ensureEditingGuard(async () => {
const { error } = await fetchBatchDeleteHistoryScoreControlLine([controlId]);
if (error) return;
onDeleted();
}
function edit(controlId: CommonType.IdType) {
handleEdit(controlId);
window.$message?.success($t('common.deleteSuccess'));
await getData();
});
}
function handleExport() {
download('/art/historyScoreControlLine/export', searchParams.value, `历年省控线_${new Date().getTime()}.xlsx`);
}
async function handleSearch() {
await ensureEditingGuard(async () => {
await getDataByPage();
});
}
async function handleRefresh() {
await ensureEditingGuard(getData);
}
function rowClassName(row: TableRow) {
return isEditingRow(row) ? 'inline-edit-row' : '';
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<HistoryScoreControlLineSearch v-model:model="searchParams" @search="getDataByPage" />
<HistoryScoreControlLineSearch v-model:model="searchParams" @search="handleSearch" />
<NCard title="历年省控线列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
@ -223,33 +447,32 @@ function handleExport() {
:show-add="hasAuth('art:historyScoreControlLine:add')"
:show-delete="hasAuth('art:historyScoreControlLine:remove')"
:show-export="hasAuth('art:historyScoreControlLine:export')"
@add="handleAdd"
@add="handleAddRow"
@delete="handleBatchDelete"
@export="handleExport"
@refresh="getData"
@refresh="handleRefresh"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
:data="tableData"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="scrollX"
:loading="loading"
remote
:row-key="row => row.controlId"
:row-key="resolveRowKey"
:row-class-name="rowClassName"
:pagination="mobilePagination"
class="sm:h-full"
/>
<HistoryScoreControlLineOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getDataByPage"
/>
</NCard>
</div>
</template>
<style scoped></style>
<style scoped>
:deep(.inline-edit-row .n-data-table-td) {
background-color: rgba(24, 160, 88, 0.08);
}
</style>

View File

@ -1,7 +1,10 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { jsonClone } from '@sa/utils';
import { fetchCreateHistoryScoreControlLine, fetchUpdateHistoryScoreControlLine } from '@/service/api/art/history-score-control-line';
import {
fetchCreateHistoryScoreControlLine,
fetchUpdateHistoryScoreControlLine
} from '@/service/api/art/history-score-control-line';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
@ -82,7 +85,7 @@ const rules: Record<RuleKey, App.Global.FormRule> = {
year: createRequiredRule('年份不能为空'),
majorCategory: createRequiredRule('专业类别不能为空'),
batchName: createRequiredRule('批次不能为空'),
subjectType: createRequiredRule('科类(文/理)不能为空'),
subjectType: createRequiredRule('科类(文/理)不能为空')
};
function handleUpdateModelWhenEdit() {
@ -100,16 +103,54 @@ function closeDrawer() {
async function handleSubmit() {
await validate();
const { controlId, provinceCode, provinceName, year, majorCategory, batchName, subjectType, cultureScore, majorScore, cultureScoreExam, majorScoreExam, remark } = model.value;
const {
controlId,
provinceCode,
provinceName,
year,
majorCategory,
batchName,
subjectType,
cultureScore,
majorScore,
cultureScoreExam,
majorScoreExam,
remark
} = model.value;
// request
if (props.operateType === 'add') {
const { error } = await fetchCreateHistoryScoreControlLine({ provinceCode, provinceName, year, majorCategory, batchName, subjectType, cultureScore, majorScore, cultureScoreExam, majorScoreExam, remark });
const { error } = await fetchCreateHistoryScoreControlLine({
provinceCode,
provinceName,
year,
majorCategory,
batchName,
subjectType,
cultureScore,
majorScore,
cultureScoreExam,
majorScoreExam,
remark
});
if (error) return;
}
if (props.operateType === 'edit') {
const { error } = await fetchUpdateHistoryScoreControlLine({ controlId, provinceCode, provinceName, year, majorCategory, batchName, subjectType, cultureScore, majorScore, cultureScoreExam, majorScoreExam, remark });
const { error } = await fetchUpdateHistoryScoreControlLine({
controlId,
provinceCode,
provinceName,
year,
majorCategory,
batchName,
subjectType,
cultureScore,
majorScore,
cultureScoreExam,
majorScoreExam,
remark
});
if (error) return;
}
@ -167,12 +208,7 @@ watch(visible, () => {
<NInput v-model:value="model.majorScoreExam" placeholder="请输入专业成绩校考分数" />
</NFormItem>
<NFormItem label="备注" path="remark">
<NInput
v-model:value="model.remark"
:rows="3"
type="textarea"
placeholder="请输入备注"
/>
<NInput v-model:value="model.remark" :rows="3" type="textarea" placeholder="请输入备注" />
</NFormItem>
</NForm>
<template #footer>

View File

@ -42,7 +42,13 @@ async function search() {
<NCollapseItem :title="$t('common.search')" name="art-history-score-control-line-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" label="省份行政区划代码" label-width="auto" path="provinceCode" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="省份行政区划代码"
label-width="auto"
path="provinceCode"
class="pr-24px"
>
<NInput v-model:value="model.provinceCode" placeholder="请输入省份行政区划代码" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="省份名称" label-width="auto" path="provinceName" class="pr-24px">
@ -58,12 +64,7 @@ async function search() {
<NInput v-model:value="model.batchName" placeholder="请输入批次" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="科类(文/理)" label-width="auto" path="subjectType" class="pr-24px">
<NSelect
v-model:value="model.subjectType"
placeholder="请选择科类(文/理)"
:options="[]"
clearable
/>
<NSelect v-model:value="model.subjectType" placeholder="请选择科类(文/理)" :options="[]" clearable />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="文化成绩分数" label-width="auto" path="cultureScore" class="pr-24px">
<NInput v-model:value="model.cultureScore" placeholder="请输入文化成绩分数" />
@ -71,10 +72,22 @@ async function search() {
<NFormItemGi span="24 s:12 m:6" label="专业成绩分数" label-width="auto" path="majorScore" class="pr-24px">
<NInput v-model:value="model.majorScore" placeholder="请输入专业成绩分数" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="文化成绩校考分数" label-width="auto" path="cultureScoreExam" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="文化成绩校考分数"
label-width="auto"
path="cultureScoreExam"
class="pr-24px"
>
<NInput v-model:value="model.cultureScoreExam" placeholder="请输入文化成绩校考分数" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="专业成绩校考分数" label-width="auto" path="majorScoreExam" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="专业成绩校考分数"
label-width="auto"
path="majorScoreExam"
class="pr-24px"
>
<NInput v-model:value="model.majorScoreExam" placeholder="请输入专业成绩校考分数" />
</NFormItemGi>
<NFormItemGi :show-feedback="false" span="24" class="pr-24px">

View File

@ -1,21 +1,21 @@
<script setup lang="tsx">
import { ref } from 'vue';
import { NDivider } from 'naive-ui';
import { fetchBatchDeleteMajor, fetchGetMajorList } from '@/service/api/art/major';
import { computed, ref } from 'vue';
import { NButton, NDivider, NInput, NSpace } from 'naive-ui';
import { jsonClone } from '@sa/utils';
import { fetchBatchDeleteMajor, fetchCreateMajor, fetchGetMajorList, fetchUpdateMajor } from '@/service/api/art/major';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
import { useDownload } from '@/hooks/business/download';
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
import { defaultTransform, useNaivePaginatedTable } from '@/hooks/common/table';
import { $t } from '@/locales';
import ButtonIcon from '@/components/custom/button-icon.vue';
import MajorOperateDrawer from './modules/major-operate-drawer.vue';
import InlineExpandTextarea from '@/components/custom/inline-expand-textarea.vue';
import MajorSearch from './modules/major-search.vue';
defineOptions({
name: 'MajorList'
});
const appStore = useAppStore();
const { download } = useDownload();
const { hasAuth } = useAuth();
@ -36,6 +36,18 @@ const searchParams = ref<Api.Art.MajorSearchParams>({
params: {}
});
type TableRow = Api.Art.Major & { tempKey?: string };
type Model = Api.Art.MajorOperateParams;
type EditableField = Extract<keyof Model, keyof TableRow>;
const editingModel = ref<Model>(createDefaultModel());
const editingSnapshot = ref<TableRow | null>(null);
const editingMode = ref<NaiveUI.TableOperateType | null>(null);
const editingRowKey = ref<string | null>(null);
const savingRowKey = ref<string | null>(null);
const tempRow = ref<TableRow | null>(null);
const checkedRowKeys = ref<CommonType.IdType[]>([]);
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination, scrollX } =
useNaivePaginatedTable({
api: () => fetchGetMajorList(searchParams.value),
@ -44,7 +56,16 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
searchParams.value.pageNum = params.page;
searchParams.value.pageSize = params.pageSize;
},
columns: () => [
columns: () => createColumns()
});
const tableData = computed<TableRow[]>(() => {
const rows = data.value as TableRow[];
return tempRow.value ? [tempRow.value, ...rows] : rows;
});
function createColumns(): NaiveUI.TableColumn<TableRow>[] {
return [
{
type: 'selection',
align: 'center',
@ -63,105 +84,68 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
align: 'center',
minWidth: 120
},
{
key: 'majorName',
title: '专业名称',
createEditableColumn('majorName', '专业名称'),
createEditableColumn('educationLevel', '学历层次'),
createEditableColumn('majorIcon', '专业图标'),
createEditableColumn('schoolingYears', '学制(年)'),
createEditableColumn('disciplinePrimary', '所属一级学科'),
createEditableColumn('disciplineSecondary', '所属二级学科'),
createEditableColumn('degreeAwarded', '授予学士学位'),
createEditableColumn('summary', '专业概括', { textarea: true }),
createEditableColumn('trainingDirection', '培养方向', { textarea: true }),
createEditableColumn('coreCourses', '主要课程', { textarea: true }),
createEditableColumn('remark', '备注', { textarea: true }),
createOperateColumn()
];
}
function createEditableColumn(key: EditableField, title: string, options?: { textarea?: boolean; minWidth?: number }) {
return {
key,
title,
align: 'center',
minWidth: 120
},
{
key: 'educationLevel',
title: '学历层次',
align: 'center',
minWidth: 120
},
{
key: 'majorIcon',
title: '专业图标',
align: 'center',
minWidth: 120
},
{
key: 'schoolingYears',
title: '学制(年)',
align: 'center',
minWidth: 120
},
{
key: 'disciplinePrimary',
title: '所属一级学科',
align: 'center',
minWidth: 120
},
{
key: 'disciplineSecondary',
title: '所属二级学科',
align: 'center',
minWidth: 120
},
{
key: 'degreeAwarded',
title: '授予学士学位',
align: 'center',
minWidth: 120
},
{
key: 'summary',
title: '专业概括',
align: 'center',
minWidth: 120
},
{
key: 'trainingDirection',
title: '培养方向',
align: 'center',
minWidth: 120
},
{
key: 'coreCourses',
title: '主要课程',
align: 'center',
minWidth: 120
},
{
key: 'remark',
title: '备注',
align: 'center',
minWidth: 120
},
{
minWidth: options?.minWidth ?? 120,
render: (row: TableRow) => renderEditableCell(row, key, options)
} satisfies NaiveUI.TableColumn<TableRow>;
}
function createOperateColumn(): NaiveUI.TableColumn<TableRow> {
return {
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => {
const divider = () => {
if (!hasAuth('art:major:edit') || !hasAuth('art:major:remove')) {
return null;
}
return <NDivider vertical />;
};
fixed: 'right',
width: 100,
render: (row: TableRow) => {
const rowKey = resolveRowKey(row);
const editing = editingRowKey.value === rowKey;
const saving = savingRowKey.value === rowKey;
const editBtn = () => {
if (!hasAuth('art:major:edit')) {
return null;
}
if (editing) {
return (
<ButtonIcon
text
type="primary"
icon="material-symbols:drive-file-rename-outline-outline"
tooltipContent={$t('common.edit')}
onClick={() => edit(row.majorId)}
/>
<NSpace size={8} justify="center">
<NButton size="tiny" quaternary disabled={saving} onClick={handleCancelEdit}>
{$t('common.cancel')}
</NButton>
<NButton size="tiny" type="primary" loading={saving} onClick={handleSaveRow}>
{$t('common.save')}
</NButton>
</NSpace>
);
};
const deleteBtn = () => {
if (!hasAuth('art:major:remove')) {
return null;
}
const showEdit = hasAuth('art:major:edit');
const showDelete = hasAuth('art:major:remove');
return (
<div class="flex-center gap-8px">
{showEdit ? (
<NButton size="tiny" text type="primary" onClick={() => handleEditRow(row)}>
{$t('common.edit')}
</NButton>
) : null}
{showEdit && showDelete ? <NDivider vertical /> : null}
{showDelete ? (
<ButtonIcon
text
type="error"
@ -170,50 +154,277 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
popconfirmContent={$t('common.confirmDelete')}
onPositiveClick={() => handleDelete(row.majorId)}
/>
);
};
return (
<div class="flex-center gap-8px">
{editBtn()}
{divider()}
{deleteBtn()}
) : null}
</div>
);
}
}
]
});
};
}
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
useTableOperate(data, 'majorId', getData);
function getRowFieldValue(row: TableRow, field: EditableField) {
return row[field as keyof TableRow];
}
function formatDisplayValue(value: unknown) {
if (value === null || value === undefined || value === '') {
return '-';
}
return typeof value === 'number' ? value : String(value);
}
function renderEditableCell(row: TableRow, field: EditableField, options?: { textarea?: boolean }) {
if (!isEditingRow(row)) {
return formatDisplayValue(getRowFieldValue(row, field));
}
const inputValue = editingModel.value[field];
const resolvedValue = inputValue === null || inputValue === undefined ? '' : String(inputValue);
if (options?.textarea) {
return renderTextareaEditor(row, field, resolvedValue);
}
return (
<NInput
size="small"
type={options?.textarea ? 'textarea' : 'text'}
autosize={options?.textarea ? { minRows: 1, maxRows: 4 } : undefined}
value={resolvedValue}
onUpdateValue={value => updateEditingField(field, value)}
/>
);
}
function renderTextareaEditor(row: TableRow, field: EditableField, value: string) {
return <InlineExpandTextarea value={value} onUpdateValue={val => updateEditingField(field, val)} />;
}
function updateEditingField(field: EditableField, value: string | number | null) {
(editingModel.value as Record<string, string | number | null>)[field] = value;
}
function resolveRowKey(row: TableRow) {
if (row.majorId !== null && row.majorId !== undefined && row.majorId !== '') {
return String(row.majorId);
}
return row.tempKey ?? '';
}
function isEditingRow(row: TableRow) {
return editingRowKey.value !== null && editingRowKey.value === resolveRowKey(row);
}
function createDefaultModel(): Model {
return {
majorId: null,
majorName: '',
educationLevel: '',
majorIcon: '',
schoolingYears: null,
disciplinePrimary: '',
disciplineSecondary: '',
degreeAwarded: '',
summary: '',
trainingDirection: '',
coreCourses: '',
remark: ''
};
}
function createTempKey() {
return `temp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
function ensureEditingGuard(action: () => void | Promise<void>) {
const executeAction = () => Promise.resolve(action());
if (!editingRowKey.value) {
return executeAction();
}
if (!window.$dialog) {
resetEditingState();
return executeAction();
}
return new Promise<void>((resolve, reject) => {
window.$dialog?.warning({
title: '提示',
content: '当前行尚未保存,确定放弃修改吗?',
positiveText: '放弃',
negativeText: '继续编辑',
onPositiveClick: async () => {
resetEditingState();
try {
await executeAction();
resolve();
} catch (error) {
reject(error);
}
},
onNegativeClick: () => resolve()
});
});
}
function resetEditingState() {
editingModel.value = createDefaultModel();
editingSnapshot.value = null;
editingMode.value = null;
editingRowKey.value = null;
savingRowKey.value = null;
tempRow.value = null;
}
async function handleAddRow() {
await ensureEditingGuard(() => {
editingMode.value = 'add';
editingModel.value = createDefaultModel();
editingSnapshot.value = null;
const key = createTempKey();
tempRow.value = { ...(editingModel.value as TableRow), tempKey: key };
editingRowKey.value = key;
checkedRowKeys.value = [];
});
}
async function handleEditRow(row: TableRow) {
await ensureEditingGuard(() => {
editingMode.value = 'edit';
tempRow.value = null;
editingRowKey.value = resolveRowKey(row);
editingSnapshot.value = jsonClone(row);
editingModel.value = Object.assign(createDefaultModel(), jsonClone(row));
});
}
function handleCancelEdit() {
resetEditingState();
}
const requiredFields: Array<{ key: EditableField; label: string }> = [
{ key: 'majorName', label: '专业名称' },
{ key: 'educationLevel', label: '学历层次' }
];
function validateModel() {
for (const item of requiredFields) {
const value = editingModel.value[item.key];
if (value === null || value === undefined || value === '') {
window.$message?.warning(`${item.label}不能为空`);
return false;
}
}
return true;
}
async function handleSaveRow() {
if (!editingMode.value) return;
if (!validateModel()) return;
const {
majorId,
majorName,
educationLevel,
majorIcon,
schoolingYears,
disciplinePrimary,
disciplineSecondary,
degreeAwarded,
summary,
trainingDirection,
coreCourses,
remark
} = editingModel.value;
savingRowKey.value = editingRowKey.value;
const requestResult =
editingMode.value === 'add'
? await fetchCreateMajor({
majorName,
educationLevel,
majorIcon,
schoolingYears,
disciplinePrimary,
disciplineSecondary,
degreeAwarded,
summary,
trainingDirection,
coreCourses,
remark
})
: await fetchUpdateMajor({
majorId,
majorName,
educationLevel,
majorIcon,
schoolingYears,
disciplinePrimary,
disciplineSecondary,
degreeAwarded,
summary,
trainingDirection,
coreCourses,
remark
});
savingRowKey.value = null;
if (requestResult.error) {
window.$message?.error(requestResult.error.message || '保存失败');
if (editingMode.value === 'edit' && editingSnapshot.value) {
editingModel.value = Object.assign(createDefaultModel(), jsonClone(editingSnapshot.value));
}
return;
}
window.$message?.success($t('common.updateSuccess'));
resetEditingState();
await getData();
}
async function handleBatchDelete() {
// request
await ensureEditingGuard(async () => {
const { error } = await fetchBatchDeleteMajor(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
window.$message?.success($t('common.deleteSuccess'));
checkedRowKeys.value = [];
await getData();
});
}
async function handleDelete(majorId: CommonType.IdType) {
// request
await ensureEditingGuard(async () => {
const { error } = await fetchBatchDeleteMajor([majorId]);
if (error) return;
onDeleted();
}
function edit(majorId: CommonType.IdType) {
handleEdit(majorId);
window.$message?.success($t('common.deleteSuccess'));
await getData();
});
}
function handleExport() {
download('/art/major/export', searchParams.value, `艺术专业库_${new Date().getTime()}.xlsx`);
}
async function handleSearch() {
await ensureEditingGuard(async () => {
await getDataByPage();
});
}
async function handleRefresh() {
await ensureEditingGuard(getData);
}
function rowClassName(row: TableRow) {
return isEditingRow(row) ? 'inline-edit-row' : '';
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<MajorSearch v-model:model="searchParams" @search="getDataByPage" />
<MajorSearch v-model:model="searchParams" @search="handleSearch" />
<NCard title="艺术专业库列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
@ -223,33 +434,32 @@ function handleExport() {
:show-add="hasAuth('art:major:add')"
:show-delete="hasAuth('art:major:remove')"
:show-export="hasAuth('art:major:export')"
@add="handleAdd"
@add="handleAddRow"
@delete="handleBatchDelete"
@export="handleExport"
@refresh="getData"
@refresh="handleRefresh"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
:data="tableData"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="scrollX"
:loading="loading"
remote
:row-key="row => row.majorId"
:row-key="resolveRowKey"
:row-class-name="rowClassName"
:pagination="mobilePagination"
class="sm:h-full"
/>
<MajorOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getDataByPage"
/>
</NCard>
</div>
</template>
<style scoped></style>
<style scoped>
:deep(.inline-edit-row .n-data-table-td) {
background-color: rgba(24, 160, 88, 0.08);
}
</style>

View File

@ -60,21 +60,14 @@ function createDefaultModel(): Model {
};
}
type RuleKey = Extract<
keyof Model,
| 'majorId'
| 'tenantId'
| 'delFlag'
| 'majorName'
| 'educationLevel'
>;
type RuleKey = Extract<keyof Model, 'majorId' | 'tenantId' | 'delFlag' | 'majorName' | 'educationLevel'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
majorId: createRequiredRule('专业主键ID不能为空'),
tenantId: createRequiredRule('租户编号不能为空'),
delFlag: createRequiredRule('删除标志0存在 1删除不能为空'),
majorName: createRequiredRule('专业名称不能为空'),
educationLevel: createRequiredRule('学历层次不能为空'),
educationLevel: createRequiredRule('学历层次不能为空')
};
function handleUpdateModelWhenEdit() {
@ -92,16 +85,54 @@ function closeDrawer() {
async function handleSubmit() {
await validate();
const { majorId, majorName, educationLevel, majorIcon, schoolingYears, disciplinePrimary, disciplineSecondary, degreeAwarded, summary, trainingDirection, coreCourses, remark } = model.value;
const {
majorId,
majorName,
educationLevel,
majorIcon,
schoolingYears,
disciplinePrimary,
disciplineSecondary,
degreeAwarded,
summary,
trainingDirection,
coreCourses,
remark
} = model.value;
// request
if (props.operateType === 'add') {
const { error } = await fetchCreateMajor({ majorName, educationLevel, majorIcon, schoolingYears, disciplinePrimary, disciplineSecondary, degreeAwarded, summary, trainingDirection, coreCourses, remark });
const { error } = await fetchCreateMajor({
majorName,
educationLevel,
majorIcon,
schoolingYears,
disciplinePrimary,
disciplineSecondary,
degreeAwarded,
summary,
trainingDirection,
coreCourses,
remark
});
if (error) return;
}
if (props.operateType === 'edit') {
const { error } = await fetchUpdateMajor({ majorId, majorName, educationLevel, majorIcon, schoolingYears, disciplinePrimary, disciplineSecondary, degreeAwarded, summary, trainingDirection, coreCourses, remark });
const { error } = await fetchUpdateMajor({
majorId,
majorName,
educationLevel,
majorIcon,
schoolingYears,
disciplinePrimary,
disciplineSecondary,
degreeAwarded,
summary,
trainingDirection,
coreCourses,
remark
});
if (error) return;
}
@ -145,36 +176,16 @@ watch(visible, () => {
<NInput v-model:value="model.degreeAwarded" placeholder="请输入授予学士学位" />
</NFormItem>
<NFormItem label="专业概括" path="summary">
<NInput
v-model:value="model.summary"
:rows="3"
type="textarea"
placeholder="请输入专业概括"
/>
<NInput v-model:value="model.summary" :rows="3" type="textarea" placeholder="请输入专业概括" />
</NFormItem>
<NFormItem label="培养方向" path="trainingDirection">
<NInput
v-model:value="model.trainingDirection"
:rows="3"
type="textarea"
placeholder="请输入培养方向"
/>
<NInput v-model:value="model.trainingDirection" :rows="3" type="textarea" placeholder="请输入培养方向" />
</NFormItem>
<NFormItem label="主要课程" path="coreCourses">
<NInput
v-model:value="model.coreCourses"
:rows="3"
type="textarea"
placeholder="请输入主要课程"
/>
<NInput v-model:value="model.coreCourses" :rows="3" type="textarea" placeholder="请输入主要课程" />
</NFormItem>
<NFormItem label="备注" path="remark">
<NInput
v-model:value="model.remark"
:rows="3"
type="textarea"
placeholder="请输入备注"
/>
<NInput v-model:value="model.remark" :rows="3" type="textarea" placeholder="请输入备注" />
</NFormItem>
</NForm>
<template #footer>

View File

@ -54,19 +54,43 @@ async function search() {
<NFormItemGi span="24 s:12 m:6" label="学制(年)" label-width="auto" path="schoolingYears" class="pr-24px">
<NInput v-model:value="model.schoolingYears" placeholder="请输入学制(年)" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="所属一级学科" label-width="auto" path="disciplinePrimary" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="所属一级学科"
label-width="auto"
path="disciplinePrimary"
class="pr-24px"
>
<NInput v-model:value="model.disciplinePrimary" placeholder="请输入所属一级学科" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="所属二级学科" label-width="auto" path="disciplineSecondary" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="所属二级学科"
label-width="auto"
path="disciplineSecondary"
class="pr-24px"
>
<NInput v-model:value="model.disciplineSecondary" placeholder="请输入所属二级学科" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="授予学士学位" label-width="auto" path="degreeAwarded" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="授予学士学位"
label-width="auto"
path="degreeAwarded"
class="pr-24px"
>
<NInput v-model:value="model.degreeAwarded" placeholder="请输入授予学士学位" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="专业概括" label-width="auto" path="summary" class="pr-24px">
<NInput v-model:value="model.summary" placeholder="请输入专业概括" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="培养方向" label-width="auto" path="trainingDirection" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="培养方向"
label-width="auto"
path="trainingDirection"
class="pr-24px"
>
<NInput v-model:value="model.trainingDirection" placeholder="请输入培养方向" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="主要课程" label-width="auto" path="coreCourses" class="pr-24px">

View File

@ -1,21 +1,26 @@
<script setup lang="tsx">
import { ref } from 'vue';
import { NDivider } from 'naive-ui';
import { fetchBatchDeleteSchoolRecruitMajorHistory, fetchGetSchoolRecruitMajorHistoryList } from '@/service/api/art/school-recruit-major-history';
import { computed, ref } from 'vue';
import { NButton, NDivider, NInput, NSpace } from 'naive-ui';
import { jsonClone } from '@sa/utils';
import {
fetchBatchDeleteSchoolRecruitMajorHistory,
fetchCreateSchoolRecruitMajorHistory,
fetchGetSchoolRecruitMajorHistoryList,
fetchUpdateSchoolRecruitMajorHistory
} from '@/service/api/art/school-recruit-major-history';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
import { useDownload } from '@/hooks/business/download';
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
import { defaultTransform, useNaivePaginatedTable } from '@/hooks/common/table';
import { $t } from '@/locales';
import ButtonIcon from '@/components/custom/button-icon.vue';
import SchoolRecruitMajorHistoryOperateDrawer from './modules/school-recruit-major-history-operate-drawer.vue';
import InlineExpandTextarea from '@/components/custom/inline-expand-textarea.vue';
import SchoolRecruitMajorHistorySearch from './modules/school-recruit-major-history-search.vue';
defineOptions({
name: 'SchoolRecruitMajorHistoryList'
});
const appStore = useAppStore();
const { download } = useDownload();
const { hasAuth } = useAuth();
@ -51,6 +56,47 @@ const searchParams = ref<Api.Art.SchoolRecruitMajorHistorySearchParams>({
params: {}
});
type TableRow = Api.Art.SchoolRecruitMajorHistory & { tempKey?: string };
type Model = Api.Art.SchoolRecruitMajorHistoryOperateParams;
type EditableField = Extract<keyof Model, keyof TableRow>;
const editingModel = ref<Model>(createDefaultModel());
const editingSnapshot = ref<TableRow | null>(null);
const editingMode = ref<NaiveUI.TableOperateType | null>(null);
const editingRowKey = ref<string | null>(null);
const savingRowKey = ref<string | null>(null);
const tempRow = ref<TableRow | null>(null);
const checkedRowKeys = ref<CommonType.IdType[]>([]);
const editableColumns: Array<{ key: EditableField; title: string; textarea?: boolean; minWidth?: number }> = [
{ key: 'recruitMajorId', title: '对应招录专业ID' },
{ key: 'schoolId', title: '学校ID' },
{ key: 'schoolCode', title: '学校代码' },
{ key: 'collegeCode', title: '院校代码' },
{ key: 'schoolName', title: '学校名称', minWidth: 160 },
{ key: 'majorId', title: '专业ID' },
{ key: 'majorCode', title: '专业代码' },
{ key: 'majorName', title: '专业名称', minWidth: 160 },
{ key: 'enrollCode', title: '招生代码' },
{ key: 'majorType', title: '专业类型' },
{ key: 'majorTypeSub', title: '专业类别子级' },
{ key: 'mainExamSubject', title: '主考科目' },
{ key: 'year', title: '年份' },
{ key: 'subjectType', title: '科类(文/理)' },
{ key: 'batchName', title: '批次' },
{ key: 'admissionFormula', title: '录取方式', minWidth: 160 },
{ key: 'probabilityOperator', title: '录取概率规则运算符', minWidth: 160 },
{ key: 'controlScore', title: '省控线' },
{ key: 'admissionScore', title: '录取线' },
{ key: 'planEnroll', title: '招生人数' },
{ key: 'filedAmount', title: '实际投档人数' },
{ key: 'admitAmount', title: '录取数' },
{ key: 'firstChoiceAdmitAmount', title: '一志愿录取数', minWidth: 160 },
{ key: 'minScoreDiff', title: '最低分数差' },
{ key: 'tuitionFee', title: '学费(元/年)' },
{ key: 'remark', title: '备注', textarea: true, minWidth: 160 }
];
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination, scrollX } =
useNaivePaginatedTable({
api: () => fetchGetSchoolRecruitMajorHistoryList(searchParams.value),
@ -59,7 +105,16 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
searchParams.value.pageNum = params.page;
searchParams.value.pageSize = params.pageSize;
},
columns: () => [
columns: () => createColumns()
});
const tableData = computed<TableRow[]>(() => {
const rows = data.value as TableRow[];
return tempRow.value ? [tempRow.value, ...rows] : rows;
});
function createColumns(): NaiveUI.TableColumn<TableRow>[] {
return [
{
type: 'selection',
align: 'center',
@ -76,197 +131,60 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
key: 'historyId',
title: '历年录取数据ID',
align: 'center',
minWidth: 120
minWidth: 160
},
{
key: 'recruitMajorId',
title: '对应招录专业ID',
...editableColumns.map(column => createEditableColumn(column)),
createOperateColumn()
];
}
function createEditableColumn(column: { key: EditableField; title: string; textarea?: boolean; minWidth?: number }) {
return {
key: column.key,
title: column.title,
align: 'center',
minWidth: 120
},
{
key: 'schoolId',
title: '学校ID',
align: 'center',
minWidth: 120
},
{
key: 'schoolCode',
title: '学校代码',
align: 'center',
minWidth: 120
},
{
key: 'collegeCode',
title: '院校代码',
align: 'center',
minWidth: 120
},
{
key: 'schoolName',
title: '学校名称',
align: 'center',
minWidth: 120
},
{
key: 'majorId',
title: '专业ID',
align: 'center',
minWidth: 120
},
{
key: 'majorCode',
title: '专业代码',
align: 'center',
minWidth: 120
},
{
key: 'majorName',
title: '专业名称',
align: 'center',
minWidth: 120
},
{
key: 'enrollCode',
title: '招生代码',
align: 'center',
minWidth: 120
},
{
key: 'majorType',
title: '专业类型',
align: 'center',
minWidth: 120
},
{
key: 'majorTypeSub',
title: '专业类别子级',
align: 'center',
minWidth: 120
},
{
key: 'mainExamSubject',
title: '主考科目',
align: 'center',
minWidth: 120
},
{
key: 'year',
title: '年份',
align: 'center',
minWidth: 120
},
{
key: 'subjectType',
title: '科类(文/理)',
align: 'center',
minWidth: 120
},
{
key: 'batchName',
title: '批次',
align: 'center',
minWidth: 120
},
{
key: 'admissionFormula',
title: '录取方式(文*x+专*y)',
align: 'center',
minWidth: 120
},
{
key: 'probabilityOperator',
title: '录取概率规则运算符',
align: 'center',
minWidth: 120
},
{
key: 'controlScore',
title: '省控线',
align: 'center',
minWidth: 120
},
{
key: 'admissionScore',
title: '录取线',
align: 'center',
minWidth: 120
},
{
key: 'planEnroll',
title: '招生人数',
align: 'center',
minWidth: 120
},
{
key: 'filedAmount',
title: '实际投档人数',
align: 'center',
minWidth: 120
},
{
key: 'admitAmount',
title: '录取数',
align: 'center',
minWidth: 120
},
{
key: 'firstChoiceAdmitAmount',
title: '一志愿录取数',
align: 'center',
minWidth: 120
},
{
key: 'minScoreDiff',
title: '最低分数差',
align: 'center',
minWidth: 120
},
{
key: 'tuitionFee',
title: '学费(元/年)',
align: 'center',
minWidth: 120
},
{
key: 'remark',
title: '备注',
align: 'center',
minWidth: 120
},
{
minWidth: column.minWidth ?? 140,
render: (row: TableRow) => renderEditableCell(row, column.key, column)
} satisfies NaiveUI.TableColumn<TableRow>;
}
function createOperateColumn(): NaiveUI.TableColumn<TableRow> {
return {
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => {
const divider = () => {
if (!hasAuth('art:schoolRecruitMajorHistory:edit') || !hasAuth('art:schoolRecruitMajorHistory:remove')) {
return null;
}
return <NDivider vertical />;
};
fixed: 'right',
width: 200,
render: (row: TableRow) => {
const rowKey = resolveRowKey(row);
const editing = editingRowKey.value === rowKey;
const saving = savingRowKey.value === rowKey;
const editBtn = () => {
if (!hasAuth('art:schoolRecruitMajorHistory:edit')) {
return null;
}
if (editing) {
return (
<ButtonIcon
text
type="primary"
icon="material-symbols:drive-file-rename-outline-outline"
tooltipContent={$t('common.edit')}
onClick={() => edit(row.historyId)}
/>
<NSpace size={8} justify="center">
<NButton size="tiny" quaternary disabled={saving} onClick={handleCancelEdit}>
{$t('common.cancel')}
</NButton>
<NButton size="tiny" type="primary" loading={saving} onClick={handleSaveRow}>
{$t('common.save')}
</NButton>
</NSpace>
);
};
const deleteBtn = () => {
if (!hasAuth('art:schoolRecruitMajorHistory:remove')) {
return null;
}
const showEdit = hasAuth('art:schoolRecruitMajorHistory:edit');
const showDelete = hasAuth('art:schoolRecruitMajorHistory:remove');
return (
<div class="flex-center gap-8px">
{showEdit ? (
<NButton size="tiny" text type="primary" onClick={() => handleEditRow(row)}>
{$t('common.edit')}
</NButton>
) : null}
{showEdit && showDelete ? <NDivider vertical /> : null}
{showDelete ? (
<ButtonIcon
text
type="error"
@ -275,50 +193,346 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
popconfirmContent={$t('common.confirmDelete')}
onPositiveClick={() => handleDelete(row.historyId)}
/>
);
};
return (
<div class="flex-center gap-8px">
{editBtn()}
{divider()}
{deleteBtn()}
) : null}
</div>
);
}
}
]
});
};
}
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
useTableOperate(data, 'historyId', getData);
function getRowFieldValue(row: TableRow, field: EditableField) {
return row[field as keyof TableRow];
}
function formatDisplayValue(value: unknown) {
if (value === null || value === undefined || value === '') {
return '-';
}
return typeof value === 'number' ? value : String(value);
}
function renderEditableCell(row: TableRow, field: EditableField, options?: { textarea?: boolean }) {
if (!isEditingRow(row)) {
return formatDisplayValue(getRowFieldValue(row, field));
}
const inputValue = editingModel.value[field];
const resolvedValue = inputValue === null || inputValue === undefined ? '' : String(inputValue);
if (options?.textarea) {
return renderTextareaEditor(row, field, resolvedValue);
}
return (
<NInput
size="small"
type={options?.textarea ? 'textarea' : 'text'}
autosize={options?.textarea ? { minRows: 1, maxRows: 4 } : undefined}
value={resolvedValue}
onUpdateValue={value => updateEditingField(field, value)}
/>
);
}
function renderTextareaEditor(row: TableRow, field: EditableField, value: string) {
return <InlineExpandTextarea value={value} onUpdateValue={val => updateEditingField(field, val)} />;
}
function updateEditingField(field: EditableField, value: string | number | null) {
(editingModel.value as Record<string, string | number | null>)[field] = value;
}
function resolveRowKey(row: TableRow) {
if (row.historyId !== null && row.historyId !== undefined && row.historyId !== '') {
return String(row.historyId);
}
return row.tempKey ?? '';
}
function isEditingRow(row: TableRow) {
return editingRowKey.value !== null && editingRowKey.value === resolveRowKey(row);
}
function createDefaultModel(): Model {
return {
historyId: null,
recruitMajorId: null,
schoolId: null,
schoolCode: '',
collegeCode: '',
schoolName: '',
majorId: null,
majorCode: '',
majorName: '',
enrollCode: '',
majorType: '',
majorTypeSub: '',
mainExamSubject: '',
year: null,
subjectType: '',
batchName: '',
admissionFormula: '',
probabilityOperator: '',
controlScore: null,
admissionScore: null,
planEnroll: null,
filedAmount: null,
admitAmount: null,
firstChoiceAdmitAmount: null,
minScoreDiff: null,
tuitionFee: null,
remark: ''
};
}
function createTempKey() {
return `temp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
function ensureEditingGuard(action: () => void | Promise<void>) {
const executeAction = () => Promise.resolve(action());
if (!editingRowKey.value) {
return executeAction();
}
if (!window.$dialog) {
resetEditingState();
return executeAction();
}
return new Promise<void>((resolve, reject) => {
window.$dialog?.warning({
title: '提示',
content: '当前行尚未保存,确定放弃修改吗?',
positiveText: '放弃',
negativeText: '继续编辑',
onPositiveClick: async () => {
resetEditingState();
try {
await executeAction();
resolve();
} catch (error) {
reject(error);
}
},
onNegativeClick: () => resolve()
});
});
}
function resetEditingState() {
editingModel.value = createDefaultModel();
editingSnapshot.value = null;
editingMode.value = null;
editingRowKey.value = null;
savingRowKey.value = null;
tempRow.value = null;
}
async function handleAddRow() {
await ensureEditingGuard(() => {
editingMode.value = 'add';
editingModel.value = createDefaultModel();
editingSnapshot.value = null;
const key = createTempKey();
tempRow.value = { ...(editingModel.value as TableRow), tempKey: key };
editingRowKey.value = key;
checkedRowKeys.value = [];
});
}
async function handleEditRow(row: TableRow) {
await ensureEditingGuard(() => {
editingMode.value = 'edit';
tempRow.value = null;
editingRowKey.value = resolveRowKey(row);
editingSnapshot.value = jsonClone(row);
editingModel.value = Object.assign(createDefaultModel(), jsonClone(row));
});
}
function handleCancelEdit() {
resetEditingState();
}
const requiredFields: Array<{ key: EditableField; label: string }> = [
{ key: 'recruitMajorId', label: '对应招录专业ID' },
{ key: 'schoolId', label: '学校ID' },
{ key: 'schoolCode', label: '学校代码' },
{ key: 'schoolName', label: '学校名称' },
{ key: 'majorCode', label: '专业代码' },
{ key: 'majorName', label: '专业名称' },
{ key: 'year', label: '年份' }
];
function validateModel() {
for (const item of requiredFields) {
const value = editingModel.value[item.key];
if (value === null || value === undefined || value === '') {
window.$message?.warning(`${item.label}不能为空`);
return false;
}
}
return true;
}
async function handleSaveRow() {
if (!editingMode.value) return;
if (!validateModel()) return;
const {
historyId,
recruitMajorId,
schoolId,
schoolCode,
collegeCode,
schoolName,
majorId,
majorCode,
majorName,
enrollCode,
majorType,
majorTypeSub,
mainExamSubject,
year,
subjectType,
batchName,
admissionFormula,
probabilityOperator,
controlScore,
admissionScore,
planEnroll,
filedAmount,
admitAmount,
firstChoiceAdmitAmount,
minScoreDiff,
tuitionFee,
remark
} = editingModel.value;
savingRowKey.value = editingRowKey.value;
const requestResult =
editingMode.value === 'add'
? await fetchCreateSchoolRecruitMajorHistory({
recruitMajorId,
schoolId,
schoolCode,
collegeCode,
schoolName,
majorId,
majorCode,
majorName,
enrollCode,
majorType,
majorTypeSub,
mainExamSubject,
year,
subjectType,
batchName,
admissionFormula,
probabilityOperator,
controlScore,
admissionScore,
planEnroll,
filedAmount,
admitAmount,
firstChoiceAdmitAmount,
minScoreDiff,
tuitionFee,
remark
})
: await fetchUpdateSchoolRecruitMajorHistory({
historyId,
recruitMajorId,
schoolId,
schoolCode,
collegeCode,
schoolName,
majorId,
majorCode,
majorName,
enrollCode,
majorType,
majorTypeSub,
mainExamSubject,
year,
subjectType,
batchName,
admissionFormula,
probabilityOperator,
controlScore,
admissionScore,
planEnroll,
filedAmount,
admitAmount,
firstChoiceAdmitAmount,
minScoreDiff,
tuitionFee,
remark
});
savingRowKey.value = null;
if (requestResult.error) {
window.$message?.error(requestResult.error.message || '保存失败');
if (editingMode.value === 'edit' && editingSnapshot.value) {
editingModel.value = Object.assign(createDefaultModel(), jsonClone(editingSnapshot.value));
}
return;
}
window.$message?.success($t('common.updateSuccess'));
resetEditingState();
await getData();
}
async function handleBatchDelete() {
// request
await ensureEditingGuard(async () => {
const { error } = await fetchBatchDeleteSchoolRecruitMajorHistory(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
window.$message?.success($t('common.deleteSuccess'));
checkedRowKeys.value = [];
await getData();
});
}
async function handleDelete(historyId: CommonType.IdType) {
// request
await ensureEditingGuard(async () => {
const { error } = await fetchBatchDeleteSchoolRecruitMajorHistory([historyId]);
if (error) return;
onDeleted();
}
function edit(historyId: CommonType.IdType) {
handleEdit(historyId);
window.$message?.success($t('common.deleteSuccess'));
await getData();
});
}
function handleExport() {
download('/art/schoolRecruitMajorHistory/export', searchParams.value, `院校招录专业历年录取数据_${new Date().getTime()}.xlsx`);
download(
'/art/schoolRecruitMajorHistory/export',
searchParams.value,
`院校招录专业历年录取数据_${new Date().getTime()}.xlsx`
);
}
async function handleSearch() {
await ensureEditingGuard(async () => {
await getDataByPage();
});
}
async function handleRefresh() {
await ensureEditingGuard(getData);
}
function rowClassName(row: TableRow) {
return isEditingRow(row) ? 'inline-edit-row' : '';
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<SchoolRecruitMajorHistorySearch v-model:model="searchParams" @search="getDataByPage" />
<SchoolRecruitMajorHistorySearch v-model:model="searchParams" @search="handleSearch" />
<NCard title="院校招录专业历年录取数据列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
@ -328,33 +542,32 @@ function handleExport() {
:show-add="hasAuth('art:schoolRecruitMajorHistory:add')"
:show-delete="hasAuth('art:schoolRecruitMajorHistory:remove')"
:show-export="hasAuth('art:schoolRecruitMajorHistory:export')"
@add="handleAdd"
@add="handleAddRow"
@delete="handleBatchDelete"
@export="handleExport"
@refresh="getData"
@refresh="handleRefresh"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
:data="tableData"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="scrollX"
:loading="loading"
remote
:row-key="row => row.historyId"
:row-key="resolveRowKey"
:row-class-name="rowClassName"
:pagination="mobilePagination"
class="sm:h-full"
/>
<SchoolRecruitMajorHistoryOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getDataByPage"
/>
</NCard>
</div>
</template>
<style scoped></style>
<style scoped>
:deep(.inline-edit-row .n-data-table-td) {
background-color: rgba(24, 160, 88, 0.08);
}
</style>

View File

@ -1,7 +1,10 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { jsonClone } from '@sa/utils';
import { fetchCreateSchoolRecruitMajorHistory, fetchUpdateSchoolRecruitMajorHistory } from '@/service/api/art/school-recruit-major-history';
import {
fetchCreateSchoolRecruitMajorHistory,
fetchUpdateSchoolRecruitMajorHistory
} from '@/service/api/art/school-recruit-major-history';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
@ -71,7 +74,7 @@ function createDefaultModel(): Model {
firstChoiceAdmitAmount: null,
minScoreDiff: null,
tuitionFee: null,
remark: '',
remark: ''
};
}
@ -99,7 +102,7 @@ const rules: Record<RuleKey, App.Global.FormRule> = {
schoolName: createRequiredRule('学校名称不能为空'),
majorCode: createRequiredRule('专业代码不能为空'),
majorName: createRequiredRule('专业名称不能为空'),
year: createRequiredRule('年份不能为空'),
year: createRequiredRule('年份不能为空')
};
function handleUpdateModelWhenEdit() {
@ -117,16 +120,99 @@ function closeDrawer() {
async function handleSubmit() {
await validate();
const { historyId, recruitMajorId, schoolId, schoolCode, collegeCode, schoolName, majorId, majorCode, majorName, enrollCode, majorType, majorTypeSub, mainExamSubject, year, subjectType, batchName, admissionFormula, probabilityOperator, controlScore, admissionScore, planEnroll, filedAmount, admitAmount, firstChoiceAdmitAmount, minScoreDiff, tuitionFee, remark } = model.value;
const {
historyId,
recruitMajorId,
schoolId,
schoolCode,
collegeCode,
schoolName,
majorId,
majorCode,
majorName,
enrollCode,
majorType,
majorTypeSub,
mainExamSubject,
year,
subjectType,
batchName,
admissionFormula,
probabilityOperator,
controlScore,
admissionScore,
planEnroll,
filedAmount,
admitAmount,
firstChoiceAdmitAmount,
minScoreDiff,
tuitionFee,
remark
} = model.value;
// request
if (props.operateType === 'add') {
const { error } = await fetchCreateSchoolRecruitMajorHistory({ recruitMajorId, schoolId, schoolCode, collegeCode, schoolName, majorId, majorCode, majorName, enrollCode, majorType, majorTypeSub, mainExamSubject, year, subjectType, batchName, admissionFormula, probabilityOperator, controlScore, admissionScore, planEnroll, filedAmount, admitAmount, firstChoiceAdmitAmount, minScoreDiff, tuitionFee, remark });
const { error } = await fetchCreateSchoolRecruitMajorHistory({
recruitMajorId,
schoolId,
schoolCode,
collegeCode,
schoolName,
majorId,
majorCode,
majorName,
enrollCode,
majorType,
majorTypeSub,
mainExamSubject,
year,
subjectType,
batchName,
admissionFormula,
probabilityOperator,
controlScore,
admissionScore,
planEnroll,
filedAmount,
admitAmount,
firstChoiceAdmitAmount,
minScoreDiff,
tuitionFee,
remark
});
if (error) return;
}
if (props.operateType === 'edit') {
const { error } = await fetchUpdateSchoolRecruitMajorHistory({ historyId, recruitMajorId, schoolId, schoolCode, collegeCode, schoolName, majorId, majorCode, majorName, enrollCode, majorType, majorTypeSub, mainExamSubject, year, subjectType, batchName, admissionFormula, probabilityOperator, controlScore, admissionScore, planEnroll, filedAmount, admitAmount, firstChoiceAdmitAmount, minScoreDiff, tuitionFee, remark });
const { error } = await fetchUpdateSchoolRecruitMajorHistory({
historyId,
recruitMajorId,
schoolId,
schoolCode,
collegeCode,
schoolName,
majorId,
majorCode,
majorName,
enrollCode,
majorType,
majorTypeSub,
mainExamSubject,
year,
subjectType,
batchName,
admissionFormula,
probabilityOperator,
controlScore,
admissionScore,
planEnroll,
filedAmount,
admitAmount,
firstChoiceAdmitAmount,
minScoreDiff,
tuitionFee,
remark
});
if (error) return;
}
@ -234,12 +320,7 @@ watch(visible, () => {
<NInput v-model:value="model.tuitionFee" placeholder="请输入学费(元/年)" />
</NFormItem>
<NFormItem label="备注" path="remark">
<NInput
v-model:value="model.remark"
:rows="3"
type="textarea"
placeholder="请输入备注"
/>
<NInput v-model:value="model.remark" :rows="3" type="textarea" placeholder="请输入备注" />
</NFormItem>
</NForm>
<template #footer>

View File

@ -42,7 +42,13 @@ async function search() {
<NCollapseItem :title="$t('common.search')" name="art-school-recruit-major-history-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" label="对应招录专业ID" label-width="auto" path="recruitMajorId" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="对应招录专业ID"
label-width="auto"
path="recruitMajorId"
class="pr-24px"
>
<NInput v-model:value="model.recruitMajorId" placeholder="请输入对应招录专业ID" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="学校ID" label-width="auto" path="schoolId" class="pr-24px">
@ -70,12 +76,7 @@ async function search() {
<NInput v-model:value="model.enrollCode" placeholder="请输入招生代码" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="专业类型" label-width="auto" path="majorType" class="pr-24px">
<NSelect
v-model:value="model.majorType"
placeholder="请选择专业类型"
:options="[]"
clearable
/>
<NSelect v-model:value="model.majorType" placeholder="请选择专业类型" :options="[]" clearable />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="专业类别子级" label-width="auto" path="majorTypeSub" class="pr-24px">
<NInput v-model:value="model.majorTypeSub" placeholder="请输入专业类别子级" />
@ -87,20 +88,27 @@ async function search() {
<NInput v-model:value="model.year" placeholder="请输入年份" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="科类(文/理)" label-width="auto" path="subjectType" class="pr-24px">
<NSelect
v-model:value="model.subjectType"
placeholder="请选择科类(文/理)"
:options="[]"
clearable
/>
<NSelect v-model:value="model.subjectType" placeholder="请选择科类(文/理)" :options="[]" clearable />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="批次" label-width="auto" path="batchName" class="pr-24px">
<NInput v-model:value="model.batchName" placeholder="请输入批次" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="录取方式(文*x+专*y)" label-width="auto" path="admissionFormula" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="录取方式(文*x+专*y)"
label-width="auto"
path="admissionFormula"
class="pr-24px"
>
<NInput v-model:value="model.admissionFormula" placeholder="请输入录取方式(文*x+专*y)" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="录取概率规则运算符" label-width="auto" path="probabilityOperator" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="录取概率规则运算符"
label-width="auto"
path="probabilityOperator"
class="pr-24px"
>
<NInput v-model:value="model.probabilityOperator" placeholder="请输入录取概率规则运算符" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="省控线" label-width="auto" path="controlScore" class="pr-24px">
@ -118,7 +126,13 @@ async function search() {
<NFormItemGi span="24 s:12 m:6" label="录取数" label-width="auto" path="admitAmount" class="pr-24px">
<NInput v-model:value="model.admitAmount" placeholder="请输入录取数" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="一志愿录取数" label-width="auto" path="firstChoiceAdmitAmount" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="一志愿录取数"
label-width="auto"
path="firstChoiceAdmitAmount"
class="pr-24px"
>
<NInput v-model:value="model.firstChoiceAdmitAmount" placeholder="请输入一志愿录取数" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="最低分数差" label-width="auto" path="minScoreDiff" class="pr-24px">

View File

@ -1,21 +1,26 @@
<script setup lang="tsx">
import { ref } from 'vue';
import { NDivider } from 'naive-ui';
import { fetchBatchDeleteSchoolRecruitMajor, fetchGetSchoolRecruitMajorList } from '@/service/api/art/school-recruit-major';
import { computed, ref } from 'vue';
import { NButton, NDivider, NInput, NSpace } from 'naive-ui';
import { jsonClone } from '@sa/utils';
import {
fetchBatchDeleteSchoolRecruitMajor,
fetchCreateSchoolRecruitMajor,
fetchGetSchoolRecruitMajorList,
fetchUpdateSchoolRecruitMajor
} from '@/service/api/art/school-recruit-major';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
import { useDownload } from '@/hooks/business/download';
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
import { defaultTransform, useNaivePaginatedTable } from '@/hooks/common/table';
import { $t } from '@/locales';
import ButtonIcon from '@/components/custom/button-icon.vue';
import SchoolRecruitMajorOperateDrawer from './modules/school-recruit-major-operate-drawer.vue';
import InlineExpandTextarea from '@/components/custom/inline-expand-textarea.vue';
import SchoolRecruitMajorSearch from './modules/school-recruit-major-search.vue';
defineOptions({
name: 'SchoolRecruitMajorList'
});
const appStore = useAppStore();
const { download } = useDownload();
const { hasAuth } = useAuth();
@ -53,6 +58,48 @@ const searchParams = ref<Api.Art.SchoolRecruitMajorSearchParams>({
params: {}
});
type TableRow = Api.Art.SchoolRecruitMajor & { tempKey?: string };
type Model = Api.Art.SchoolRecruitMajorOperateParams;
type EditableField = Extract<keyof Model, keyof TableRow>;
const editingModel = ref<Model>(createDefaultModel());
const editingSnapshot = ref<TableRow | null>(null);
const editingMode = ref<NaiveUI.TableOperateType | null>(null);
const editingRowKey = ref<string | null>(null);
const savingRowKey = ref<string | null>(null);
const tempRow = ref<TableRow | null>(null);
const checkedRowKeys = ref<CommonType.IdType[]>([]);
const editableColumns: Array<{ key: EditableField; title: string; textarea?: boolean; minWidth?: number }> = [
{ key: 'schoolCode', title: '学校代码' },
{ key: 'schoolName', title: '学校名称(冗余)', minWidth: 160 },
{ key: 'year', title: '年份' },
{ key: 'majorId', title: '专业ID' },
{ key: 'majorCode', title: '专业代码' },
{ key: 'majorName', title: '专业名称', minWidth: 160 },
{ key: 'enrollCode', title: '招生代码' },
{ key: 'dataStatus', title: '数据状态' },
{ key: 'batchName', title: '批次' },
{ key: 'majorType', title: '专业类型' },
{ key: 'majorTypeSub', title: '二级专业类型' },
{ key: 'subjectType', title: '科类(文/理)' },
{ key: 'admissionWayShort', title: '录取方式缩写' },
{ key: 'admissionWayExternal', title: '对外录取方式' },
{ key: 'admissionWayExternalOp', title: '对外录取方式运算符' },
{ key: 'admissionWayInternal', title: '内部录取方式' },
{ key: 'admissionWayInternalOp', title: '内部录取方式运算符' },
{ key: 'planEnroll', title: '计划招生人数' },
{ key: 'mainExamSubject', title: '主考科目' },
{ key: 'schoolingYears', title: '学制(年)' },
{ key: 'enrollLimitDesc', title: '院校限制说明', textarea: true, minWidth: 180 },
{ key: 'tuitionFee', title: '学费(元/年)' },
{ key: 'cultureScoreLimit', title: '文化分数限制' },
{ key: 'majorScoreLimit', title: '专业分数限制' },
{ key: 'chineseScoreLimit', title: '语文成绩限制' },
{ key: 'englishScoreLimit', title: '英语成绩限制' },
{ key: 'remark', title: '备注', textarea: true, minWidth: 160 }
];
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination, scrollX } =
useNaivePaginatedTable({
api: () => fetchGetSchoolRecruitMajorList(searchParams.value),
@ -61,7 +108,16 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
searchParams.value.pageNum = params.page;
searchParams.value.pageSize = params.pageSize;
},
columns: () => [
columns: () => createColumns()
});
const tableData = computed<TableRow[]>(() => {
const rows = data.value as TableRow[];
return tempRow.value ? [tempRow.value, ...rows] : rows;
});
function createColumns(): NaiveUI.TableColumn<TableRow>[] {
return [
{
type: 'selection',
align: 'center',
@ -78,209 +134,60 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
key: 'recruitMajorId',
title: '院校招录专业ID',
align: 'center',
minWidth: 120
minWidth: 150
},
{
key: 'schoolId',
title: '学校ID',
...editableColumns.map(column => createEditableColumn(column)),
createOperateColumn()
];
}
function createEditableColumn(column: { key: EditableField; title: string; textarea?: boolean; minWidth?: number }) {
return {
key: column.key,
title: column.title,
align: 'center',
minWidth: 120
},
{
key: 'schoolCode',
title: '学校代码',
align: 'center',
minWidth: 120
},
{
key: 'schoolName',
title: '学校名称(冗余)',
align: 'center',
minWidth: 120
},
{
key: 'year',
title: '年份',
align: 'center',
minWidth: 120
},
{
key: 'majorId',
title: '专业ID',
align: 'center',
minWidth: 120
},
{
key: 'majorCode',
title: '专业代码',
align: 'center',
minWidth: 120
},
{
key: 'majorName',
title: '专业名称',
align: 'center',
minWidth: 120
},
{
key: 'enrollCode',
title: '招生代码(为空则存空串)',
align: 'center',
minWidth: 120
},
{
key: 'dataStatus',
title: '数据状态(停招/新招/新增)',
align: 'center',
minWidth: 120
},
{
key: 'batchName',
title: '批次',
align: 'center',
minWidth: 120
},
{
key: 'majorType',
title: '专业类型',
align: 'center',
minWidth: 120
},
{
key: 'majorTypeSub',
title: '二级专业类型',
align: 'center',
minWidth: 120
},
{
key: 'subjectType',
title: '科类(文/理)',
align: 'center',
minWidth: 120
},
{
key: 'admissionWayShort',
title: '录取方式缩写',
align: 'center',
minWidth: 120
},
{
key: 'admissionWayExternal',
title: '对外录取方式',
align: 'center',
minWidth: 120
},
{
key: 'admissionWayExternalOp',
title: '对外录取方式运算符',
align: 'center',
minWidth: 120
},
{
key: 'admissionWayInternal',
title: '内部录取方式',
align: 'center',
minWidth: 120
},
{
key: 'admissionWayInternalOp',
title: '内部录取方式运算符',
align: 'center',
minWidth: 120
},
{
key: 'planEnroll',
title: '计划招生人数',
align: 'center',
minWidth: 120
},
{
key: 'mainExamSubject',
title: '主考科目',
align: 'center',
minWidth: 120
},
{
key: 'schoolingYears',
title: '学制(年)',
align: 'center',
minWidth: 120
},
{
key: 'enrollLimitDesc',
title: '院校限制说明',
align: 'center',
minWidth: 120
},
{
key: 'tuitionFee',
title: '学费(元/年)',
align: 'center',
minWidth: 120
},
{
key: 'cultureScoreLimit',
title: '文化分数限制',
align: 'center',
minWidth: 120
},
{
key: 'majorScoreLimit',
title: '专业分数限制',
align: 'center',
minWidth: 120
},
{
key: 'chineseScoreLimit',
title: '语文成绩限制',
align: 'center',
minWidth: 120
},
{
key: 'englishScoreLimit',
title: '英语成绩限制',
align: 'center',
minWidth: 120
},
{
key: 'remark',
title: '备注',
align: 'center',
minWidth: 120
},
{
minWidth: column.minWidth ?? 140,
render: (row: TableRow) => renderEditableCell(row, column.key, column)
} satisfies NaiveUI.TableColumn<TableRow>;
}
function createOperateColumn(): NaiveUI.TableColumn<TableRow> {
return {
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => {
const divider = () => {
if (!hasAuth('art:schoolRecruitMajor:edit') || !hasAuth('art:schoolRecruitMajor:remove')) {
return null;
}
return <NDivider vertical />;
};
fixed: 'right',
width: 200,
render: (row: TableRow) => {
const rowKey = resolveRowKey(row);
const editing = editingRowKey.value === rowKey;
const saving = savingRowKey.value === rowKey;
const editBtn = () => {
if (!hasAuth('art:schoolRecruitMajor:edit')) {
return null;
}
if (editing) {
return (
<ButtonIcon
text
type="primary"
icon="material-symbols:drive-file-rename-outline-outline"
tooltipContent={$t('common.edit')}
onClick={() => edit(row.recruitMajorId)}
/>
<NSpace size={8} justify="center">
<NButton size="tiny" quaternary disabled={saving} onClick={handleCancelEdit}>
{$t('common.cancel')}
</NButton>
<NButton size="tiny" type="primary" loading={saving} onClick={handleSaveRow}>
{$t('common.save')}
</NButton>
</NSpace>
);
};
const deleteBtn = () => {
if (!hasAuth('art:schoolRecruitMajor:remove')) {
return null;
}
const showEdit = hasAuth('art:schoolRecruitMajor:edit');
const showDelete = hasAuth('art:schoolRecruitMajor:remove');
return (
<div class="flex-center gap-8px">
{showEdit ? (
<NButton size="tiny" text type="primary" onClick={() => handleEditRow(row)}>
{$t('common.edit')}
</NButton>
) : null}
{showEdit && showDelete ? <NDivider vertical /> : null}
{showDelete ? (
<ButtonIcon
text
type="error"
@ -289,50 +196,350 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
popconfirmContent={$t('common.confirmDelete')}
onPositiveClick={() => handleDelete(row.recruitMajorId)}
/>
);
};
return (
<div class="flex-center gap-8px">
{editBtn()}
{divider()}
{deleteBtn()}
) : null}
</div>
);
}
}
]
});
};
}
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
useTableOperate(data, 'recruitMajorId', getData);
function getRowFieldValue(row: TableRow, field: EditableField) {
return row[field as keyof TableRow];
}
function formatDisplayValue(value: unknown) {
if (value === null || value === undefined || value === '') {
return '-';
}
return typeof value === 'number' ? value : String(value);
}
function renderEditableCell(row: TableRow, field: EditableField, options?: { textarea?: boolean }) {
if (!isEditingRow(row)) {
return formatDisplayValue(getRowFieldValue(row, field));
}
const inputValue = editingModel.value[field];
const resolvedValue = inputValue === null || inputValue === undefined ? '' : String(inputValue);
if (options?.textarea) {
return renderTextareaEditor(row, field, resolvedValue);
}
return (
<NInput
size="small"
type={options?.textarea ? 'textarea' : 'text'}
autosize={options?.textarea ? { minRows: 1, maxRows: 4 } : undefined}
value={resolvedValue}
onUpdateValue={value => updateEditingField(field, value)}
/>
);
}
function renderTextareaEditor(row: TableRow, field: EditableField, value: string) {
return <InlineExpandTextarea value={value} onUpdateValue={val => updateEditingField(field, val)} />;
}
function updateEditingField(field: EditableField, value: string | number | null) {
(editingModel.value as Record<string, string | number | null>)[field] = value;
}
function resolveRowKey(row: TableRow) {
if (row.recruitMajorId !== null && row.recruitMajorId !== undefined && row.recruitMajorId !== '') {
return String(row.recruitMajorId);
}
return row.tempKey ?? '';
}
function isEditingRow(row: TableRow) {
return editingRowKey.value !== null && editingRowKey.value === resolveRowKey(row);
}
function createDefaultModel(): Model {
return {
recruitMajorId: null,
schoolId: null,
schoolCode: '',
schoolName: '',
year: null,
majorId: null,
majorCode: '',
majorName: '',
enrollCode: '',
dataStatus: '',
batchName: '',
majorType: '',
majorTypeSub: '',
subjectType: '',
admissionWayShort: '',
admissionWayExternal: '',
admissionWayExternalOp: '',
admissionWayInternal: '',
admissionWayInternalOp: '',
planEnroll: null,
mainExamSubject: '',
schoolingYears: null,
enrollLimitDesc: '',
tuitionFee: null,
cultureScoreLimit: null,
majorScoreLimit: null,
chineseScoreLimit: null,
englishScoreLimit: null,
remark: ''
};
}
function createTempKey() {
return `temp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
function ensureEditingGuard(action: () => void | Promise<void>) {
const executeAction = () => Promise.resolve(action());
if (!editingRowKey.value) {
return executeAction();
}
if (!window.$dialog) {
resetEditingState();
return executeAction();
}
return new Promise<void>((resolve, reject) => {
window.$dialog?.warning({
title: '提示',
content: '当前行尚未保存,确定放弃修改吗?',
positiveText: '放弃',
negativeText: '继续编辑',
onPositiveClick: async () => {
resetEditingState();
try {
await executeAction();
resolve();
} catch (error) {
reject(error);
}
},
onNegativeClick: () => resolve()
});
});
}
function resetEditingState() {
editingModel.value = createDefaultModel();
editingSnapshot.value = null;
editingMode.value = null;
editingRowKey.value = null;
savingRowKey.value = null;
tempRow.value = null;
}
async function handleAddRow() {
await ensureEditingGuard(() => {
editingMode.value = 'add';
editingModel.value = createDefaultModel();
editingSnapshot.value = null;
const key = createTempKey();
tempRow.value = { ...(editingModel.value as TableRow), tempKey: key };
editingRowKey.value = key;
checkedRowKeys.value = [];
});
}
async function handleEditRow(row: TableRow) {
await ensureEditingGuard(() => {
editingMode.value = 'edit';
tempRow.value = null;
editingRowKey.value = resolveRowKey(row);
editingSnapshot.value = jsonClone(row);
editingModel.value = Object.assign(createDefaultModel(), jsonClone(row));
});
}
function handleCancelEdit() {
resetEditingState();
}
const requiredFields: Array<{ key: EditableField; label: string }> = [
{ key: 'schoolCode', label: '学校代码' },
{ key: 'schoolName', label: '学校名称' },
{ key: 'year', label: '年份' },
{ key: 'majorCode', label: '专业代码' },
{ key: 'majorName', label: '专业名称' },
{ key: 'enrollCode', label: '招生代码' },
{ key: 'dataStatus', label: '数据状态' }
];
function validateModel() {
for (const item of requiredFields) {
const value = editingModel.value[item.key];
if (value === null || value === undefined || value === '') {
window.$message?.warning(`${item.label}不能为空`);
return false;
}
}
return true;
}
async function handleSaveRow() {
if (!editingMode.value) return;
if (!validateModel()) return;
const {
recruitMajorId,
schoolId,
schoolCode,
schoolName,
year,
majorId,
majorCode,
majorName,
enrollCode,
dataStatus,
batchName,
majorType,
majorTypeSub,
subjectType,
admissionWayShort,
admissionWayExternal,
admissionWayExternalOp,
admissionWayInternal,
admissionWayInternalOp,
planEnroll,
mainExamSubject,
schoolingYears,
enrollLimitDesc,
tuitionFee,
cultureScoreLimit,
majorScoreLimit,
chineseScoreLimit,
englishScoreLimit,
remark
} = editingModel.value;
savingRowKey.value = editingRowKey.value;
const requestResult =
editingMode.value === 'add'
? await fetchCreateSchoolRecruitMajor({
schoolId,
schoolCode,
schoolName,
year,
majorId,
majorCode,
majorName,
enrollCode,
dataStatus,
batchName,
majorType,
majorTypeSub,
subjectType,
admissionWayShort,
admissionWayExternal,
admissionWayExternalOp,
admissionWayInternal,
admissionWayInternalOp,
planEnroll,
mainExamSubject,
schoolingYears,
enrollLimitDesc,
tuitionFee,
cultureScoreLimit,
majorScoreLimit,
chineseScoreLimit,
englishScoreLimit,
remark
})
: await fetchUpdateSchoolRecruitMajor({
recruitMajorId,
schoolId,
schoolCode,
schoolName,
year,
majorId,
majorCode,
majorName,
enrollCode,
dataStatus,
batchName,
majorType,
majorTypeSub,
subjectType,
admissionWayShort,
admissionWayExternal,
admissionWayExternalOp,
admissionWayInternal,
admissionWayInternalOp,
planEnroll,
mainExamSubject,
schoolingYears,
enrollLimitDesc,
tuitionFee,
cultureScoreLimit,
majorScoreLimit,
chineseScoreLimit,
englishScoreLimit,
remark
});
savingRowKey.value = null;
if (requestResult.error) {
window.$message?.error(requestResult.error.message || '保存失败');
if (editingMode.value === 'edit' && editingSnapshot.value) {
editingModel.value = Object.assign(createDefaultModel(), jsonClone(editingSnapshot.value));
}
return;
}
window.$message?.success($t('common.updateSuccess'));
resetEditingState();
await getData();
}
async function handleBatchDelete() {
// request
await ensureEditingGuard(async () => {
const { error } = await fetchBatchDeleteSchoolRecruitMajor(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
window.$message?.success($t('common.deleteSuccess'));
checkedRowKeys.value = [];
await getData();
});
}
async function handleDelete(recruitMajorId: CommonType.IdType) {
// request
await ensureEditingGuard(async () => {
const { error } = await fetchBatchDeleteSchoolRecruitMajor([recruitMajorId]);
if (error) return;
onDeleted();
}
function edit(recruitMajorId: CommonType.IdType) {
handleEdit(recruitMajorId);
window.$message?.success($t('common.deleteSuccess'));
await getData();
});
}
function handleExport() {
download('/art/schoolRecruitMajor/export', searchParams.value, `院校招录专业_${new Date().getTime()}.xlsx`);
}
async function handleSearch() {
await ensureEditingGuard(async () => {
await getDataByPage();
});
}
async function handleRefresh() {
await ensureEditingGuard(getData);
}
function rowClassName(row: TableRow) {
return isEditingRow(row) ? 'inline-edit-row' : '';
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<SchoolRecruitMajorSearch v-model:model="searchParams" @search="getDataByPage" />
<SchoolRecruitMajorSearch v-model:model="searchParams" @search="handleSearch" />
<NCard title="院校招录专业列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
@ -342,33 +549,32 @@ function handleExport() {
:show-add="hasAuth('art:schoolRecruitMajor:add')"
:show-delete="hasAuth('art:schoolRecruitMajor:remove')"
:show-export="hasAuth('art:schoolRecruitMajor:export')"
@add="handleAdd"
@add="handleAddRow"
@delete="handleBatchDelete"
@export="handleExport"
@refresh="getData"
@refresh="handleRefresh"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
:data="tableData"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="scrollX"
:loading="loading"
remote
:row-key="row => row.recruitMajorId"
:row-key="resolveRowKey"
:row-class-name="rowClassName"
:pagination="mobilePagination"
class="sm:h-full"
/>
<SchoolRecruitMajorOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getDataByPage"
/>
</NCard>
</div>
</template>
<style scoped></style>
<style scoped>
:deep(.inline-edit-row .n-data-table-td) {
background-color: rgba(24, 160, 88, 0.08);
}
</style>

View File

@ -73,7 +73,7 @@ function createDefaultModel(): Model {
majorScoreLimit: null,
chineseScoreLimit: null,
englishScoreLimit: null,
remark: '',
remark: ''
};
}
@ -103,7 +103,7 @@ const rules: Record<RuleKey, App.Global.FormRule> = {
majorCode: createRequiredRule('专业代码不能为空'),
majorName: createRequiredRule('专业名称不能为空'),
enrollCode: createRequiredRule('招生代码(为空则存空串)不能为空'),
dataStatus: createRequiredRule('数据状态(停招/新招/新增)不能为空'),
dataStatus: createRequiredRule('数据状态(停招/新招/新增)不能为空')
};
function handleUpdateModelWhenEdit() {
@ -121,16 +121,105 @@ function closeDrawer() {
async function handleSubmit() {
await validate();
const { recruitMajorId, schoolId, schoolCode, schoolName, year, majorId, majorCode, majorName, enrollCode, dataStatus, batchName, majorType, majorTypeSub, subjectType, admissionWayShort, admissionWayExternal, admissionWayExternalOp, admissionWayInternal, admissionWayInternalOp, planEnroll, mainExamSubject, schoolingYears, enrollLimitDesc, tuitionFee, cultureScoreLimit, majorScoreLimit, chineseScoreLimit, englishScoreLimit, remark } = model.value;
const {
recruitMajorId,
schoolId,
schoolCode,
schoolName,
year,
majorId,
majorCode,
majorName,
enrollCode,
dataStatus,
batchName,
majorType,
majorTypeSub,
subjectType,
admissionWayShort,
admissionWayExternal,
admissionWayExternalOp,
admissionWayInternal,
admissionWayInternalOp,
planEnroll,
mainExamSubject,
schoolingYears,
enrollLimitDesc,
tuitionFee,
cultureScoreLimit,
majorScoreLimit,
chineseScoreLimit,
englishScoreLimit,
remark
} = model.value;
// request
if (props.operateType === 'add') {
const { error } = await fetchCreateSchoolRecruitMajor({ schoolId, schoolCode, schoolName, year, majorId, majorCode, majorName, enrollCode, dataStatus, batchName, majorType, majorTypeSub, subjectType, admissionWayShort, admissionWayExternal, admissionWayExternalOp, admissionWayInternal, admissionWayInternalOp, planEnroll, mainExamSubject, schoolingYears, enrollLimitDesc, tuitionFee, cultureScoreLimit, majorScoreLimit, chineseScoreLimit, englishScoreLimit, remark });
const { error } = await fetchCreateSchoolRecruitMajor({
schoolId,
schoolCode,
schoolName,
year,
majorId,
majorCode,
majorName,
enrollCode,
dataStatus,
batchName,
majorType,
majorTypeSub,
subjectType,
admissionWayShort,
admissionWayExternal,
admissionWayExternalOp,
admissionWayInternal,
admissionWayInternalOp,
planEnroll,
mainExamSubject,
schoolingYears,
enrollLimitDesc,
tuitionFee,
cultureScoreLimit,
majorScoreLimit,
chineseScoreLimit,
englishScoreLimit,
remark
});
if (error) return;
}
if (props.operateType === 'edit') {
const { error } = await fetchUpdateSchoolRecruitMajor({ recruitMajorId, schoolId, schoolCode, schoolName, year, majorId, majorCode, majorName, enrollCode, dataStatus, batchName, majorType, majorTypeSub, subjectType, admissionWayShort, admissionWayExternal, admissionWayExternalOp, admissionWayInternal, admissionWayInternalOp, planEnroll, mainExamSubject, schoolingYears, enrollLimitDesc, tuitionFee, cultureScoreLimit, majorScoreLimit, chineseScoreLimit, englishScoreLimit, remark });
const { error } = await fetchUpdateSchoolRecruitMajor({
recruitMajorId,
schoolId,
schoolCode,
schoolName,
year,
majorId,
majorCode,
majorName,
enrollCode,
dataStatus,
batchName,
majorType,
majorTypeSub,
subjectType,
admissionWayShort,
admissionWayExternal,
admissionWayExternalOp,
admissionWayInternal,
admissionWayInternalOp,
planEnroll,
mainExamSubject,
schoolingYears,
enrollLimitDesc,
tuitionFee,
cultureScoreLimit,
majorScoreLimit,
chineseScoreLimit,
englishScoreLimit,
remark
});
if (error) return;
}
@ -230,12 +319,7 @@ watch(visible, () => {
<NInput v-model:value="model.schoolingYears" placeholder="请输入学制(年)" />
</NFormItem>
<NFormItem label="院校限制说明" path="enrollLimitDesc">
<NInput
v-model:value="model.enrollLimitDesc"
:rows="3"
type="textarea"
placeholder="请输入院校限制说明"
/>
<NInput v-model:value="model.enrollLimitDesc" :rows="3" type="textarea" placeholder="请输入院校限制说明" />
</NFormItem>
<NFormItem label="学费(元/年)" path="tuitionFee">
<NInput v-model:value="model.tuitionFee" placeholder="请输入学费(元/年)" />
@ -253,12 +337,7 @@ watch(visible, () => {
<NInput v-model:value="model.englishScoreLimit" placeholder="请输入英语成绩限制" />
</NFormItem>
<NFormItem label="备注" path="remark">
<NInput
v-model:value="model.remark"
:rows="3"
type="textarea"
placeholder="请输入备注"
/>
<NInput v-model:value="model.remark" :rows="3" type="textarea" placeholder="请输入备注" />
</NFormItem>
</NForm>
<template #footer>

View File

@ -63,10 +63,22 @@ async function search() {
<NFormItemGi span="24 s:12 m:6" label="专业名称" label-width="auto" path="majorName" class="pr-24px">
<NInput v-model:value="model.majorName" placeholder="请输入专业名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="招生代码(为空则存空串)" label-width="auto" path="enrollCode" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="招生代码(为空则存空串)"
label-width="auto"
path="enrollCode"
class="pr-24px"
>
<NInput v-model:value="model.enrollCode" placeholder="请输入招生代码(为空则存空串)" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="数据状态(停招/新招/新增)" label-width="auto" path="dataStatus" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="数据状态(停招/新招/新增)"
label-width="auto"
path="dataStatus"
class="pr-24px"
>
<NSelect
v-model:value="model.dataStatus"
placeholder="请选择数据状态(停招/新招/新增)"
@ -78,37 +90,57 @@ async function search() {
<NInput v-model:value="model.batchName" placeholder="请输入批次" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="专业类型" label-width="auto" path="majorType" class="pr-24px">
<NSelect
v-model:value="model.majorType"
placeholder="请选择专业类型"
:options="[]"
clearable
/>
<NSelect v-model:value="model.majorType" placeholder="请选择专业类型" :options="[]" clearable />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="二级专业类型" label-width="auto" path="majorTypeSub" class="pr-24px">
<NInput v-model:value="model.majorTypeSub" placeholder="请输入二级专业类型" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="科类(文/理)" label-width="auto" path="subjectType" class="pr-24px">
<NSelect
v-model:value="model.subjectType"
placeholder="请选择科类(文/理)"
:options="[]"
clearable
/>
<NSelect v-model:value="model.subjectType" placeholder="请选择科类(文/理)" :options="[]" clearable />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="录取方式缩写" label-width="auto" path="admissionWayShort" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="录取方式缩写"
label-width="auto"
path="admissionWayShort"
class="pr-24px"
>
<NInput v-model:value="model.admissionWayShort" placeholder="请输入录取方式缩写" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="对外录取方式" label-width="auto" path="admissionWayExternal" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="对外录取方式"
label-width="auto"
path="admissionWayExternal"
class="pr-24px"
>
<NInput v-model:value="model.admissionWayExternal" placeholder="请输入对外录取方式" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="对外录取方式运算符" label-width="auto" path="admissionWayExternalOp" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="对外录取方式运算符"
label-width="auto"
path="admissionWayExternalOp"
class="pr-24px"
>
<NInput v-model:value="model.admissionWayExternalOp" placeholder="请输入对外录取方式运算符" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="内部录取方式" label-width="auto" path="admissionWayInternal" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="内部录取方式"
label-width="auto"
path="admissionWayInternal"
class="pr-24px"
>
<NInput v-model:value="model.admissionWayInternal" placeholder="请输入内部录取方式" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="内部录取方式运算符" label-width="auto" path="admissionWayInternalOp" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="内部录取方式运算符"
label-width="auto"
path="admissionWayInternalOp"
class="pr-24px"
>
<NInput v-model:value="model.admissionWayInternalOp" placeholder="请输入内部录取方式运算符" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="计划招生人数" label-width="auto" path="planEnroll" class="pr-24px">
@ -120,22 +152,52 @@ async function search() {
<NFormItemGi span="24 s:12 m:6" label="学制(年)" label-width="auto" path="schoolingYears" class="pr-24px">
<NInput v-model:value="model.schoolingYears" placeholder="请输入学制(年)" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="院校限制说明" label-width="auto" path="enrollLimitDesc" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="院校限制说明"
label-width="auto"
path="enrollLimitDesc"
class="pr-24px"
>
<NInput v-model:value="model.enrollLimitDesc" placeholder="请输入院校限制说明" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="学费(元/年)" label-width="auto" path="tuitionFee" class="pr-24px">
<NInput v-model:value="model.tuitionFee" placeholder="请输入学费(元/年)" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="文化分数限制" label-width="auto" path="cultureScoreLimit" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="文化分数限制"
label-width="auto"
path="cultureScoreLimit"
class="pr-24px"
>
<NInput v-model:value="model.cultureScoreLimit" placeholder="请输入文化分数限制" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="专业分数限制" label-width="auto" path="majorScoreLimit" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="专业分数限制"
label-width="auto"
path="majorScoreLimit"
class="pr-24px"
>
<NInput v-model:value="model.majorScoreLimit" placeholder="请输入专业分数限制" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="语文成绩限制" label-width="auto" path="chineseScoreLimit" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="语文成绩限制"
label-width="auto"
path="chineseScoreLimit"
class="pr-24px"
>
<NInput v-model:value="model.chineseScoreLimit" placeholder="请输入语文成绩限制" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="英语成绩限制" label-width="auto" path="englishScoreLimit" class="pr-24px">
<NFormItemGi
span="24 s:12 m:6"
label="英语成绩限制"
label-width="auto"
path="englishScoreLimit"
class="pr-24px"
>
<NInput v-model:value="model.englishScoreLimit" placeholder="请输入英语成绩限制" />
</NFormItemGi>
<NFormItemGi :show-feedback="false" span="24" class="pr-24px">

View File

@ -25,8 +25,7 @@ type SchoolSubModuleType =
| 'schoolMajor'
| 'schoolEnrollPlan'
| 'schoolDorm'
| 'schoolMedia'
| 'schoolTag';
| 'schoolMedia';
type SchoolSubModuleButton = {
key: SchoolSubModuleType;
@ -59,7 +58,6 @@ const searchParams = ref<Api.Art.SchoolSearchParams>({
mainCode: null,
mainName: null,
shortName: null,
schoolIcon: null,
province: null,
city: null,
district: null,
@ -67,10 +65,6 @@ const searchParams = ref<Api.Art.SchoolSearchParams>({
educationLevel: null,
schoolNature: null,
supervisorDept: null,
establishYear: null,
campusAreaMu: null,
maleRatio: null,
femaleRatio: null,
params: {}
});
@ -119,12 +113,6 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
align: 'center',
minWidth: 120
},
{
key: 'schoolIcon',
title: '院校图标',
align: 'center',
minWidth: 120
},
{
key: 'province',
title: '省份',
@ -167,30 +155,6 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
align: 'center',
minWidth: 120
},
{
key: 'establishYear',
title: '建校时间(年)',
align: 'center',
minWidth: 120
},
{
key: 'campusAreaMu',
title: '占地面积(亩)',
align: 'center',
minWidth: 120
},
{
key: 'maleRatio',
title: '男生比例(%)',
align: 'center',
minWidth: 120
},
{
key: 'femaleRatio',
title: '女生比例(%)',
align: 'center',
minWidth: 120
},
{
key: 'remark',
title: '备注',

View File

@ -1,6 +1,6 @@
<script setup lang="tsx">
import { computed, ref } from 'vue';
import { NDivider } from 'naive-ui';
import { NDivider, NTag } from 'naive-ui';
import { fetchBatchDeleteSchoolDetail, fetchGetSchoolDetailList } from '@/service/api/art/school-detail';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
@ -32,12 +32,33 @@ const { hasAuth } = useAuth();
const searchParams = ref<Api.Art.SchoolDetailSearchParams>({
pageNum: 1,
pageSize: 10,
detailId: null,
schoolId: props.schoolId,
introduction: null,
schoolIcon: null,
address: null,
contact: null,
email: null,
website: null,
postcode: null,
establishYear: null,
campusAreaMu: null,
libraryCollection: null,
maleRatio: null,
femaleRatio: null,
is985: null,
is211: null,
isDoubleFirstClass: null,
isKeyUniversity: null,
tags: [],
studentCount: null,
teacherCount: null,
masterPoint: null,
doctorPoint: null,
keyMajorCount: null,
employmentRate: null,
satisfactionRate: null,
univId: null,
params: {}
});
@ -46,6 +67,17 @@ const requestParams = computed<Api.Art.SchoolDetailSearchParams>(() => ({
schoolId: props.schoolId ?? searchParams.value.schoolId
}));
function renderBooleanTag(value?: number | null) {
if (value === null || value === undefined) return '-';
return <NTag type={value === 1 ? 'success' : 'default'}>{value === 1 ? '是' : '否'}</NTag>;
}
function renderStringArray(values?: string[] | null) {
if (!values?.length) return '-';
return values.join(' / ');
}
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination, scrollX } =
useNaivePaginatedTable({
api: () => fetchGetSchoolDetailList(requestParams.value),
@ -85,6 +117,12 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
align: 'center',
minWidth: 120
},
{
key: 'schoolIcon',
title: '院校图标',
align: 'center',
minWidth: 140
},
{
key: 'address',
title: '学校地址',
@ -109,6 +147,89 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
align: 'center',
minWidth: 120
},
{
key: 'postcode',
title: '邮编',
align: 'center',
minWidth: 100
},
{
key: 'establishYear',
title: '建校年份',
align: 'center',
minWidth: 100
},
{
key: 'campusAreaMu',
title: '占地面积(亩)',
align: 'center',
minWidth: 120
},
{
key: 'libraryCollection',
title: '图书馆藏书量',
align: 'center',
minWidth: 120
},
{
key: 'studentCount',
title: '学生人数',
align: 'center',
minWidth: 100
},
{
key: 'teacherCount',
title: '教师人数',
align: 'center',
minWidth: 100
},
{
key: 'employmentRate',
title: '就业率(%)',
align: 'center',
minWidth: 100
},
{
key: 'satisfactionRate',
title: '满意度(%)',
align: 'center',
minWidth: 100
},
{
key: 'is985',
title: '985',
align: 'center',
minWidth: 80,
render: row => renderBooleanTag(row.is985)
},
{
key: 'is211',
title: '211',
align: 'center',
minWidth: 80,
render: row => renderBooleanTag(row.is211)
},
{
key: 'isDoubleFirstClass',
title: '双一流',
align: 'center',
minWidth: 90,
render: row => renderBooleanTag(row.isDoubleFirstClass)
},
{
key: 'isKeyUniversity',
title: '重点大学',
align: 'center',
minWidth: 100,
render: row => renderBooleanTag(row.isKeyUniversity)
},
{
key: 'tags',
title: '详情标签',
align: 'center',
minWidth: 160,
render: row => renderStringArray(row.tags)
},
{
key: 'remark',
title: '备注',

View File

@ -35,6 +35,9 @@ const visible = defineModel<boolean>('visible', {
const { formRef, validate, restoreValidation } = useNaiveForm();
const { createRequiredRule } = useFormRules();
const submitLoading = ref(false);
const detailTagInputValue = ref('');
const detailTagList = ref<string[]>([]);
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
@ -44,6 +47,17 @@ const title = computed(() => {
return titles[props.operateType];
});
const booleanOptions = [
{ value: 1, label: '是' },
{ value: 0, label: '否' }
];
function toNumberValue(value: CommonType.IdType | null | undefined) {
if (value === null || value === undefined || value === '') return null;
return typeof value === 'number' ? value : Number(value);
}
type Model = Api.Art.SchoolDetailOperateParams;
const model = ref<Model>(createDefaultModel());
@ -53,31 +67,94 @@ function createDefaultModel(): Model {
detailId: null,
schoolId: null,
introduction: '',
schoolIcon: '',
address: '',
contact: '',
email: '',
website: '',
postcode: '',
establishYear: null,
campusAreaMu: null,
libraryCollection: null,
maleRatio: null,
femaleRatio: null,
is985: null,
is211: null,
isDoubleFirstClass: null,
isKeyUniversity: null,
tags: [],
studentCount: null,
teacherCount: null,
masterPoint: null,
doctorPoint: null,
keyMajorCount: null,
employmentRate: null,
satisfactionRate: null,
univId: null,
remark: ''
};
}
type RuleKey = Extract<keyof Model, 'detailId' | 'schoolId'>;
type RuleKey = 'schoolId' | 'address' | 'contact';
const rules: Record<RuleKey, App.Global.FormRule> = {
detailId: createRequiredRule('主键ID不能为空'),
schoolId: createRequiredRule('关联学校主表ID不能为空')
schoolId: createRequiredRule('关联学校主表ID不能为空'),
address: createRequiredRule('学校地址不能为空'),
contact: createRequiredRule('联系电话不能为空')
};
function normalizeStringList(values?: string[] | null) {
if (!values?.length) return [];
const uniqueValues = new Set<string>();
values.forEach(value => {
const normalizedValue = String(value).trim();
if (normalizedValue) {
uniqueValues.add(normalizedValue);
}
});
return Array.from(uniqueValues);
}
function isSameStringList(source: string[], target: string[]) {
if (source.length !== target.length) return false;
return source.every((value, index) => value === target[index]);
}
function syncTagsToModel() {
model.value.tags = normalizeStringList(detailTagList.value);
}
function addDetailTags() {
const values = detailTagInputValue.value
.split(/[,]/)
.map(item => item.trim())
.filter(Boolean);
if (!values.length) return;
detailTagList.value = normalizeStringList([...detailTagList.value, ...values]);
detailTagInputValue.value = '';
}
function handleUpdateModelWhenEdit() {
model.value = createDefaultModel();
detailTagInputValue.value = '';
detailTagList.value = [];
if (props.operateType === 'add' && props.defaultSchoolId !== null) {
model.value.schoolId = props.defaultSchoolId;
}
if (props.operateType === 'edit' && props.rowData) {
Object.assign(model.value, jsonClone(props.rowData));
const detailData = jsonClone(props.rowData);
Object.assign(model.value, detailData);
detailTagList.value = normalizeStringList(detailData.tags);
}
syncTagsToModel();
}
function closeDrawer() {
@ -87,61 +164,138 @@ function closeDrawer() {
async function handleSubmit() {
await validate();
const { detailId, schoolId, introduction, address, contact, email, website, remark } = model.value;
submitLoading.value = true;
// request
if (props.operateType === 'add') {
const { error } = await fetchCreateSchoolDetail({
const {
detailId,
schoolId,
introduction,
schoolIcon,
address,
contact,
email,
website,
postcode,
establishYear,
campusAreaMu,
libraryCollection,
maleRatio,
femaleRatio,
is985,
is211,
isDoubleFirstClass,
isKeyUniversity,
studentCount,
teacherCount,
masterPoint,
doctorPoint,
keyMajorCount,
employmentRate,
satisfactionRate,
univId,
remark
});
} = model.value;
const payload: Api.Art.SchoolDetailOperateParams = {
detailId,
schoolId,
introduction,
schoolIcon,
address,
contact,
email,
website,
postcode,
establishYear,
campusAreaMu,
libraryCollection,
maleRatio,
femaleRatio,
is985,
is211,
isDoubleFirstClass,
isKeyUniversity,
tags: normalizeStringList(detailTagList.value),
studentCount,
teacherCount,
masterPoint,
doctorPoint,
keyMajorCount,
employmentRate,
satisfactionRate,
univId,
remark
};
if (props.operateType === 'add') {
const { error } = await fetchCreateSchoolDetail(payload);
submitLoading.value = false;
if (error) return;
}
if (props.operateType === 'edit') {
const { error } = await fetchUpdateSchoolDetail({
detailId,
schoolId,
introduction,
address,
contact,
email,
website,
remark
});
const { error } = await fetchUpdateSchoolDetail(payload);
submitLoading.value = false;
if (error) return;
}
window.$message?.success($t('common.updateSuccess'));
window.$message?.success(props.operateType === 'add' ? $t('common.addSuccess') : $t('common.updateSuccess'));
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
watch(visible, show => {
if (show) {
handleUpdateModelWhenEdit();
restoreValidation();
}
});
watch(
detailTagList,
tags => {
const normalizedTags = normalizeStringList(tags);
if (!isSameStringList(tags, normalizedTags)) {
detailTagList.value = normalizedTags;
return;
}
syncTagsToModel();
},
{ deep: true }
);
</script>
<template>
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%">
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="980" class="max-w-96%">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules">
<NForm ref="formRef" :model="model" :rules="rules" label-placement="left">
<NGrid :cols="24" :x-gap="16">
<NGi :span="8">
<NFormItem label="关联学校主表ID" path="schoolId">
<NInput
v-model:value="model.schoolId"
<NInputNumber
:value="toNumberValue(model.schoolId)"
:disabled="props.defaultSchoolId !== null"
class="w-full"
clearable
placeholder="请输入关联学校主表ID"
@update:value="value => (model.schoolId = value)"
/>
</NFormItem>
<NFormItem label="学校详细介绍(大文本)" path="introduction">
</NGi>
<NGi :span="8">
<NFormItem label="外部学校ID" path="univId">
<NInputNumber v-model:value="model.univId" class="w-full" clearable placeholder="请输入外部学校ID" />
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="邮编" path="postcode">
<NInput v-model:value="model.postcode" placeholder="请输入邮编" />
</NFormItem>
</NGi>
<NGi :span="24">
<NFormItem label="学校详细介绍" path="introduction">
<NInput
v-model:value="model.introduction"
:rows="3"
@ -149,26 +303,144 @@ watch(visible, () => {
placeholder="请输入学校详细介绍(大文本)"
/>
</NFormItem>
<NFormItem label="学校地址" path="address">
<NInput v-model:value="model.address" placeholder="请输入学校地址" />
</NFormItem>
<NFormItem label="联系电话" path="contact">
<NInput v-model:value="model.contact" placeholder="请输入联系电话" />
</NFormItem>
<NFormItem label="邮箱" path="email">
<NInput v-model:value="model.email" placeholder="请输入邮箱" />
</NGi>
<NGi :span="12">
<NFormItem label="院校图标" path="schoolIcon">
<NInput v-model:value="model.schoolIcon" placeholder="请输入院校图标地址" />
</NFormItem>
</NGi>
<NGi :span="12">
<NFormItem label="官网地址" path="website">
<NInput v-model:value="model.website" placeholder="请输入官网地址" />
</NFormItem>
</NGi>
<NGi :span="12">
<NFormItem label="学校地址" path="address">
<NInput v-model:value="model.address" placeholder="请输入学校地址" />
</NFormItem>
</NGi>
<NGi :span="12">
<NFormItem label="联系电话" path="contact">
<NInput v-model:value="model.contact" placeholder="请输入联系电话" />
</NFormItem>
</NGi>
<NGi :span="12">
<NFormItem label="邮箱" path="email">
<NInput v-model:value="model.email" placeholder="请输入邮箱" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="建校年份" path="establishYear">
<NInputNumber v-model:value="model.establishYear" class="w-full" clearable placeholder="请输入建校年份" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="占地面积(亩)" path="campusAreaMu">
<NInputNumber v-model:value="model.campusAreaMu" class="w-full" clearable placeholder="请输入占地面积" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="图书馆藏书量" path="libraryCollection">
<NInputNumber
v-model:value="model.libraryCollection"
class="w-full"
clearable
placeholder="请输入图书馆藏书量"
/>
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="学生人数" path="studentCount">
<NInputNumber v-model:value="model.studentCount" class="w-full" clearable placeholder="请输入学生人数" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="教师人数" path="teacherCount">
<NInputNumber v-model:value="model.teacherCount" class="w-full" clearable placeholder="请输入教师人数" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="硕士点数量" path="masterPoint">
<NInputNumber v-model:value="model.masterPoint" class="w-full" clearable placeholder="请输入硕士点数量" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="博士点数量" path="doctorPoint">
<NInputNumber v-model:value="model.doctorPoint" class="w-full" clearable placeholder="请输入博士点数量" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="重点专业数" path="keyMajorCount">
<NInputNumber v-model:value="model.keyMajorCount" class="w-full" clearable placeholder="请输入重点专业数" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="男生比例(%)" path="maleRatio">
<NInputNumber v-model:value="model.maleRatio" class="w-full" clearable placeholder="请输入男生比例" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="女生比例(%)" path="femaleRatio">
<NInputNumber v-model:value="model.femaleRatio" class="w-full" clearable placeholder="请输入女生比例" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="就业率(%)" path="employmentRate">
<NInputNumber v-model:value="model.employmentRate" class="w-full" clearable placeholder="请输入就业率" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="满意度(%)" path="satisfactionRate">
<NInputNumber v-model:value="model.satisfactionRate" class="w-full" clearable placeholder="请输入满意度" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="是否985" path="is985">
<NSelect v-model:value="model.is985" :options="booleanOptions" clearable placeholder="请选择" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="是否211" path="is211">
<NSelect v-model:value="model.is211" :options="booleanOptions" clearable placeholder="请选择" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="是否双一流" path="isDoubleFirstClass">
<NSelect v-model:value="model.isDoubleFirstClass" :options="booleanOptions" clearable placeholder="请选择" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="是否重点大学" path="isKeyUniversity">
<NSelect v-model:value="model.isKeyUniversity" :options="booleanOptions" clearable placeholder="请选择" />
</NFormItem>
</NGi>
<NGi :span="24">
<NFormItem label="详情标签" path="tags">
<NSpace vertical class="w-full">
<NSpace class="w-full" :wrap-item="false">
<NInput
v-model:value="detailTagInputValue"
class="flex-1"
placeholder="输入详情标签后回车或点击确认,可用逗号一次输入多个"
@keydown.enter.prevent="addDetailTags"
/>
<NButton type="primary" ghost @click="addDetailTags">确认</NButton>
</NSpace>
<NDynamicTags v-model:value="detailTagList" />
</NSpace>
</NFormItem>
</NGi>
<NGi :span="24">
<NFormItem label="备注" path="remark">
<NInput v-model:value="model.remark" :rows="3" type="textarea" placeholder="请输入备注" />
</NFormItem>
</NGi>
</NGrid>
</NForm>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
<NButton type="primary" :loading="submitLoading" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NDrawerContent>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { toRaw } from 'vue';
import { ref, toRaw, watch } from 'vue';
import { jsonClone } from '@sa/utils';
import { useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
@ -19,9 +19,60 @@ const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.Art.SchoolDetailSearchParams>('model', { required: true });
const defaultModel = jsonClone(toRaw(model.value));
const detailTagInputValue = ref('');
const detailTagList = ref<string[]>([]);
const booleanOptions = [
{ value: 1, label: '是' },
{ value: 0, label: '否' }
];
function toNumberValue(value: CommonType.IdType | null | undefined) {
if (value === null || value === undefined || value === '') return null;
return typeof value === 'number' ? value : Number(value);
}
function normalizeStringList(values?: string[] | null) {
if (!values?.length) return [];
const uniqueValues = new Set<string>();
values.forEach(value => {
const normalizedValue = String(value).trim();
if (normalizedValue) {
uniqueValues.add(normalizedValue);
}
});
return Array.from(uniqueValues);
}
function isSameStringList(source: string[], target: string[]) {
if (source.length !== target.length) return false;
return source.every((value, index) => value === target[index]);
}
function syncTagsToModel() {
model.value.tags = normalizeStringList(detailTagList.value);
}
function addDetailTags() {
const values = detailTagInputValue.value
.split(/[,]/)
.map(item => item.trim())
.filter(Boolean);
if (!values.length) return;
detailTagList.value = normalizeStringList([...detailTagList.value, ...values]);
detailTagInputValue.value = '';
}
function resetModel() {
Object.assign(model.value, defaultModel);
Object.assign(model.value, jsonClone(defaultModel));
detailTagInputValue.value = '';
detailTagList.value = normalizeStringList(defaultModel.tags);
syncTagsToModel();
}
async function reset() {
@ -34,6 +85,33 @@ async function search() {
await validate();
emit('search');
}
watch(
detailTagList,
tags => {
const normalizedTags = normalizeStringList(tags);
if (!isSameStringList(tags, normalizedTags)) {
detailTagList.value = normalizedTags;
return;
}
syncTagsToModel();
},
{ deep: true }
);
watch(
() => model.value.tags,
tags => {
const normalizedTags = normalizeStringList(tags);
if (!isSameStringList(detailTagList.value, normalizedTags)) {
detailTagList.value = normalizedTags;
}
},
{ immediate: true }
);
</script>
<template>
@ -42,17 +120,23 @@ async function search() {
<NCollapseItem :title="$t('common.search')" name="art-school-detail-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" label="关联学校主表ID" label-width="auto" path="schoolId" class="pr-24px">
<NInput v-model:value="model.schoolId" placeholder="请输入关联学校主表ID" />
<NFormItemGi span="24 s:12 m:6" label="详情ID" label-width="auto" path="detailId" class="pr-24px">
<NInputNumber
:value="toNumberValue(model.detailId)"
class="w-full"
clearable
placeholder="请输入详情ID"
@update:value="value => (model.detailId = value)"
/>
</NFormItemGi>
<NFormItemGi
span="24 s:12 m:6"
label="学校详细介绍(大文本)"
label-width="auto"
path="introduction"
class="pr-24px"
>
<NInput v-model:value="model.introduction" placeholder="请输入学校详细介绍(大文本)" />
<NFormItemGi span="24 s:12 m:6" label="学校ID" label-width="auto" path="schoolId" class="pr-24px">
<NInputNumber
:value="toNumberValue(model.schoolId)"
class="w-full"
clearable
placeholder="请输入学校ID"
@update:value="value => (model.schoolId = value)"
/>
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="学校地址" label-width="auto" path="address" class="pr-24px">
<NInput v-model:value="model.address" placeholder="请输入学校地址" />
@ -60,12 +144,97 @@ async function search() {
<NFormItemGi span="24 s:12 m:6" label="联系电话" label-width="auto" path="contact" class="pr-24px">
<NInput v-model:value="model.contact" placeholder="请输入联系电话" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="邮箱" label-width="auto" path="email" class="pr-24px">
<NInput v-model:value="model.email" placeholder="请输入邮箱" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="官网地址" label-width="auto" path="website" class="pr-24px">
<NInput v-model:value="model.website" placeholder="请输入官网地址" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="院校图标" label-width="auto" path="schoolIcon" class="pr-24px">
<NInput v-model:value="model.schoolIcon" placeholder="请输入院校图标地址" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="邮编" label-width="auto" path="postcode" class="pr-24px">
<NInput v-model:value="model.postcode" placeholder="请输入邮编" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="外部学校ID" label-width="auto" path="univId" class="pr-24px">
<NInputNumber v-model:value="model.univId" class="w-full" clearable placeholder="请输入外部学校ID" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="建校年份" label-width="auto" path="establishYear" class="pr-24px">
<NInputNumber v-model:value="model.establishYear" class="w-full" clearable placeholder="请输入建校年份" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="占地面积(亩)" label-width="auto" path="campusAreaMu" class="pr-24px">
<NInputNumber v-model:value="model.campusAreaMu" class="w-full" clearable placeholder="请输入占地面积" />
</NFormItemGi>
<NFormItemGi
span="24 s:12 m:6"
label="图书馆藏书量"
label-width="auto"
path="libraryCollection"
class="pr-24px"
>
<NInputNumber
v-model:value="model.libraryCollection"
class="w-full"
clearable
placeholder="请输入图书馆藏书量"
/>
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="学生人数" label-width="auto" path="studentCount" class="pr-24px">
<NInputNumber v-model:value="model.studentCount" class="w-full" clearable placeholder="请输入学生人数" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="教师人数" label-width="auto" path="teacherCount" class="pr-24px">
<NInputNumber v-model:value="model.teacherCount" class="w-full" clearable placeholder="请输入教师人数" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="硕士点数量" label-width="auto" path="masterPoint" class="pr-24px">
<NInputNumber v-model:value="model.masterPoint" class="w-full" clearable placeholder="请输入硕士点数量" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="博士点数量" label-width="auto" path="doctorPoint" class="pr-24px">
<NInputNumber v-model:value="model.doctorPoint" class="w-full" clearable placeholder="请输入博士点数量" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="重点专业数" label-width="auto" path="keyMajorCount" class="pr-24px">
<NInputNumber v-model:value="model.keyMajorCount" class="w-full" clearable placeholder="请输入重点专业数" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="就业率(%)" label-width="auto" path="employmentRate" class="pr-24px">
<NInputNumber v-model:value="model.employmentRate" class="w-full" clearable placeholder="请输入就业率" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="满意度(%)" label-width="auto" path="satisfactionRate" class="pr-24px">
<NInputNumber v-model:value="model.satisfactionRate" class="w-full" clearable placeholder="请输入满意度" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="是否985" label-width="auto" path="is985" class="pr-24px">
<NSelect v-model:value="model.is985" :options="booleanOptions" clearable placeholder="请选择" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="是否211" label-width="auto" path="is211" class="pr-24px">
<NSelect v-model:value="model.is211" :options="booleanOptions" clearable placeholder="请选择" />
</NFormItemGi>
<NFormItemGi
span="24 s:12 m:6"
label="是否双一流"
label-width="auto"
path="isDoubleFirstClass"
class="pr-24px"
>
<NSelect v-model:value="model.isDoubleFirstClass" :options="booleanOptions" clearable placeholder="请选择" />
</NFormItemGi>
<NFormItemGi
span="24 s:12 m:6"
label="是否重点大学"
label-width="auto"
path="isKeyUniversity"
class="pr-24px"
>
<NSelect v-model:value="model.isKeyUniversity" :options="booleanOptions" clearable placeholder="请选择" />
</NFormItemGi>
<NFormItemGi span="24" label="详情标签" label-width="auto" path="tags" class="pr-24px">
<NSpace vertical class="w-full">
<NSpace class="w-full" :wrap-item="false">
<NInput
v-model:value="detailTagInputValue"
class="flex-1"
placeholder="输入详情标签后回车或点击确认,可用逗号一次输入多个"
@keydown.enter.prevent="addDetailTags"
/>
<NButton type="primary" ghost @click="addDetailTags">确认</NButton>
</NSpace>
<NDynamicTags v-model:value="detailTagList" />
</NSpace>
</NFormItemGi>
<NFormItemGi :show-feedback="false" span="24" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">

View File

@ -39,6 +39,8 @@ const enrollCodeInputValue = ref('');
const enrollCodeList = ref<string[]>([]);
const schoolTagInputValue = ref('');
const schoolTagList = ref<string[]>([]);
const detailTagInputValue = ref('');
const detailTagList = ref<string[]>([]);
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
@ -48,6 +50,11 @@ const title = computed(() => {
return titles[props.operateType];
});
const booleanOptions = [
{ value: 1, label: '是' },
{ value: 0, label: '否' }
];
type Model = Api.Art.SchoolWithDetailOperateParams;
const model = ref<Model>(createDefaultModel());
@ -59,7 +66,6 @@ function createDefaultModel(): Model {
mainCode: '',
mainName: '',
shortName: '',
schoolIcon: '',
province: '',
city: '',
district: '',
@ -67,20 +73,36 @@ function createDefaultModel(): Model {
educationLevel: '',
schoolNature: '',
supervisorDept: '',
establishYear: null,
campusAreaMu: null,
maleRatio: null,
femaleRatio: null,
remark: ''
},
detail: {
detailId: null,
schoolId: null,
introduction: '',
schoolIcon: '',
address: '',
contact: '',
email: '',
website: '',
postcode: '',
establishYear: null,
campusAreaMu: null,
libraryCollection: null,
maleRatio: null,
femaleRatio: null,
is985: null,
is211: null,
isDoubleFirstClass: null,
isKeyUniversity: null,
tags: [],
studentCount: null,
teacherCount: null,
masterPoint: null,
doctorPoint: null,
keyMajorCount: null,
employmentRate: null,
satisfactionRate: null,
univId: null,
remark: ''
},
enrollCodes: [],
@ -114,33 +136,35 @@ function normalizeStringList(values?: string[] | null) {
function isSameStringList(source: string[], target: string[]) {
if (source.length !== target.length) return false;
return source.every((code, index) => code === target[index]);
return source.every((value, index) => value === target[index]);
}
function appendValues(inputValue: string, target: string[]) {
const values = inputValue
.split(/[,]/)
.map(item => item.trim())
.filter(Boolean);
if (!values.length) return target;
return normalizeStringList([...target, ...values]);
}
function addEnrollCodes() {
const inputCodes = enrollCodeInputValue.value
.split(/[,]/)
.map(code => code.trim())
.filter(Boolean);
if (!inputCodes.length) return;
enrollCodeList.value = normalizeStringList([...enrollCodeList.value, ...inputCodes]);
enrollCodeList.value = appendValues(enrollCodeInputValue.value, enrollCodeList.value);
enrollCodeInputValue.value = '';
}
function addSchoolTags() {
const inputTags = schoolTagInputValue.value
.split(/[,]/)
.map(tag => tag.trim())
.filter(Boolean);
if (!inputTags.length) return;
schoolTagList.value = normalizeStringList([...schoolTagList.value, ...inputTags]);
schoolTagList.value = appendValues(schoolTagInputValue.value, schoolTagList.value);
schoolTagInputValue.value = '';
}
function addDetailTags() {
detailTagList.value = appendValues(detailTagInputValue.value, detailTagList.value);
detailTagInputValue.value = '';
}
function syncEnrollCodesToModel() {
model.value.enrollCodes = normalizeStringList(enrollCodeList.value);
}
@ -149,6 +173,10 @@ function syncSchoolTagsToModel() {
model.value.schoolTags = normalizeStringList(schoolTagList.value);
}
function syncDetailTagsToModel() {
model.value.detail.tags = normalizeStringList(detailTagList.value);
}
async function loadEditFormData(schoolId: CommonType.IdType) {
detailLoading.value = true;
@ -172,11 +200,14 @@ async function loadEditFormData(schoolId: CommonType.IdType) {
}
if (!detailRes.error && detailRes.data) {
Object.assign(model.value.detail, jsonClone(detailRes.data));
const detailData = jsonClone(detailRes.data);
Object.assign(model.value.detail, detailData);
detailTagList.value = normalizeStringList(detailData.tags);
}
syncEnrollCodesToModel();
syncSchoolTagsToModel();
syncDetailTagsToModel();
model.value.detail.schoolId = model.value.school.schoolId;
detailLoading.value = false;
}
@ -187,12 +218,15 @@ async function handleUpdateModelWhenOpen() {
enrollCodeList.value = [];
schoolTagInputValue.value = '';
schoolTagList.value = [];
detailTagInputValue.value = '';
detailTagList.value = [];
if (props.operateType === 'edit' && props.rowData?.schoolId !== null && props.rowData?.schoolId !== undefined) {
await loadEditFormData(props.rowData.schoolId);
} else {
syncEnrollCodesToModel();
syncSchoolTagsToModel();
syncDetailTagsToModel();
}
}
@ -216,13 +250,94 @@ async function handleSubmit() {
submitLoading.value = true;
const {
schoolId,
mainCode,
mainName,
shortName,
province,
city,
district,
universityType,
educationLevel,
schoolNature,
supervisorDept,
remark: schoolRemark
} = model.value.school;
const {
detailId,
introduction,
schoolIcon,
address,
contact,
email,
website,
postcode,
establishYear,
campusAreaMu,
libraryCollection,
maleRatio,
femaleRatio,
is985,
is211,
isDoubleFirstClass,
isKeyUniversity,
studentCount,
teacherCount,
masterPoint,
doctorPoint,
keyMajorCount,
employmentRate,
satisfactionRate,
univId,
remark: detailRemark
} = model.value.detail;
const payload: Api.Art.SchoolWithDetailOperateParams = {
school: {
...model.value.school
schoolId,
mainCode,
mainName,
shortName,
province,
city,
district,
universityType,
educationLevel,
schoolNature,
supervisorDept,
remark: schoolRemark
},
detail: {
...model.value.detail,
schoolId: model.value.school.schoolId
detailId,
schoolId,
introduction,
schoolIcon,
address,
contact,
email,
website,
postcode,
establishYear,
campusAreaMu,
libraryCollection,
maleRatio,
femaleRatio,
is985,
is211,
isDoubleFirstClass,
isKeyUniversity,
tags: normalizeStringList(detailTagList.value),
studentCount,
teacherCount,
masterPoint,
doctorPoint,
keyMajorCount,
employmentRate,
satisfactionRate,
univId,
remark: detailRemark
},
enrollCodes: normalizeStringList(enrollCodeList.value),
schoolTags: normalizeStringList(schoolTagList.value)
@ -288,10 +403,25 @@ watch(
},
{ deep: true }
);
watch(
detailTagList,
tags => {
const normalizedTags = normalizeStringList(tags);
if (!isSameStringList(tags, normalizedTags)) {
detailTagList.value = normalizedTags;
return;
}
syncDetailTagsToModel();
},
{ deep: true }
);
</script>
<template>
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="960" class="max-w-92%">
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="1120" class="max-w-96%">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules" label-placement="left">
<NCard title="院校基础信息" size="small" :bordered="true" class="mb-16px">
@ -343,11 +473,6 @@ watch(
</NSpace>
</NFormItem>
</NGi>
<NGi :span="12">
<NFormItem label="院校图标" path="school.schoolIcon">
<NInput v-model:value="model.school.schoolIcon" placeholder="请输入院校图标地址" />
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="省份" path="school.province">
<NInput v-model:value="model.school.province" placeholder="请输入省份" />
@ -365,7 +490,7 @@ watch(
</NGi>
<NGi :span="8">
<NFormItem label="大学类型" path="school.universityType">
<NInput v-model:value="model.school.universityType" placeholder="综合/工科/财经/艺术" />
<NInput v-model:value="model.school.universityType" placeholder="请输入大学类型,如综合/工科/艺术" />
</NFormItem>
</NGi>
<NGi :span="8">
@ -374,7 +499,6 @@ watch(
v-model:value="model.school.educationLevel"
placeholder="请选择学历层次"
:options="[
{ value: '', label: '请选择' },
{ value: '本科', label: '本科' },
{ value: '专科', label: '专科' }
]"
@ -401,26 +525,6 @@ watch(
<NInput v-model:value="model.school.supervisorDept" placeholder="请输入主管部门" />
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="建校时间(年)" path="school.establishYear">
<NInput v-model:value="model.school.establishYear" placeholder="请输入建校年份" />
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="占地面积(亩)" path="school.campusAreaMu">
<NInput v-model:value="model.school.campusAreaMu" placeholder="请输入占地面积(亩)" />
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="男生比例(%)" path="school.maleRatio">
<NInput v-model:value="model.school.maleRatio" placeholder="请输入男生比例(%)" />
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="女生比例(%)" path="school.femaleRatio">
<NInput v-model:value="model.school.femaleRatio" placeholder="请输入女生比例(%)" />
</NFormItem>
</NGi>
<NGi :span="24">
<NFormItem label="备注" path="school.remark">
<NInput v-model:value="model.school.remark" :rows="2" type="textarea" placeholder="请输入备注" />
@ -441,6 +545,16 @@ watch(
/>
</NFormItem>
</NGi>
<NGi :span="12">
<NFormItem label="院校图标" path="detail.schoolIcon">
<NInput v-model:value="model.detail.schoolIcon" placeholder="请输入院校图标地址" />
</NFormItem>
</NGi>
<NGi :span="12">
<NFormItem label="官网地址" path="detail.website">
<NInput v-model:value="model.detail.website" placeholder="请输入官网地址" />
</NFormItem>
</NGi>
<NGi :span="12">
<NFormItem label="学校地址" path="detail.address">
<NInput v-model:value="model.detail.address" placeholder="请输入学校地址" />
@ -451,14 +565,150 @@ watch(
<NInput v-model:value="model.detail.contact" placeholder="请输入联系电话" />
</NFormItem>
</NGi>
<NGi :span="12">
<NGi :span="8">
<NFormItem label="邮箱" path="detail.email">
<NInput v-model:value="model.detail.email" placeholder="请输入邮箱" />
</NFormItem>
</NGi>
<NGi :span="12">
<NFormItem label="官网地址" path="detail.website">
<NInput v-model:value="model.detail.website" placeholder="请输入官网地址" />
<NGi :span="8">
<NFormItem label="邮编" path="detail.postcode">
<NInput v-model:value="model.detail.postcode" placeholder="请输入邮编" />
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="外部学校ID" path="detail.univId">
<NInputNumber v-model:value="model.detail.univId" class="w-full" clearable placeholder="请输入外部学校ID" />
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="建校年份" path="detail.establishYear">
<NInputNumber v-model:value="model.detail.establishYear" class="w-full" clearable placeholder="请输入建校年份" />
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="占地面积(亩)" path="detail.campusAreaMu">
<NInputNumber
v-model:value="model.detail.campusAreaMu"
class="w-full"
clearable
placeholder="请输入占地面积(亩)"
/>
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="图书馆藏书量" path="detail.libraryCollection">
<NInputNumber
v-model:value="model.detail.libraryCollection"
class="w-full"
clearable
placeholder="请输入图书馆藏书量"
/>
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="男生比例(%)" path="detail.maleRatio">
<NInputNumber v-model:value="model.detail.maleRatio" class="w-full" clearable placeholder="请输入男生比例" />
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="女生比例(%)" path="detail.femaleRatio">
<NInputNumber v-model:value="model.detail.femaleRatio" class="w-full" clearable placeholder="请输入女生比例" />
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="就业率(%)" path="detail.employmentRate">
<NInputNumber
v-model:value="model.detail.employmentRate"
class="w-full"
clearable
placeholder="请输入就业率"
/>
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="满意度(%)" path="detail.satisfactionRate">
<NInputNumber
v-model:value="model.detail.satisfactionRate"
class="w-full"
clearable
placeholder="请输入满意度"
/>
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="是否985" path="detail.is985">
<NSelect v-model:value="model.detail.is985" :options="booleanOptions" clearable placeholder="请选择" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="是否211" path="detail.is211">
<NSelect v-model:value="model.detail.is211" :options="booleanOptions" clearable placeholder="请选择" />
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="是否双一流" path="detail.isDoubleFirstClass">
<NSelect
v-model:value="model.detail.isDoubleFirstClass"
:options="booleanOptions"
clearable
placeholder="请选择"
/>
</NFormItem>
</NGi>
<NGi :span="6">
<NFormItem label="是否重点大学" path="detail.isKeyUniversity">
<NSelect
v-model:value="model.detail.isKeyUniversity"
:options="booleanOptions"
clearable
placeholder="请选择"
/>
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="学生人数" path="detail.studentCount">
<NInputNumber v-model:value="model.detail.studentCount" class="w-full" clearable placeholder="请输入学生人数" />
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="教师人数" path="detail.teacherCount">
<NInputNumber v-model:value="model.detail.teacherCount" class="w-full" clearable placeholder="请输入教师人数" />
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="重点专业数" path="detail.keyMajorCount">
<NInputNumber
v-model:value="model.detail.keyMajorCount"
class="w-full"
clearable
placeholder="请输入重点专业数"
/>
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="硕士点数量" path="detail.masterPoint">
<NInputNumber v-model:value="model.detail.masterPoint" class="w-full" clearable placeholder="请输入硕士点数量" />
</NFormItem>
</NGi>
<NGi :span="8">
<NFormItem label="博士点数量" path="detail.doctorPoint">
<NInputNumber v-model:value="model.detail.doctorPoint" class="w-full" clearable placeholder="请输入博士点数量" />
</NFormItem>
</NGi>
<NGi :span="24">
<NFormItem label="详情标签" path="detail.tags">
<NSpace vertical class="w-full">
<NSpace class="w-full" :wrap-item="false">
<NInput
v-model:value="detailTagInputValue"
class="flex-1"
placeholder="输入详情标签后回车或点击确认,可用逗号一次输入多个"
@keydown.enter.prevent="addDetailTags"
/>
<NButton type="primary" ghost @click="addDetailTags">确认</NButton>
</NSpace>
<NDynamicTags v-model:value="detailTagList" />
</NSpace>
</NFormItem>
</NGi>
<NGi :span="24">

View File

@ -51,13 +51,7 @@ async function search() {
>
<NInput v-model:value="model.mainCode" placeholder="请输入学校编码(唯一标识,如国标代码)" />
</NFormItemGi>
<NFormItemGi
span="24 s:12 m:6"
label="学校主名称"
label-width="auto"
path="mainName"
class="pr-24px"
>
<NFormItemGi span="24 s:12 m:6" label="学校主名称" label-width="auto" path="mainName" class="pr-24px">
<NInput v-model:value="model.mainName" placeholder="请输入学校主名称(官方全称)" />
</NFormItemGi>
<NFormItemGi
@ -69,9 +63,6 @@ async function search() {
>
<NInput v-model:value="model.shortName" placeholder="请输入学校简称(备用)" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="院校图标" label-width="auto" path="schoolIcon" class="pr-24px">
<NInput v-model:value="model.schoolIcon" placeholder="请输入院校图标" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="省份" label-width="auto" path="province" class="pr-24px">
<NInput v-model:value="model.province" placeholder="请输入省份" />
</NFormItemGi>
@ -122,24 +113,6 @@ async function search() {
>
<NInput v-model:value="model.supervisorDept" placeholder="请输入主管部门:教育部/工信部/民委..." />
</NFormItemGi>
<NFormItemGi
span="24 s:12 m:6"
label="建校时间(年)"
label-width="auto"
path="establishYear"
class="pr-24px"
>
<NInput v-model:value="model.establishYear" placeholder="请输入建校时间(年)" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="占地面积(亩)" label-width="auto" path="campusAreaMu" class="pr-24px">
<NInput v-model:value="model.campusAreaMu" placeholder="请输入占地面积(亩)" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="男生比例(%)" label-width="auto" path="maleRatio" class="pr-24px">
<NInput v-model:value="model.maleRatio" placeholder="请输入男生比例(%)" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="女生比例(%)" label-width="auto" path="femaleRatio" class="pr-24px">
<NInput v-model:value="model.femaleRatio" placeholder="请输入女生比例(%)" />
</NFormItemGi>
<NFormItemGi :show-feedback="false" span="24" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">

View File

@ -8,7 +8,7 @@ import SchoolEnrollPlanList from './school-enroll-plan/index.vue';
import SchoolMajorList from './school-major/index.vue';
import SchoolMediaList from './school-media/index.vue';
import SchoolNameList from './school-name/index.vue';
//import SchoolTagList from './school-tag/index.vue';
// import SchoolTagList from './school-tag/index.vue';
defineOptions({
name: 'SchoolSubTableModal'
@ -63,7 +63,7 @@ const componentMap = {
schoolMajor: SchoolMajorList,
schoolEnrollPlan: SchoolEnrollPlanList,
schoolDorm: SchoolDormList,
schoolMedia: SchoolMediaList,
schoolMedia: SchoolMediaList
} as const;
const activeComponent = computed(() => componentMap[activeTab.value]);
@ -158,7 +158,7 @@ watch(
<NGi :span="8">办学性质{{ formatFieldValue(schoolInfo?.schoolNature) }}</NGi>
<NGi :span="8">学历层次{{ formatFieldValue(schoolInfo?.educationLevel) }}</NGi>
<NGi :span="8">主管部门{{ formatFieldValue(schoolInfo?.supervisorDept) }}</NGi>
<NGi :span="8">建校年份{{ formatFieldValue(schoolInfo?.establishYear) }}</NGi>
<NGi :span="8">学校简称{{ formatFieldValue(schoolInfo?.shortName) }}</NGi>
</NGrid>
</NCard>
<NDivider class="my-10px">子表管理区</NDivider>