574 lines
16 KiB
Vue
574 lines
16 KiB
Vue
<script setup lang="tsx">
|
|
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 } from '@/hooks/common/table';
|
|
import { $t } from '@/locales';
|
|
import ButtonIcon from '@/components/custom/button-icon.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();
|
|
|
|
const searchParams = ref<Api.Art.SchoolRecruitMajorHistorySearchParams>({
|
|
pageNum: 1,
|
|
pageSize: 10,
|
|
recruitMajorId: null,
|
|
schoolId: null,
|
|
schoolCode: null,
|
|
collegeCode: null,
|
|
schoolName: null,
|
|
majorId: null,
|
|
majorCode: null,
|
|
majorName: null,
|
|
enrollCode: null,
|
|
majorType: null,
|
|
majorTypeSub: null,
|
|
mainExamSubject: null,
|
|
year: null,
|
|
subjectType: null,
|
|
batchName: null,
|
|
admissionFormula: null,
|
|
probabilityOperator: null,
|
|
controlScore: null,
|
|
admissionScore: null,
|
|
planEnroll: null,
|
|
filedAmount: null,
|
|
admitAmount: null,
|
|
firstChoiceAdmitAmount: null,
|
|
minScoreDiff: null,
|
|
tuitionFee: null,
|
|
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),
|
|
transform: response => defaultTransform(response),
|
|
onPaginationParamsChange: params => {
|
|
searchParams.value.pageNum = params.page;
|
|
searchParams.value.pageSize = params.pageSize;
|
|
},
|
|
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',
|
|
width: 48
|
|
},
|
|
{
|
|
key: 'index',
|
|
title: $t('common.index'),
|
|
align: 'center',
|
|
width: 64,
|
|
render: (_, index) => index + 1
|
|
},
|
|
{
|
|
key: 'historyId',
|
|
title: '历年录取数据ID',
|
|
align: 'center',
|
|
minWidth: 160
|
|
},
|
|
...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: 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',
|
|
fixed: 'right',
|
|
width: 200,
|
|
render: (row: TableRow) => {
|
|
const rowKey = resolveRowKey(row);
|
|
const editing = editingRowKey.value === rowKey;
|
|
const saving = savingRowKey.value === rowKey;
|
|
|
|
if (editing) {
|
|
return (
|
|
<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 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"
|
|
icon="material-symbols:delete-outline"
|
|
tooltipContent={$t('common.delete')}
|
|
popconfirmContent={$t('common.confirmDelete')}
|
|
onPositiveClick={() => handleDelete(row.historyId)}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
};
|
|
}
|
|
|
|
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() {
|
|
await ensureEditingGuard(async () => {
|
|
const { error } = await fetchBatchDeleteSchoolRecruitMajorHistory(checkedRowKeys.value);
|
|
if (error) return;
|
|
window.$message?.success($t('common.deleteSuccess'));
|
|
checkedRowKeys.value = [];
|
|
await getData();
|
|
});
|
|
}
|
|
|
|
async function handleDelete(historyId: CommonType.IdType) {
|
|
await ensureEditingGuard(async () => {
|
|
const { error } = await fetchBatchDeleteSchoolRecruitMajorHistory([historyId]);
|
|
if (error) return;
|
|
window.$message?.success($t('common.deleteSuccess'));
|
|
await getData();
|
|
});
|
|
}
|
|
|
|
function handleExport() {
|
|
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="handleSearch" />
|
|
<NCard title="院校招录专业历年录取数据列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
|
|
<template #header-extra>
|
|
<TableHeaderOperation
|
|
v-model:columns="columnChecks"
|
|
:disabled-delete="checkedRowKeys.length === 0"
|
|
:loading="loading"
|
|
:show-add="hasAuth('art:schoolRecruitMajorHistory:add')"
|
|
:show-delete="hasAuth('art:schoolRecruitMajorHistory:remove')"
|
|
:show-export="hasAuth('art:schoolRecruitMajorHistory:export')"
|
|
@add="handleAddRow"
|
|
@delete="handleBatchDelete"
|
|
@export="handleExport"
|
|
@refresh="handleRefresh"
|
|
/>
|
|
</template>
|
|
<NDataTable
|
|
v-model:checked-row-keys="checkedRowKeys"
|
|
:columns="columns"
|
|
:data="tableData"
|
|
size="small"
|
|
:flex-height="!appStore.isMobile"
|
|
:scroll-x="scrollX"
|
|
:loading="loading"
|
|
remote
|
|
:row-key="resolveRowKey"
|
|
:row-class-name="rowClassName"
|
|
:pagination="mobilePagination"
|
|
class="sm:h-full"
|
|
/>
|
|
</NCard>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
:deep(.inline-edit-row .n-data-table-td) {
|
|
background-color: rgba(24, 160, 88, 0.08);
|
|
}
|
|
</style>
|