wz-management-fronted/src/views/art/school-recruit-major-history/index.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>