561 lines
16 KiB
Vue
Executable File
561 lines
16 KiB
Vue
Executable File
<template>
|
||
<div class="titlerelation-page">
|
||
<div class="page-wrapper">
|
||
<!-- 内容区:最上搜索,其次标题+按钮,再下方表格 -->
|
||
<div class="content-block">
|
||
<!-- 最上:搜索区 -->
|
||
<div v-show="showSearch" class="content-block__filter">
|
||
<search-form :model="search" ref="searchFormRef" @keyup-enter="handleFilter">
|
||
<template #default="{ visible }">
|
||
<template v-if="visible">
|
||
<el-form-item label="姓名" prop="realName">
|
||
<el-input v-model="search.realName" clearable placeholder="请输入姓名" class="filter-input" />
|
||
</el-form-item>
|
||
<el-form-item label="工号" prop="teacherNo">
|
||
<el-input v-model="search.teacherNo" clearable placeholder="请输入工号" class="filter-input" />
|
||
</el-form-item>
|
||
<el-form-item label="审核状态" prop="state">
|
||
<el-select v-model="search.state" clearable placeholder="请选择审核状态" class="filter-select">
|
||
<el-option v-for="item in professionalState" :key="item.value" :label="item.label" :value="item.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="职称" prop="professionalTitleConfigId">
|
||
<el-select v-model="search.professionalTitleConfigId" clearable filterable placeholder="请选择职称" class="filter-select">
|
||
<el-option v-for="item in professionalTitleList" :key="item.id" :label="item.professionalTitle" :value="item.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="专业技术职务" prop="majorStation">
|
||
<el-select v-model="search.majorStation" clearable filterable placeholder="请选择专业技术职务" class="filter-select">
|
||
<el-option v-for="item in majorStationList" :key="item.id" :label="item.majorStationName" :value="item.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</template>
|
||
</template>
|
||
<template #actions>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="handleFilter" icon="Search">查询</el-button>
|
||
<el-button @click="resetQuery" icon="Refresh">重置</el-button>
|
||
</el-form-item>
|
||
</template>
|
||
</search-form>
|
||
</div>
|
||
|
||
<!-- 其次:左侧按钮,右侧 RightToolbar -->
|
||
<div class="content-block__header">
|
||
<div class="header-actions">
|
||
<div class="action-group">
|
||
<el-button v-if="hasAuth('professional_professionaltitlerelation_add')" type="primary" icon="FolderAdd" @click="handleAdd"
|
||
>新增</el-button
|
||
>
|
||
<el-button
|
||
v-if="hasAuth('professional_teacherbase_export')"
|
||
type="warning"
|
||
plain
|
||
icon="Download"
|
||
@click="handleDownLoadWord"
|
||
:loading="exportLoading"
|
||
>导出信息</el-button
|
||
>
|
||
<el-button
|
||
v-auth="'professional_teacherinfo_import'"
|
||
type="primary"
|
||
plain
|
||
icon="UploadFilled"
|
||
:loading="exportLoading"
|
||
@click="handleImportDialog"
|
||
>导入信息
|
||
</el-button>
|
||
</div>
|
||
<div class="header-right">
|
||
<RightToolbar v-model:showSearch="showSearch" @queryTable="getDataList" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 再下方:表格 -->
|
||
<el-table
|
||
ref="tableRef"
|
||
:data="state.dataList"
|
||
row-key="id"
|
||
v-loading="state.loading"
|
||
border
|
||
stripe
|
||
class="titlerelation-table"
|
||
:cell-style="tableStyle.cellStyle"
|
||
:header-cell-style="tableStyle.headerCellStyle"
|
||
>
|
||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||
|
||
<el-table-column prop="state" label="审核状态" width="120" align="center">
|
||
<template #default="scope">
|
||
<AuditState :state="scope.row.state" />
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="姓名/工号" min-width="150" align="center" show-overflow-tooltip>
|
||
<template #default="scope">
|
||
<TeacherNameNo :name="scope.row.realName" :no="scope.row.teacherNo" />
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column prop="professionalTitleConfigId" label="职称" min-width="150" align="center" show-overflow-tooltip>
|
||
<template #default="scope">
|
||
{{ getProfessionalTitleName(scope.row.professionalTitleConfigId) }}
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column prop="majorStation" label="专业技术职务" min-width="150" align="center" show-overflow-tooltip>
|
||
<template #default="scope">
|
||
{{ getMajorStationName(scope.row.majorStation) }}
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="证明材料" width="120" align="center">
|
||
<template #default="scope">
|
||
<el-button
|
||
v-if="scope.row.evidence && scope.row.evidence !== ''"
|
||
type="primary"
|
||
link
|
||
icon="Document"
|
||
@click="handlePreview(scope.row.srcList)"
|
||
>查看
|
||
</el-button>
|
||
<span v-else class="empty-text">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column prop="backReason" label="驳回理由" min-width="150" align="center" show-overflow-tooltip />
|
||
|
||
<el-table-column label="操作" width="280" align="center" fixed="right">
|
||
<template #default="scope">
|
||
<div class="op-cell">
|
||
<el-button
|
||
v-if="hasAuth('professional_professionaltitlerelation_edit') && (scope.row.state === '0' || scope.row.state === '-2')"
|
||
type="primary"
|
||
link
|
||
icon="edit-pen"
|
||
@click="handleEdit(scope.row)"
|
||
>修改
|
||
</el-button>
|
||
<el-button
|
||
v-if="hasAuth('professional_professionaltitlerelation_exam') && scope.row.canExam"
|
||
type="success"
|
||
link
|
||
icon="CircleCheck"
|
||
@click="changeState(scope.row, 1)"
|
||
>通过
|
||
</el-button>
|
||
<el-button
|
||
v-if="hasAuth('professional_professionaltitlerelation_exam') && scope.row.canDeptExam"
|
||
type="success"
|
||
link
|
||
icon="CircleCheck"
|
||
@click="changeState(scope.row, 1)"
|
||
>部门通过
|
||
</el-button>
|
||
<el-button
|
||
v-if="hasAuth('professional_professionaltitlerelation_exam') && scope.row.canBack"
|
||
type="danger"
|
||
link
|
||
icon="CircleClose"
|
||
@click="changeState(scope.row, -2)"
|
||
>驳回
|
||
</el-button>
|
||
<el-button
|
||
v-if="hasAuth('professional_professionaltitlerelation_exam') && scope.row.canDeptBack"
|
||
type="danger"
|
||
link
|
||
icon="CircleClose"
|
||
@click="changeState(scope.row, -2)"
|
||
>部门驳回
|
||
</el-button>
|
||
<el-button v-if="hasAuth('professional_professionaltitlerelation_del')" type="danger" link icon="delete" @click="handleDel(scope.row)"
|
||
>删除
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- 分页 -->
|
||
<pagination v-bind="state.pagination" @current-change="currentChangeHandle" @size-change="sizeChangeHandle" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 材料预览:图片直接显示,PDF 在组件内部 dialog 中显示 -->
|
||
<preview-file v-for="src in imgUrl" :key="src.title" :authSrc="src.url" dialog-title="职称材料" />
|
||
|
||
<!-- 子组件 -->
|
||
<MultiDialog ref="multiDialogRef" @getList="getDataList" :page="state.pagination" :nowRow="null" />
|
||
<DataForm ref="dataFormRef" @refreshData="handleFilter" />
|
||
<ProfessionalBackResaon ref="backReasonRef" @refreshData="handleFilter" />
|
||
|
||
<import-teacher-other-info ref="importTeacherOtherInfoRef" @refreshData="handleFilter"></import-teacher-other-info>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, onMounted, nextTick } from 'vue';
|
||
import { BasicTableProps, useTable } from '/@/hooks/table';
|
||
import { useAuth } from '/@/hooks/auth';
|
||
import { useMessage } from '/@/hooks/message';
|
||
import { useMessageBox } from '/@/hooks/message';
|
||
import { useDict } from '/@/hooks/dict';
|
||
// 接口
|
||
import { fetchList, examObj, delObj } from '/@/api/professional/professionaluser/professionaltitlerelation';
|
||
import { getProfessionalTitleList } from '/@/api/professional/rsbase/professionaltitlelevelconfig';
|
||
import { getMajorStationList } from '/@/api/professional/rsbase/professionalmajorstation';
|
||
import { makeExportTeacherInfoByTypeTask } from '/@/api/professional/professionalfile';
|
||
import { defineAsyncComponent } from 'vue';
|
||
// 子组件
|
||
const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNameNo/index.vue'));
|
||
const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/index.vue'));
|
||
const MultiDialog = defineAsyncComponent(() => import('/@/views/professional/teacherbase/multiDialog.vue'));
|
||
const DataForm = defineAsyncComponent(() => import('./form.vue'));
|
||
const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue'));
|
||
const previewFile = defineAsyncComponent(() => import('/@/components/tools/preview-file.vue'));
|
||
const ImportTeacherOtherInfo = defineAsyncComponent(() => import('/@/views/professional/common/import-teacher-other-info.vue'));
|
||
|
||
// 无权限即无节点:使用 useAuth + v-if
|
||
const { hasAuth } = useAuth();
|
||
|
||
// 消息提示 hooks
|
||
const message = useMessage();
|
||
const messageBox = useMessageBox();
|
||
|
||
// 字典数据
|
||
const { professional_state: professionalState } = useDict('professional_state');
|
||
|
||
// 表格引用
|
||
const tableRef = ref();
|
||
const searchFormRef = ref();
|
||
const multiDialogRef = ref();
|
||
const dataFormRef = ref();
|
||
const backReasonRef = ref();
|
||
const showSearch = ref(true); // 方案 F:默认收起筛选,首屏仅一张大卡片
|
||
|
||
// 搜索表单数据
|
||
const search = reactive({
|
||
state: '',
|
||
teacherNo: '',
|
||
realName: '',
|
||
professionalTitleConfigId: '',
|
||
majorStation: '',
|
||
});
|
||
|
||
// 材料预览
|
||
const imgUrl = ref<Array<{ title: string; url: string }>>([]);
|
||
|
||
// 导出加载状态
|
||
const exportLoading = ref(false);
|
||
|
||
// 职称和专业技术职务列表
|
||
const professionalTitleList = ref<any[]>([]);
|
||
const majorStationList = ref<any[]>([]);
|
||
|
||
// 配置 useTable
|
||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||
pageList: async (params: any) => {
|
||
const response = await fetchList(params);
|
||
const records = response.data.records || [];
|
||
// 处理证明材料列表
|
||
records.forEach((v: any) => {
|
||
if (v.evidence != null) {
|
||
v.srcList = [v.evidence];
|
||
} else {
|
||
v.srcList = [];
|
||
}
|
||
});
|
||
return {
|
||
data: {
|
||
records,
|
||
total: response.data.total || 0,
|
||
},
|
||
};
|
||
},
|
||
queryForm: search,
|
||
});
|
||
|
||
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
|
||
|
||
// 预览材料
|
||
const handlePreview = (list: string[]) => {
|
||
imgUrl.value = [];
|
||
nextTick(() => {
|
||
list.forEach((v) => {
|
||
imgUrl.value.push({
|
||
title: '',
|
||
url: v,
|
||
});
|
||
});
|
||
});
|
||
};
|
||
|
||
// 审核状态变更
|
||
const changeState = (row: any, val: number) => {
|
||
if (val === 1) {
|
||
const str = '通过';
|
||
messageBox
|
||
.confirm(`是否确认${str}${row.realName}的申请`)
|
||
.then(async () => {
|
||
try {
|
||
await examObj({
|
||
id: row.id,
|
||
state: val,
|
||
});
|
||
message.success('操作成功');
|
||
getDataList();
|
||
} catch (error: any) {
|
||
// 处理业务错误
|
||
}
|
||
})
|
||
.catch(() => {});
|
||
} else if (val === -2) {
|
||
backReasonRef.value?.init(row, 'title');
|
||
}
|
||
};
|
||
|
||
// 查询
|
||
const handleFilter = () => {
|
||
getDataList();
|
||
};
|
||
|
||
// 重置查询
|
||
const resetQuery = () => {
|
||
Object.assign(search, {
|
||
state: '',
|
||
teacherNo: '',
|
||
realName: '',
|
||
professionalTitleConfigId: '',
|
||
majorStation: '',
|
||
});
|
||
getDataList();
|
||
};
|
||
|
||
// 打开新增窗口
|
||
const handleAdd = () => {
|
||
dataFormRef.value?.openDialog();
|
||
};
|
||
|
||
// 打开编辑窗口
|
||
const handleEdit = (row: any) => {
|
||
dataFormRef.value?.openDialog(row);
|
||
};
|
||
|
||
// 删除
|
||
const handleDel = (row: any) => {
|
||
messageBox
|
||
.confirm('是否确认删除该条记录')
|
||
.then(async () => {
|
||
try {
|
||
await delObj(row.id);
|
||
message.success('删除成功');
|
||
// 如果当前页只剩一条数据,且不是第一页,则跳转到上一页
|
||
if (state.pagination && state.dataList && state.dataList.length === 1 && state.pagination.current && state.pagination.current > 1) {
|
||
state.pagination.current = state.pagination.current - 1;
|
||
}
|
||
getDataList();
|
||
} catch (error: any) {
|
||
// 处理业务错误
|
||
message.error(error.msg);
|
||
}
|
||
})
|
||
.catch(() => {});
|
||
};
|
||
|
||
// 导出
|
||
const handleDownLoadWord = async () => {
|
||
exportLoading.value = true;
|
||
let params = Object.assign(search, { type: 'P20002' });
|
||
makeExportTeacherInfoByTypeTask(params).then((res: any) => {
|
||
message.success('后台下载进行中,请稍后查看任务列表');
|
||
});
|
||
setTimeout(() => {
|
||
exportLoading.value = false;
|
||
}, 3000); // 5分钟后自动关闭
|
||
};
|
||
|
||
// 获取职称名称
|
||
const getProfessionalTitleName = (id: string | number) => {
|
||
const item = professionalTitleList.value.find((item: any) => item.id === id);
|
||
return item ? item.professionalTitle : '-';
|
||
};
|
||
|
||
// 获取专业技术职务名称
|
||
const getMajorStationName = (id: string | number) => {
|
||
const item = majorStationList.value.find((item: any) => item.id === id);
|
||
return item ? item.majorStationName : '-';
|
||
};
|
||
|
||
// 加载字典数据
|
||
const loadDictData = async () => {
|
||
try {
|
||
// 加载职称列表
|
||
const titleRes = await getProfessionalTitleList();
|
||
if (titleRes && titleRes.data) {
|
||
professionalTitleList.value = titleRes.data;
|
||
}
|
||
|
||
// 加载专业技术职务列表
|
||
const majorRes = await getMajorStationList();
|
||
if (majorRes && majorRes.data) {
|
||
majorStationList.value = majorRes.data;
|
||
}
|
||
} catch (error) {
|
||
// Failed to load dict data
|
||
}
|
||
};
|
||
|
||
const importTeacherOtherInfoRef = ref();
|
||
const handleImportDialog = () => {
|
||
importTeacherOtherInfoRef.value?.init('titleRelation');
|
||
};
|
||
|
||
// 初始化:仅加载下拉字典,表格数据由 useTable 在 createdIsNeed: true 时自动请求
|
||
onMounted(async () => {
|
||
await loadDictData();
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.titlerelation-page {
|
||
padding: 12px;
|
||
min-height: 100%;
|
||
background: var(--el-bg-color-page, #f5f6f8);
|
||
}
|
||
|
||
.page-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0;
|
||
}
|
||
|
||
/* 筛选:内容区最上方,无上外边距;与下方标题栏间距用 margin-bottom */
|
||
.content-block__filter {
|
||
padding: 16px 20px 5px 20px;
|
||
margin-top: 0;
|
||
margin-bottom: 12px;
|
||
background: var(--el-fill-color-light);
|
||
border-radius: 8px;
|
||
|
||
:deep(.filter-input),
|
||
:deep(.filter-select) {
|
||
width: 180px;
|
||
}
|
||
}
|
||
|
||
/* 职称关系区域:无卡片,表头 + 表格 + 分页 */
|
||
.content-block {
|
||
background: var(--el-bg-color);
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.content-block__header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
padding-bottom: 14px;
|
||
margin-bottom: 0;
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
}
|
||
|
||
.card-title {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: var(--el-text-color-primary);
|
||
|
||
.title-icon {
|
||
font-size: 16px;
|
||
color: var(--el-color-primary);
|
||
}
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
width: 100%;
|
||
}
|
||
|
||
/* 按钮间距按规范 10px;与 RightToolbar 区隔 */
|
||
.action-group {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding-left: 12px;
|
||
// border-left: 1px solid var(--el-border-color-lighter);
|
||
}
|
||
|
||
/* 表格 */
|
||
.titlerelation-table {
|
||
:deep(.el-table__body td) {
|
||
transition: background-color 0.12s ease;
|
||
}
|
||
:deep(.el-button + .el-button) {
|
||
margin-left: 0;
|
||
}
|
||
}
|
||
|
||
/* 操作列 */
|
||
.op-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
gap: 4px 8px;
|
||
}
|
||
|
||
.empty-text {
|
||
color: var(--el-text-color-secondary, #909399);
|
||
}
|
||
|
||
/* 表格与分页间距 */
|
||
.content-block .titlerelation-table {
|
||
margin-top: 12px;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.content-block .titlerelation-table + * {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
/* 响应式 */
|
||
@media (max-width: 1200px) {
|
||
.titlerelation-page {
|
||
padding: 8px;
|
||
}
|
||
.card-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
.header-actions {
|
||
width: 100%;
|
||
justify-content: space-between;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.action-group {
|
||
flex-direction: column;
|
||
width: 100%;
|
||
.el-button {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
</style>
|