This commit is contained in:
guochunsi
2026-02-25 10:39:44 +08:00
144 changed files with 76646 additions and 21701 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
<LockScreen v-if="themeConfig.isLockScreen" />
<Settings ref="settingsRef" v-show="themeConfig.lockScreenTime > 1" />
<CloseFull v-if="!themeConfig.isLockScreen" />
<ChangeRole ref="changeRoleFirRef" title="请选择角色" :require-select-to-close="true" />
</el-config-provider>
</template>
@@ -14,16 +15,19 @@ import { useThemeConfig } from '/@/stores/themeConfig';
import other from '/@/utils/other';
import { Local, Session } from '/@/utils/storage';
import mittBus from '/@/utils/mitt';
import { needRoleSelection, isRoleDialogTriggered, setRoleDialogTriggered } from '/@/utils/roleSelect';
import setIntroduction from '/@/utils/setIconfont';
// 引入组件
const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue'));
const Settings = defineAsyncComponent(() => import('./layout/navBars/breadcrumb/settings.vue'));
const CloseFull = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue'));
const ChangeRole = defineAsyncComponent(() => import('/@/views/admin/system/role/change-role.vue'));
// 定义变量内容
const { messages, locale } = useI18n();
const settingsRef = ref();
const changeRoleFirRef = ref<{ open: () => void }>();
const route = useRoute();
const stores = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig();
@@ -44,6 +48,7 @@ const getGlobalComponentSize = computed(() => {
const getGlobalI18n = computed(() => {
return messages.value[locale.value];
});
// 设置初始化,防止刷新时恢复默认
onBeforeMount(() => {
// 设置批量第三方 icon 图标
@@ -51,10 +56,19 @@ onBeforeMount(() => {
// 设置批量第三方 js
setIntroduction.jsCdn();
});
// 页面加载时
// 角色选择弹框是否已在本轮打开过(防止事件被触发两次)
let roleDialogOpenedThisSession = false
onMounted(() => {
// 唯一入口:只通过事件打开,且只打开一次;延迟打开以等待异步组件挂载
mittBus.on('openRoleSelectDialog', () => {
if (roleDialogOpenedThisSession) return
roleDialogOpenedThisSession = true
setTimeout(() => {
changeRoleFirRef.value?.open()
}, 300)
})
nextTick(() => {
// 监听布局配'置弹窗点击打开
// 监听布局配置弹窗点击打开
mittBus.on('openSettingsDrawer', () => {
settingsRef.value.openDrawer();
});
@@ -67,11 +81,17 @@ onMounted(() => {
if (Session.get('isTagsViewCurrenFull')) {
stores.setCurrenFullscreen(Session.get('isTagsViewCurrenFull'));
}
// 与请求拦截器共用同一逻辑:先设标志再 emit由监听器统一打开监听器内会延迟 300ms 以等待异步组件挂载)
if (Session.getToken() && needRoleSelection() && !isRoleDialogTriggered()) {
setRoleDialogTriggered(true)
mittBus.emit('openRoleSelectDialog')
}
})
});
});
// 页面销毁时,关闭监听布局配置/i18n监听
// 页面销毁时,关闭监听
onUnmounted(() => {
mittBus.off('openSettingsDrawer', () => {});
mittBus.off('openRoleSelectDialog');
});
// 监听路由的变化,设置网站标题
watch(

View File

@@ -61,6 +61,54 @@ export const delObj = (ids: Object) => {
data: ids,
});
};
/**
* 批量设置角色分组
* @param roleIds 角色ID列表
* @param roleGroup 分组名称(空表示未分组)
*/
export const batchUpdateRoleGroup = (roleIds: string[], roleGroup: string) => {
return request({
url: '/admin/role/batchGroup',
method: 'put',
data: { roleIds, roleGroup: roleGroup || '' },
});
};
/**
* 批量指定角色关联用户
* @param roleId 角色ID
* @param userIds 用户ID列表
*/
export const assignUsersToRole = (roleId: string, userIds: string[]) => {
return request({
url: '/admin/role/assignUsers',
method: 'post',
data: { roleId, userIds },
});
};
/**
* 根据角色ID查询该角色下绑定的用户列表含部门、姓名、工号
* @param roleId 角色ID
*/
export const getUsersByRoleId = (roleId: string) => {
return request({
url: '/admin/role/users/' + roleId,
method: 'get',
});
};
/**
* 解除指定用户与该角色的关联
* @param roleId 角色ID
* @param userId 用户ID
*/
export const unassignUserFromRole = (roleId: string, userId: string) => {
return request({
url: `/admin/role/users/${roleId}/${userId}`,
method: 'delete',
});
};
export const permissionUpd = (roleId: string, menuIds: string) => {
return request({

View File

@@ -28,3 +28,16 @@ export const fetchList = (query?: any) => {
params: query,
});
};
/**
* 下载任务文件
* @param data 如 { id: 任务id }
*/
export const downloadTaskFile = (data?: any) => {
return request({
url: '/basic/basicAsyncTask/downloadTaskFile',
method: 'post',
data: data,
responseType: 'blob',
});
};

View File

@@ -76,13 +76,13 @@ export const delObj = (id: string | number) => {
};
/**
* 更新
* @param obj
* 更新(编辑)
* @param obj 含 id 及需修改字段,走接口文档 /edit 接口
*/
export const putObj = (obj: any) => {
return request({
url: '/basic/basicclass',
method: 'put',
url: '/basic/basicclass/edit',
method: 'post',
data: obj,
});
};

View File

@@ -18,7 +18,7 @@
import request from '/@/utils/request';
/**
* 获取树形列表
* 获取树形列表(全量,数据量大时建议用懒加载接口)
* @param params 查询参数
*/
export function getTree(params?: any) {
@@ -29,6 +29,28 @@ export function getTree(params?: any) {
});
}
/**
* 获取树根节点(懒加载用)
*/
export function getTreeRoots() {
return request({
url: '/purchase/purchasingcategory/tree/roots',
method: 'get'
});
}
/**
* 获取子节点(懒加载用)
* @param parentCode 父节点编码
*/
export function getTreeChildren(parentCode: string) {
return request({
url: '/purchase/purchasingcategory/tree/children',
method: 'get',
params: { parentCode }
});
}
/**
* 新增
* @param obj 对象数据
@@ -49,7 +71,7 @@ export function delObj(id: string | number) {
return request({
url: '/purchase/purchasingcategory/delete',
method: 'post',
data: id
data: {id:id}
});
}

View File

@@ -100,3 +100,101 @@ export function delObj(id: number) {
});
}
/**
* 获取采购申请附件列表
* @param purchaseId 采购申请ID
*/
export function getApplyFiles(purchaseId: string | number) {
return request({
url: '/purchase/purchasingfiles/applyFiles',
method: 'post',
params: { purchaseId }
});
}
/**
* 履约验收关联的合同列表(未被使用的合同)
* @param params 可选参数,如 id 等
*/
export function getContracts(params?: any) {
return request({
url: '/purchase/purchasingapply/getContracts',
method: 'get',
params
});
}
/**
* 实施采购:上传采购文件并关联到申请单(可同时保存采购代表人方式与人员)
* @param id 采购申请ID
* @param fileIds 已上传的采购文件ID列表fileType=130
* @param implementType 实施采购方式 1:自行组织采购 2:委托代理采购
* @param representorTeacherNo 需求部门初审-指定采购代表人(单人)
* @param representors 需求部门初审-部门多人逗号分隔
*/
export function implementApply(
id: number,
fileIds: string[],
implementType?: string,
representorTeacherNo?: string,
representors?: string
) {
return request({
url: '/purchase/purchasingapply/implement',
method: 'get',
params: { id, fileIds, implementType, representorTeacherNo, representors }
});
}
/**
* 发起采购文件审批流程(需已实施采购并上传采购文件)
* @param id 采购申请ID
* @param representorTeacherNo 需求部门初审-指定采购代表人单人用户ID或工号
* @param representors 需求部门初审-部门多人由系统抽取多人用户ID或工号逗号分隔
*/
export function startFileFlow(
id: number,
representorTeacherNo?: string,
representors?: string
) {
return request({
url: '/purchase/purchasingapply/startFileFlow',
method: 'post',
data: { id, representorTeacherNo, representors }
});
}
/**
* 获取部门下人员(用于选采购代表人)
*/
export function getDeptMembers() {
return request({
url: '/purchase/purchasingapply/getDeptMembers',
method: 'get'
});
}
/**
* 文件归档按文件类型打包下载该申请单下所有附件的下载地址GET 请求,浏览器直接下载 zip
* @param purchaseId 采购申请ID
*/
export function getArchiveDownloadUrl(purchaseId: string | number) {
return `/purchase/purchasingfiles/archive?purchaseId=${encodeURIComponent(String(purchaseId))}`;
}
/**
* 下载审批表:导出采购审批表 Word 文档apply.docx 模板,仅占位符替换)
* @param id 采购申请ID
*/
export function getApplyTemplateDownloadUrl(id: string | number) {
return `/purchase/purchasingapply/export-apply-template?id=${encodeURIComponent(String(id))}`;
}
/**
* 下载文件审批表:导出采购文件审批表 Word 文档fileapply.docx 模板)
* @param id 采购申请ID
*/
export function getFileApplyTemplateDownloadUrl(id: string | number) {
return `/purchase/purchasingapply/export-file-apply-template?id=${encodeURIComponent(String(id))}`;
}

View File

@@ -8,3 +8,11 @@ export const makeExportTeacherInfoBySelfTask = (data?: any) => {
data: data,
});
};
export const makeExportTeacherInfoByTypeTask = (data?: any) => {
return request({
url: '/professional/file/makeExportTeacherInfoByTypeTask',
method: 'post',
data: data,
});
};

View File

@@ -138,16 +138,3 @@ export const titleCountInfo = () => {
});
};
/**
* 导出Excel
* @param data 查询参数
*/
export const exportRelation = (data: any) => {
return request({
url: '/professional/professionaltitlerelation/exportRelation',
method: 'post',
data: data,
responseType: 'blob',
});
};

View File

@@ -67,6 +67,85 @@ export function putObj(obj?: Object) {
})
}
// ========== 履约验收流程接口 ==========
/**
* 第一步:保存履约验收公共配置,按分期次数自动生成批次
*/
export function saveCommonConfig(data: any) {
return request({
url: '/purchase/purchasingAccept/saveCommonConfig',
method: 'post',
data
})
}
/**
* 获取履约验收公共配置及批次列表
*/
export function getCommonConfigWithBatches(purchaseId: string) {
return request({
url: '/purchase/purchasingAccept/commonConfigWithBatches',
method: 'get',
params: { purchaseId }
})
}
/**
* 第二步:更新单个批次
*/
export function updateBatch(data: any) {
return request({
url: '/purchase/purchasingAccept/updateBatch',
method: 'put',
data
})
}
/**
* 获取验收详情(含验收内容、验收小组)
*/
export function getDetail(purchaseId: string, batch?: number) {
return request({
url: '/purchase/purchasingAccept/detail',
method: 'get',
params: { purchaseId, batch }
})
}
/**
* 是否允许填报方式(金额<30万
*/
export function canFillForm(purchaseId: string) {
return request({
url: '/purchase/purchasingAccept/canFillForm',
method: 'get',
params: { purchaseId }
})
}
/**
* 根据品目类型获取验收项配置
*/
export function getAcceptanceItems(acceptanceType: string) {
return request({
url: `/purchase/acceptanceItemConfig/listByType/${acceptanceType}`,
method: 'get'
})
}
/**
* 下载履约验收模板
*/
export function downloadPerformanceAcceptanceTemplate(purchaseId: string, batch?: number) {
return request({
url: '/purchase/purchasingAccept/export-performance-acceptance-template',
method: 'get',
params: { purchaseId, batch },
responseType: 'blob' // 重要:用于文件下载
})
}
// ========== 工具函数 ==========
/**

View File

@@ -42,7 +42,7 @@ export const getObj = (id: string | number) => {
*/
export const delObj = (id: string | number) => {
return request({
url: `/recruit/recruitstudentplan/deletById`,
url: `/recruit/recruitstudentplan/deleteById`,
method: 'post',
data: { id :id},
});

View File

@@ -22,6 +22,19 @@ export const getActivityInfoList = () => {
});
};
/**
* 查看详情 - 根据活动ID获取活动子项目列表
* 接口文档GET /api/stuwork/activityinfosub/getActivityInfoSubList
* @param activityInfoId 活动信息ID
*/
export const getActivityInfoSubList = (activityInfoId: string) => {
return request({
url: '/stuwork/activityinfosub/getActivityInfoSubList',
method: 'get',
params: { activityInfoId }
});
};
/**
* 删除活动子项目
* @param ids

View File

@@ -0,0 +1,14 @@
import request from '/@/utils/request';
/**
* 教室公物编辑及门锁密码
* 接口文档POST /api/stuwork/classassets/edit
* @param data 教室公物数据(包含 buildingNo, deptName, classCode, position, platformType, tyType, tvType, chairCnt, tableCnt, remarks, password 等)
*/
export const editAssets = (data: any) => {
return request({
url: '/stuwork/classassets/edit',
method: 'post',
data,
});
};

View File

@@ -103,3 +103,37 @@ export const fearchRoomStuNum = (roomNo: string) => {
});
};
/**
* 互换宿舍文档sourceSutNo / targetStuNO
*/
export const exchangeRoom = (data: { sourceSutNo: string; targetStuNO: string }) => {
return request({
url: '/stuwork/dormroomstudent/exchangeRoom',
method: 'post',
data
});
};
/**
* 打印宿舍卡(按房间号获取打印数据)
*/
export const printDormRoomData = (roomNo: string) => {
return request({
url: '/stuwork/dormroomstudent/printDormRoomData',
method: 'get',
params: { roomNo }
});
};
/**
* 空 n 人宿舍导出
*/
export const exportEmptyPeopleRoomExcel = (data?: any) => {
return request({
url: '/stuwork/dormroomstudent/exportEmptyPeopleRoomExcel',
method: 'post',
data: data || {},
responseType: 'blob'
});
};

View File

@@ -0,0 +1,35 @@
import request from '/@/utils/request';
/**
* 分页查询宿舍点名列表
* @param query
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/dormsignrecord/page',
method: 'get',
params: query
});
};
/**
* 新增宿舍点名
* @param data
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/dormsignrecord',
method: 'post',
data
});
};
/**
* 初始化宿舍学生信息用于考勤
*/
export const initDormStuInfoForAttendance = () => {
return request({
url: '/stuwork/dormsignrecord/task/initDormStuInfoForAttendance',
method: 'get'
});
};

View File

@@ -0,0 +1,85 @@
import request from '/@/utils/request';
/**
* 文件列表(分页)
* @param query
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/filemanager/page',
method: 'get',
params: query
});
};
/**
* 文件列表(不分页,按层级)
* @param query
*/
export const getList = (query?: any) => {
return request({
url: '/stuwork/filemanager/list',
method: 'get',
params: query
});
};
/**
* 文件详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/filemanager/detail',
method: 'get',
params: { id }
});
};
/**
* 新增文件
* @param data
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/filemanager',
method: 'post',
data
});
};
/**
* 编辑文件
* @param data
*/
export const editObj = (data: any) => {
return request({
url: '/stuwork/filemanager/edit',
method: 'post',
data
});
};
/**
* 编辑文件夹
* @param data
*/
export const editFile = (data: any) => {
return request({
url: '/stuwork/filemanager/editFile',
method: 'post',
data
});
};
/**
* 删除文件
* @param ids
*/
export const delObj = (ids: string[]) => {
return request({
url: '/stuwork/filemanager/delete',
method: 'post',
data: ids
});
};

View File

@@ -0,0 +1,61 @@
import request from '/@/utils/request'
/**
* 分页查询毕业学生(毕业审核列表)
* 后端返回 data.dataList.records / data.dataList.total此处归一为 data.records / data.total 供 useTable 使用
* @param query current, size, graduYear, status, type, stuNo, realName, classCode, deptCode, ...
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/stugraducheck/page',
method: 'get',
params: query
}).then((res: any) => {
const raw = res.data || {}
const dataList = raw.dataList || {}
return {
...res,
data: {
records: dataList.records || [],
total: dataList.total ?? 0,
canExamConduct: raw.canExamConduct,
canExamScore: raw.canExamScore,
canExamSkill: raw.canExamSkill,
canExamStuPunish: raw.canExamStuPunish,
canExamBaseInfo: raw.canExamBaseInfo
}
}
})
}
/**
* 生成毕业生信息
* @param data 如 { type: '0' }
*/
export const makeGraduStu = (data?: { type?: string }) => {
return request({
url: '/stuwork/stugraducheck/makeGraduStu',
method: 'post',
data: data || { type: '0' }
})
}
/**
* 获取某年毕业生列表(用于统计页前端汇总,大 size 拉取)
* @param graduYear 毕业年份
*/
export const fetchListForAnalyse = (graduYear: string | number) => {
return request({
url: '/stuwork/stugraducheck/page',
method: 'get',
params: {
graduYear,
current: 1,
size: 9999
}
}).then((res: any) => {
const raw = res.data || {}
const dataList = raw.dataList || {}
return (dataList.records || []) as any[]
})
}

View File

@@ -0,0 +1,61 @@
import request from '/@/utils/request';
/**
* 分页查询德育计划列表
* @param query
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/moralplan/page',
method: 'get',
params: query
});
};
/**
* 新增德育计划
* @param data
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/moralplan',
method: 'post',
data
});
};
/**
* 获取详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/moralplan/detail',
method: 'get',
params: { id }
});
};
/**
* 编辑德育计划
* @param data
*/
export const editObj = (data: any) => {
return request({
url: '/stuwork/moralplan/edit',
method: 'post',
data
});
};
/**
* 删除德育计划
* @param ids
*/
export const delObj = (ids: string[]) => {
return request({
url: '/stuwork/moralplan/delete',
method: 'post',
data: ids
});
};

View File

@@ -0,0 +1,74 @@
import request from '/@/utils/request';
/**
* 分页查询在线书列表
* @param query
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/onlinebooks/page',
method: 'get',
params: query
});
};
/**
* 获取在线书列表(用于下拉选择)
* @param query
*/
export const getList = (query?: any) => {
return request({
url: '/stuwork/onlinebooks/list',
method: 'get',
params: query
});
};
/**
* 新增在线书
* @param data
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/onlinebooks',
method: 'post',
data
});
};
/**
* 获取详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/onlinebooks/detail',
method: 'get',
params: { id }
});
};
/**
* 编辑在线书
* @param data
*/
export const editObj = (data: any) => {
return request({
url: '/stuwork/onlinebooks/edit',
method: 'post',
data
});
};
/**
* 删除在线书
* @param ids
*/
export const delObj = (ids: string[]) => {
return request({
url: '/stuwork/onlinebooks/delete',
method: 'post',
data: ids
});
};

View File

@@ -0,0 +1,74 @@
import request from '/@/utils/request';
/**
* 分页查询在线书浏览记录列表
* @param query
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/onlinebooksbrowsinghistory/page',
method: 'get',
params: query
});
};
/**
* 获取在线书浏览记录列表(用于下拉选择)
* @param query
*/
export const getList = (query?: any) => {
return request({
url: '/stuwork/onlinebooksbrowsinghistory/list',
method: 'get',
params: query
});
};
/**
* 新增在线书浏览记录
* @param data
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/onlinebooksbrowsinghistory',
method: 'post',
data
});
};
/**
* 获取详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/onlinebooksbrowsinghistory/detail',
method: 'get',
params: { id }
});
};
/**
* 编辑在线书浏览记录
* @param data
*/
export const editObj = (data: any) => {
return request({
url: '/stuwork/onlinebooksbrowsinghistory/edit',
method: 'post',
data
});
};
/**
* 删除在线书浏览记录
* @param ids
*/
export const delObj = (ids: string[]) => {
return request({
url: '/stuwork/onlinebooksbrowsinghistory/delete',
method: 'post',
data: ids
});
};

View File

@@ -0,0 +1,74 @@
import request from '/@/utils/request';
/**
* 分页查询在线书类别列表
* @param query
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/onlinebookscategory/page',
method: 'get',
params: query
});
};
/**
* 获取在线书类别列表(用于下拉选择)
* @param query
*/
export const getList = (query?: any) => {
return request({
url: '/stuwork/onlinebookscategory/list',
method: 'get',
params: query
});
};
/**
* 新增在线书类别
* @param data
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/onlinebookscategory',
method: 'post',
data
});
};
/**
* 获取详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/onlinebookscategory/detail',
method: 'get',
params: { id }
});
};
/**
* 编辑在线书类别
* @param data
*/
export const editObj = (data: any) => {
return request({
url: '/stuwork/onlinebookscategory/edit',
method: 'post',
data
});
};
/**
* 删除在线书类别
* @param ids
*/
export const delObj = (ids: string[]) => {
return request({
url: '/stuwork/onlinebookscategory/delete',
method: 'post',
data: ids
});
};

View File

@@ -0,0 +1,73 @@
import request from '/@/utils/request'
/**
* 按月份返回值班表(列表)
* @param params year, month
*/
export const listByMonth = (params: { year: string | number; month: string | number }) => {
return request({
url: '/stuwork/psychologicalcounselingduty/listByMonth',
method: 'get',
params
})
}
/**
* 后台获取某年某月值班表,回显到日历/列表
* @param params year, month
*/
export const getDutyByMonth = (params: { year: string | number; month: string | number }) => {
return request({
url: '/stuwork/psychologicalcounselingduty/getDutyByMonth',
method: 'get',
params
})
}
/**
* 通过 id 查询值班详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/psychologicalcounselingduty/detail',
method: 'get',
params: { id }
})
}
/**
* 新增/批量新增值班
* @param list 每项 { date: 'YYYY-MM-DD', teacherUserName: '工号', weekType?: 'single'|'double' }
*/
export const saveDuty = (list: Array<{ date: string; teacherUserName: string; weekType?: string }>) => {
return request({
url: '/stuwork/psychologicalcounselingduty/saveDuty',
method: 'post',
data: list
})
}
/**
* 一键清空某月值班
* @param data { year, month }
*/
export const clearDuty = (data: { year: number; month: number }) => {
return request({
url: '/stuwork/psychologicalcounselingduty/clearDuty',
method: 'post',
data
})
}
/**
* 清除单个值班(按日期)
* @param data { days: 'YYYY-MM-DD' }
*/
export const clearOneDuty = (data: { days: string }) => {
return request({
url: '/stuwork/psychologicalcounselingduty/clearOneDuty',
method: 'post',
data
})
}

View File

@@ -0,0 +1,61 @@
import request from '/@/utils/request'
/**
* 分页查询预约记录
* @param query current, size, stuNo, classNo, reservationTime, isHandle
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/psychologicalcounselingreservation/page',
method: 'get',
params: query
})
}
/**
* 通过 id 查询预约记录详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/psychologicalcounselingreservation/detail',
method: 'get',
params: { id }
})
}
/**
* 新增预约记录
* @param data teacherNo, reservationTime, classNo, stuNo, stuName, phone, remarks, realName?, isHandle?
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/psychologicalcounselingreservation',
method: 'post',
data
})
}
/**
* 修改预约记录
* @param data 含 id 及需修改字段
*/
export const editObj = (data: any) => {
return request({
url: '/stuwork/psychologicalcounselingreservation/edit',
method: 'post',
data
})
}
/**
* 通过 id 删除预约记录
* @param ids id 数组
*/
export const delObj = (ids: string[]) => {
return request({
url: '/stuwork/psychologicalcounselingreservation/delete',
method: 'post',
data: ids
})
}

View File

@@ -0,0 +1,71 @@
import request from '/@/utils/request'
/**
* 分页查询心理咨询预约师
* @param query current, size, realName
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/psychologicalcounselingteacher/page',
method: 'get',
params: query
})
}
/**
* 获取预约师列表(不分页)
*/
export const getList = () => {
return request({
url: '/stuwork/psychologicalcounselingteacher/list',
method: 'get'
})
}
/**
* 通过 id 查询详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/psychologicalcounselingteacher/detail',
method: 'get',
params: { id }
})
}
/**
* 新增心理咨询预约师
* @param data userName, realName, phone, remarks
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/psychologicalcounselingteacher',
method: 'post',
data
})
}
/**
* 修改心理咨询预约师
* @param data id, userName, realName, phone, remarks
*/
export const editObj = (data: any) => {
return request({
url: '/stuwork/psychologicalcounselingteacher/edit',
method: 'post',
data
})
}
/**
* 通过 id 删除心理咨询预约师
* @param ids id 数组
*/
export const delObj = (ids: string[]) => {
return request({
url: '/stuwork/psychologicalcounselingteacher/delete',
method: 'post',
data: ids
})
}

View File

@@ -36,6 +36,18 @@ export const getDetail = (id: string) => {
});
};
/**
* 通过学年学号查看详情接口文档GET /stuwork/stuconduct/queryDataByStuNo
* @param params stuNo 学号, schoolYear 学年
*/
export const queryDataByStuNo = (params: { stuNo: string; schoolYear: string }) => {
return request({
url: '/stuwork/stuconduct/queryDataByStuNo',
method: 'get',
params
});
};
/**
* 编辑操行考核
* @param data

View File

@@ -0,0 +1,25 @@
import request from '/@/utils/request'
/**
* 毕业学生名单 - 分页查询
* 接口文档GET /api/stuwork/stugraducheck/page
* 参数current, size, graduYear, status, type, stuNo, realName, classCode, deptCode 等
* 返回归一为 data.records / data.total 供 useTable 使用
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/stugraducheck/page',
method: 'get',
params: query
}).then((res: any) => {
const raw = res.data || {}
const dataList = raw.dataList || {}
return {
...res,
data: {
records: dataList.records || [],
total: dataList.total ?? 0
}
}
})
}

View File

@@ -60,6 +60,18 @@ export const delObj = (ids: string[]) => {
});
};
/**
* 撤销学籍异动
* @param ids 异动记录ID列表
*/
export const cancelObj = (ids: string[]) => {
return request({
url: '/stuwork/stuturnover/cancel',
method: 'post',
data: ids
});
};
/**
* 导出学籍异动
* @param query

View File

@@ -12,3 +12,29 @@ export const getClassRoomByClassCode = (classCode: string | number) => {
});
};
/**
* 教室安排
* 接口文档POST /api/stuwork/teachclassroomassign/addClassRoomAssign
* @param data buildingNo 楼号, position 位置, classCode 班级代码
*/
export const addClassRoomAssign = (data: { buildingNo?: string | number; position?: string; classCode?: string }) => {
return request({
url: '/stuwork/teachclassroomassign/addClassRoomAssign',
method: 'post',
data,
});
};
/**
* 取消教室安排
* 接口文档POST /api/stuwork/teachclassroomassign/delClassRoomAssign
* @param data 教室基础数据(包含 id, classCode, position 等)
*/
export const delClassRoomAssign = (data: any) => {
return request({
url: '/stuwork/teachclassroomassign/delClassRoomAssign',
method: 'post',
data,
});
};

View File

@@ -0,0 +1,61 @@
import request from '/@/utils/request';
/**
* 分页查询学期活动列表
* @param query
*/
export const fetchList = (query?: any) => {
return request({
url: '/stuwork/termactivity/page',
method: 'get',
params: query
});
};
/**
* 获取学期活动详情
* @param id
*/
export const getDetail = (id: string) => {
return request({
url: '/stuwork/termactivity/detail',
method: 'get',
params: { id }
});
};
/**
* 新增学期活动
* @param data
*/
export const addObj = (data: any) => {
return request({
url: '/stuwork/termactivity',
method: 'post',
data
});
};
/**
* 编辑学期活动
* @param data
*/
export const editObj = (data: any) => {
return request({
url: '/stuwork/termactivity/edit',
method: 'post',
data
});
};
/**
* 删除学期活动
* @param ids
*/
export const delObj = (ids: string[]) => {
return request({
url: '/stuwork/termactivity/delete',
method: 'post',
data: ids
});
};

View File

@@ -162,6 +162,8 @@ let saveDialog = () => {
id: item.id,
name: item.name,
avatar: item.avatar,
username: item.username,
userName: item.username,
}));
emits('change', checkedList);
//selectedList.value=[]

View File

@@ -5,7 +5,7 @@
<div v-if="props.disabled">
<div v-if="fileList.length === 0" class="flex justify-center items-center px-4 text-gray-400 bg-gray-50 rounded-md p">
<el-icon class="mr-2 text-lg"><Document /></el-icon>
<span class="text-sm">{{ $t('excel.noFiles') }}</span>
<span class="text-sm"></span>
</div>
<div v-else>
<div
@@ -139,6 +139,13 @@ const baseUrl = import.meta.env.VITE_API_URL || '';
// 获取文件名
const getFileName = (file: any): string => {
// 优先使用 fileTitle其次使用 name最后从 URL 中提取
if (file.fileTitle) {
return file.fileTitle;
}
if (file.name) {
return file.name;
}
return file.url ? other.getQueryString(file.url, 'fileName') || other.getQueryString(file.url, 'originalFileName') : 'File';
};
@@ -349,23 +356,23 @@ function handleUploadSuccess(res: any, file: any) {
}
}
// 上传结束处理
// 上传结束处理:传出完整 fileList含 name便于父组件回显文件名
const uploadedSuccessfully = () => {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
uploadList.value = [];
number.value = 0;
emit('update:modelValue', listToString(fileList.value));
emit('update:modelValue', fileList.value);
emit('change', listToString(fileList.value), fileList.value);
}
};
const handleRemove = (file: { name?: string }) => {
if (file.name) {
fileList.value = fileList.value.filter((f) => f.name !== file.name);
emit('update:modelValue', listToString(fileList.value));
const handleRemove = (file: { name?: string; id?: string; url?: string }) => {
fileList.value = fileList.value.filter(
(f) => !(f.id && f.id === file.id) && !(f.name && f.name === file.name)
);
emit('update:modelValue', fileList.value.length ? fileList.value : '');
emit('change', listToString(fileList.value), fileList.value);
}
};
const handlePreview = (file: any) => {

Binary file not shown.

View File

@@ -529,11 +529,11 @@
if (elTabs) {
let find = elTabs.find(f => f.isSave !== true);
// 这里测试流程,临时屏蔽判断 todo
// if (find) useMessage().info(find.formName + ' 未保存')
// else {
if (find) useMessage().info(find.formName + ' 未保存')
else {
methods.timeoutLoading()
btnMethods.onHandleJob(jobBtn)
// }
}
return
}
methods.timeoutLoading()

View File

@@ -17,7 +17,8 @@ import SignInput from "./sign/index.vue";
// vite glob导入
const modules: Record<string, () => Promise<unknown>> = import.meta.glob(
['../../views/jsonflow/*/*.vue', '../../views/order/*/*.vue',
'../../views/purchase/*/*.vue','../../views/finance/purchasingrequisition/add.vue']
'../../views/purchase/*/*.vue', '../../views/finance/purchasingrequisition/add.vue',
'../../views/finance/purchasingrequisition/implement.vue']
)
/**

View File

@@ -156,8 +156,10 @@ export function useTable(options?: BasicTableProps) {
if (state.onLoaded) await state.onLoaded(state);
if (state.onCascaded) await state.onCascaded(state);
} catch (err: any) {
// 捕获异常并显示错误提示
ElMessage.error(err.msg || err.data.msg);
// 全局拦截器已展示过错误时不再重复弹窗
if (!err?._messageShown) {
ElMessage.error(err?.msg || err?.data?.msg || '请求失败');
}
} finally {
// 结束加载数据设置state.loading为false
state.loading = false;

View File

@@ -155,6 +155,10 @@ export function useTableColumnControl(
* 根据 visibleColumns 和 columnOrder 计算最终显示的列
*/
const visibleColumnsSorted = computed(() => {
// 如果 visibleColumns 为空,显示所有列(初始化时)
if (visibleColumns.value.length === 0) {
return tableColumns.filter(col => !col.alwaysShow && !col.fixed)
}
// 过滤出可见的列
const columns = tableColumns.filter(col => {
const key = col.prop || col.label || ''

View File

@@ -28,7 +28,12 @@
>
<el-table-column label="所属模块" prop="moduleName" width="120" show-overflow-tooltip />
<el-table-column label="任务类型" prop="typeLabel" width="100" show-overflow-tooltip />
<el-table-column label="任务名称" prop="detailType" min-width="150" show-overflow-tooltip />
<el-table-column label="任务名称" prop="detailType" min-width="150" show-overflow-tooltip >
<template #default="{ row }">
<el-button v-if="row.type==2" type="text" icon="Download" class="task-name-text" :loading="downloadingId === row.id" @click="handleDownloadFile(row)">{{row.detailType}}</el-button>
<span v-else class="task-name-text">{{row.detailType}}</span>
</template>
</el-table-column>
<el-table-column label="任务状态" align="center" show-overflow-tooltip>
<template #default="{ row }">
<el-tag>{{ row.status }}</el-tag>
@@ -58,7 +63,8 @@
<script setup lang="ts">
import { ref, reactive, watch, computed } from 'vue'
import { fetchList } from '/@/api/basic/basicasynctask'
import { fetchList, downloadTaskFile } from '/@/api/basic/basicasynctask'
import { useMessage } from '/@/hooks/message'
type TaskTab = 'upload' | 'download' | 'other'
@@ -87,6 +93,7 @@ const tableStyle = {
}
const emptyText = computed(() => EMPTY_TEXT_MAP[activeTab.value])
const message = useMessage()
const loadList = async () => {
const type = activeTab.value
@@ -135,6 +142,34 @@ const open = () => {
visible.value = true
}
const downloadingId = ref<string | number | null>(null)
const handleDownloadFile = async (row: any) => {
if (!row?.id) return
downloadingId.value = row.id
try {
const response: any = await downloadTaskFile({ id: row.id })
const blob = (response && response.data instanceof Blob)
? response.data
: (response instanceof Blob ? response : new Blob([response]))
const dateStr = new Date().toISOString().slice(0, 10)
const baseName = row.detailType ? String(row.detailType).replace(/\s+/g, '_') : '下载文件'
const fileName = `${baseName}_${dateStr}.xls`
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
message.success('下载成功')
} catch {
message.error('下载失败')
} finally {
downloadingId.value = null
}
}
defineExpose({ open })
</script>

View File

@@ -253,15 +253,7 @@ const getIsDot = () => {
});
};
// 登录后若存储中无角色信息则弹出角色切换框
const openChangeRoleIfMissing = () => {
const hasRole = Local.get('roleCode') && Local.get('roleName') && Local.get('roleId')
if (!hasRole) {
nextTick(() => {
setTimeout(() => ChangeRoleRef.value?.open(), 100)
})
}
}
// 首次登录缺角色时由 App.vue + 请求拦截器统一弹出「首次登录请选择角色」弹框,此处不再自动打开
// 页面加载时
onMounted(() => {
@@ -271,7 +263,6 @@ onMounted(() => {
}
useFlowJob().topJobList()
getIsDot()
openChangeRoleIfMissing()
});
</script>

View File

@@ -41,11 +41,18 @@ export async function initBackEndControlRoutes() {
await useUserInfo().setUserInfos();
// 获取路由菜单数据
const res = await getBackEndControlRoutes();
// 无登录权限时,添加判断
const menuList = res.data || [];
// 无登录权限时仍走后续流程,用 dynamicRoutes 作为子路由并写入 store避免 routesList 一直为空导致 beforeEach 无限请求
// https://gitee.com/lyt-top/vue-next-admin/issues/I64HVO
if ((res.data || []).length <= 0) return Promise.resolve(true);
if (menuList.length <= 0) {
useRequestOldRoutes().setRequestOldRoutes([]);
baseRoutes[0].children = [...dynamicRoutes, ...(await backEndComponent([]) || [])];
await setAddRoute();
await setFilterMenuAndCacheTagsViewRoutes();
return Promise.resolve(true);
}
// 存储接口原始路由未处理component根据需求选择使用
useRequestOldRoutes().setRequestOldRoutes(JSON.parse(JSON.stringify(res.data)));
useRequestOldRoutes().setRequestOldRoutes(JSON.parse(JSON.stringify(menuList)));
// 处理路由component替换 baseRoutes/@/router/route第一个顶级 children 的路由
baseRoutes[0].children = [...dynamicRoutes, ...(await backEndComponent(res.data))];
// 添加动态路由

View File

@@ -107,6 +107,14 @@ export const staticRoutes: Array<RouteRecordRaw> = [
isAuth: false, // 不需要认证,纯页面展示
},
},
{
path: '/finance/purchasingrequisition/implement',
name: 'purchasingrequisition.implement',
component: () => import('/@/views/finance/purchasingrequisition/implement.vue'),
meta: {
isAuth: false, // 供流程 iframe 嵌入
},
},
...staticRoutesFlow
];

1
src/types/mitt.d.ts vendored
View File

@@ -23,6 +23,7 @@ declare type MittType<T = any> = {
openShareTagsView?: string;
onTagsViewRefreshRouterView?: T;
onCurrentContextmenuClick?: T;
openRoleSelectDialog?: string;
};
// mitt 参数类型定义

View File

@@ -5,6 +5,8 @@ import qs from 'qs';
import other from './other';
import { paramsFilter } from "/@/flow";
import { wrapEncryption, encryptRequestParams, decrypt } from './apiCrypto';
import mittBus from '/@/utils/mitt';
import { needRoleSelection, isRoleDialogTriggered, setRoleDialogTriggered } from '/@/utils/roleSelect';
// 常用header
export enum CommonHeaderEnum {
@@ -81,6 +83,12 @@ service.interceptors.request.use(
// 自动适配单体和微服务架构不同的URL
config.url = other.adaptationUrl(config.url);
// 发送请求时判断:已登录但缺少角色信息则弹出角色选择;若弹框已触发则不再重复弹出
if (token && needRoleSelection() && !isRoleDialogTriggered()) {
setRoleDialogTriggered(true);
mittBus.emit('openRoleSelectDialog');
}
// 处理完毕返回config对象
return config;
},
@@ -97,9 +105,10 @@ service.interceptors.request.use(
*/
const handleResponse = (response: AxiosResponse<any>) => {
if (response.data.code === 1) {
// 业务错误,统一弹出错误提示
// 业务错误,统一弹出错误提示(标记已展示,避免 hook/页面 catch 再次弹窗)
if (response.data.msg) {
useMessage().error(response.data.msg);
response.data._messageShown = true;
}
throw response.data;
}

24
src/utils/roleSelect.ts Normal file
View File

@@ -0,0 +1,24 @@
import { Local } from '/@/utils/storage';
/** Local 是否缺少角色信息(缺任一则需弹出角色选择) */
export function needRoleSelection(): boolean {
try {
const roleCode = Local.get('roleCode');
const roleName = Local.get('roleName');
const roleId = Local.get('roleId');
return !roleCode || !roleName || !roleId;
} catch {
return true;
}
}
/** 角色选择弹框是否已触发(防止多请求同时触发时重复弹出) */
let roleDialogTriggered = false;
export function isRoleDialogTriggered(): boolean {
return roleDialogTriggered;
}
export function setRoleDialogTriggered(value: boolean): void {
roleDialogTriggered = value;
}

View File

@@ -1,56 +1,79 @@
<template>
<el-dialog
v-model="visible"
title="角色切换"
:title="dialogTitle"
width="50%"
:show-close="false"
:close-on-click-modal="false"
:close-on-press-escape="false"
:before-close="handleBeforeClose"
center
>
<el-form>
<!-- <el-form-item label="学校">-->
<!-- <el-tag>{{schoolName}}</el-tag>-->
<!-- </el-form-item>-->
<el-form-item label="角色" class="role-form-item">
<el-form-item class="role-form-item">
<el-radio-group v-model="radio" class="role-radio-group" @change="handleChangeRole">
<template v-for="(roles, groupName) in allRoleGroups" :key="groupName">
<div class="role-group">
<el-divider>{{ groupName }}</el-divider>
<el-radio-button
v-for="item in allRole"
v-for="item in roles"
:key="item.roleCode"
:label="item.roleCode"
>
{{ item.roleName }}
</el-radio-button>
</div>
</template>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<!-- <el-button type="primary" @click="handleChangeRole">切换</el-button>-->
<template v-if="!requireSelectToClose" #footer>
<el-button @click="handleFooterClose"> </el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, toRef } from 'vue'
import { listAllRole } from '/@/api/admin/role'
import { Local } from '/@/utils/storage'
import { useMessage } from '/@/hooks/message'
/** 弹框标题,如「角色切换」「登录角色选择」 */
const props = withDefaults(
defineProps<{ title?: string; requireSelectToClose?: boolean }>(),
{ title: '角色切换', requireSelectToClose: false }
)
const dialogTitle = computed(() => props.title)
const visible = ref(false)
const radio = ref('')
const allRole = reactive<any[]>([])
/** 按分组名分组的角色列表:{ "未分组": [{ roleId, roleName, roleCode, ... }], ... } */
const allRoleGroups = ref<Record<string, any[]>>({})
const requireSelectToClose = toRef(props, 'requireSelectToClose')
const open = () => {
if (visible.value) return
visible.value = true
listAllRole().then((res) => {
Object.assign(allRole, res.data)
allRoleGroups.value = res.data && typeof res.data === 'object' && !Array.isArray(res.data)
? res.data
: { '未分组': Array.isArray(res.data) ? res.data : [] }
radio.value = Local.get('roleCode')
visible.value = true
})
}
/** 根据 roleCode 从分组数据中查找角色 */
const findRoleByCode = (code: string) => {
for (const roles of Object.values(allRoleGroups.value)) {
if (!Array.isArray(roles)) continue
const found = roles.find((r: any) => r.roleCode === code)
if (found) return found
}
return null
}
const canClose = () => {
if (!radio.value) {
useMessage().warning('请选择一个角色')
@@ -60,6 +83,10 @@ const canClose = () => {
}
const handleBeforeClose = (done: () => void) => {
if (requireSelectToClose.value) {
useMessage().warning('请先选择登录角色')
return
}
if (!canClose()) return
done()
}
@@ -70,7 +97,7 @@ const handleFooterClose = () => {
}
const handleChangeRole = (label: string) => {
const obj = allRole.find((v: any) => v.roleCode === label)
const obj = findRoleByCode(label)
if (!obj) return
Local.set('roleCode', obj.roleCode)
Local.set('roleName', obj.roleName)
@@ -94,6 +121,18 @@ defineExpose({
flex-wrap: wrap;
}
}
.role-group {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
.group-name {
font-size: 13px;
color: var(--el-text-color-secondary);
margin-bottom: 6px;
}
.role-radio-group {
display: flex;
flex-wrap: wrap;

View File

@@ -1,59 +0,0 @@
<template>
<el-dialog v-model="visible" width="50%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false" center>
<template #title>
<div style="margin: 0 auto;width:100%;text-align:center;font-size:18px;font-weight:bold;">
登录角色选择
</div>
</template>
<div style="margin: 0 auto;width:100%;text-align:center;font-size:18px;font-weight:bold;">
<el-radio-group v-model="radio">
<el-radio-button v-for="(item,index) in allRole" :key="index" :label="item.roleCode" @click.native="handleChangeRole(item.roleCode)">{{item.roleName}}</el-radio-button>
</el-radio-group>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import {listAllRole} from '/@/api/admin/role'
import {Local, Session} from '/@/utils/storage';
import {useMessage} from "/@/hooks/message";
// import {querySchoolName} from "/@/api/admin/tenant"
const visible=ref(false)
const radio=ref('')
const allRole=reactive([])
const schoolName=ref('')
const open=()=>{
// handleQuerySchoolName()
listAllRole().then(res=>{
Object.assign(allRole,res.data)
radio.value=Local.get("roleCode")
visible.value=true
})
}
const handleChangeRole=(label:any)=>{
let obj:any=allRole.find((v:any) => v.roleCode == label)
Local.set("roleCode",obj.roleCode)
Local.set("roleName",obj.roleName)
Local.set("roleId",obj.roleId)
useMessage().success("操作成功")
setTimeout(()=>{
window.location.reload()
},500)
}
// const handleQuerySchoolName=()=>{
// querySchoolName({id:Session.get("tenantId")}).then((res:any)=>{
// schoolName.value=res.data
// })
// }
defineExpose({
open
})
</script>
<style scoped>
</style>

View File

@@ -7,6 +7,12 @@
<el-form-item :label="$t('sysrole.roleCode')" prop="roleCode">
<el-input :placeholder="$t('sysrole.please_enter_the_role_Code')" :disabled="form.roleId !== ''" clearable v-model="form.roleCode"></el-input>
</el-form-item>
<el-form-item label="分组" prop="roleGroup">
<el-input placeholder="用于列表树状分组展示,可留空" clearable v-model="form.roleGroup" maxlength="50" show-word-limit />
</el-form-item>
<el-form-item label="排序" prop="roleSort">
<el-input-number v-model="form.roleSort" :min="0" :max="9999" placeholder="数值越小越靠前" controls-position="right" style="width: 140px" />
</el-form-item>
<el-form-item :label="$t('sysrole.roleDesc')" prop="roleDesc">
<el-input
maxlength="100"
@@ -67,6 +73,8 @@ const form = reactive({
roleId: '',
roleName: '',
roleCode: '',
roleGroup: '',
roleSort: 0,
roleDesc: '',
dsType: 0,
dsScope: '',
@@ -187,6 +195,7 @@ const getRoleData = (id: string) => {
// 获取部门数据
getObj(id).then((res: any) => {
Object.assign(form, res.data);
if (res.data.roleSort == null) form.roleSort = 0;
if (res.data.dsScope) {
dataForm.checkedDsScope = res.data.dsScope.split(',');
} else {

View File

@@ -25,6 +25,12 @@
<el-button plain :disabled="multiple" icon="Delete" type="primary" class="ml10" v-auth="'sys_user_del'" @click="handleDelete(selectObjs)">
{{ $t('common.delBtn') }}
</el-button>
<el-button plain :disabled="multiple" type="primary" class="ml10" v-auth="'sys_role_edit'" @click="showBatchGroupDialog = true">
批量指定分组
</el-button>
<el-button plain :disabled="selectObjs.length !== 1" type="primary" class="ml10" v-auth="'sys_role_edit'" @click="openAssignUserDialog">
批量指定关联用户
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'sys_role_export'"
@@ -36,36 +42,59 @@
</div>
</el-row>
<el-table
:data="state.dataList"
:data="roleTreeData"
v-loading="state.loading"
style="width: 100%"
row-key="roleId"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
@selection-change="handleSelectionChange"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="selection" :selectable="handleSelectable" width="50" align="center" />
<el-table-column type="index" :label="$t('sysrole.index')" width="80" />
<el-table-column prop="roleName" :label="$t('sysrole.roleName')" show-overflow-tooltip></el-table-column>
<el-table-column prop="roleCode" :label="$t('sysrole.roleCode')" show-overflow-tooltip></el-table-column>
<el-table-column prop="roleDesc" :label="$t('sysrole.roleDesc')" show-overflow-tooltip></el-table-column>
<el-table-column prop="data_authority" :label="$t('sysrole.data_authority')" show-overflow-tooltip>
<el-table-column type="selection" :selectable="handleSelectable" width="50" align="left" />
<el-table-column type="index" :label="$t('sysrole.index')" width="80">
<template #default="scope">
<dict-tag :options="dictType" :value="scope.row.dsType"></dict-tag>
<span v-if="scope.row._isGroup"></span>
<span v-else>{{ scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" :label="$t('sysrole.createTime')" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('common.action')" width="250">
<el-table-column prop="roleName" :label="$t('sysrole.roleName')" show-overflow-tooltip min-width="140" align="left">
<template #default="scope">
<span v-if="scope.row._isGroup" class="role-group-name">{{ scope.row.roleName }}</span>
<span v-else>{{ scope.row.roleName }}</span>
</template>
</el-table-column>
<el-table-column prop="roleSort" label="排序" width="80" align="center">
<template #default="scope">{{ scope.row._isGroup ? '—' : (scope.row.roleSort ?? 0) }}</template>
</el-table-column>
<el-table-column prop="roleCode" :label="$t('sysrole.roleCode')" show-overflow-tooltip min-width="120">
<template #default="scope">{{ scope.row._isGroup ? '—' : scope.row.roleCode }}</template>
</el-table-column>
<el-table-column prop="roleDesc" :label="$t('sysrole.roleDesc')" show-overflow-tooltip min-width="140">
<template #default="scope">{{ scope.row._isGroup ? '—' : scope.row.roleDesc }}</template>
</el-table-column>
<el-table-column prop="data_authority" :label="$t('sysrole.data_authority')" show-overflow-tooltip width="100">
<template #default="scope">
<template v-if="scope.row._isGroup"></template>
<dict-tag v-else :options="dictType" :value="scope.row.dsType"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" :label="$t('sysrole.createTime')" show-overflow-tooltip width="165">
<template #default="scope">{{ scope.row._isGroup ? '—' : scope.row.createTime }}</template>
</el-table-column>
<el-table-column :label="$t('common.action')" width="250" fixed="right">
<template #default="scope">
<template v-if="scope.row._isGroup"></template>
<template v-else>
<el-button text type="primary" icon="edit-pen" v-auth="'sys_role_edit'" @click="roleDialogRef.openDialog(scope.row.roleId)">{{
$t('common.editBtn')
}}</el-button>
<el-button text type="primary" icon="turn-off" v-auth="'sys_role_perm'" @click="permessionRef.openDialog(scope.row)">{{
$t('sysrole.permissionTip')
}}</el-button>
<el-button text type="primary" icon="user" v-auth="'sys_role_view'" @click="openRoleUsersDialog(scope.row)">查看关联用户</el-button>
<el-tooltip :content="$t('sysrole.deleteDisabledTip')" :disabled="scope.row.roleId !== '1'" placement="top">
<span style="margin-left: 12px">
<el-button
@@ -80,11 +109,98 @@
</span>
</el-tooltip>
</template>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
<!-- 批量指定分组弹窗 -->
<el-dialog v-model="showBatchGroupDialog" title="批量指定分组" width="400px" @close="batchGroupName = ''">
<el-form label-width="80px">
<el-form-item label="分组名称">
<el-input v-model="batchGroupName" placeholder="输入分组名,留空为未分组" clearable maxlength="50" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showBatchGroupDialog = false">取消</el-button>
<el-button type="primary" :loading="batchGroupLoading" @click="handleBatchGroup">确定</el-button>
</template>
</el-dialog>
<!-- 批量指定关联用户弹窗 -->
<el-dialog
v-model="showAssignUserDialog"
title="批量指定关联用户"
width="720px"
destroy-on-close
@close="assignUserKeyword = ''; assignUserType = ''; assignUserList = []; assignUserTree = []; assignSelectedIds = []">
<template v-if="assignCurrentRole">
<div class="mb12"><el-text type="info">当前角色{{ assignCurrentRole.roleName }}{{ assignCurrentRole.roleCode }}</el-text></div>
<el-form :inline="true" class="mb12">
<el-form-item label="用户类型" required>
<el-radio-group v-model="assignUserType">
<el-radio label="1">教职工</el-radio>
<el-radio label="2">学生</el-radio>
<el-radio label="3">驻校单位</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="姓名/工号">
<el-input v-model="assignUserKeyword" placeholder="姓名或工号检索" clearable style="width: 180px" @keyup.enter="loadAssignUserList" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="assignUserLoading" @click="loadAssignUserList">查询</el-button>
</el-form-item>
</el-form>
<div class="assign-user-tip mb8" v-if="!assignUserType">请先选择用户类型后再查询</div>
<el-table
v-else
:data="assignUserTree"
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
default-expand-all
v-loading="assignUserLoading"
max-height="360"
border
@selection-change="handleAssignUserSelectionChange">
<el-table-column type="selection" width="50" align="left" :selectable="(row: any) => !row._isDept" />
<el-table-column prop="label" label="部门 / 姓名" min-width="200">
<template #default="{ row }">
<span v-if="row._isDept" class="dept-row">{{ row.label }}</span>
<span v-else>{{ row.realName || row.username || row.label }}</span>
</template>
</el-table-column>
<el-table-column prop="username" label="工号" width="120">
<template #default="{ row }">{{ row._isDept ? '—' : row.username }}</template>
</el-table-column>
<el-table-column prop="deptName" label="部门" width="140">
<template #default="{ row }">{{ row._isDept ? '—' : (row.deptName || '—') }}</template>
</el-table-column>
</el-table>
</template>
<template #footer>
<el-button @click="showAssignUserDialog = false">取消</el-button>
<el-button type="primary" :loading="assignSubmitLoading" :disabled="assignSelectedIds.length === 0" @click="handleAssignUsersSubmit">确定已选 {{ assignSelectedIds.length }} </el-button>
</template>
</el-dialog>
<!-- 查看角色关联用户弹窗 -->
<el-dialog v-model="showRoleUsersDialog" title="关联用户" width="560px" destroy-on-close>
<template v-if="currentRoleForUsers">
<div class="mb12"><el-text type="info">角色{{ currentRoleForUsers.roleName }}{{ currentRoleForUsers.roleCode }}</el-text></div>
<el-table :data="roleUsersList" v-loading="roleUsersLoading" max-height="400" border>
<el-table-column prop="deptName" label="部门" min-width="140" show-overflow-tooltip />
<el-table-column prop="realName" label="姓名" width="100" show-overflow-tooltip />
<el-table-column prop="username" label="工号" width="120" show-overflow-tooltip />
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button text type="danger" icon="delete" v-auth="'sys_role_edit'" @click="handleUnassignUser(row)">解除</el-button>
</template>
</el-table-column>
</el-table>
<div class="mt8" v-if="!roleUsersLoading && roleUsersList.length === 0"><el-text type="info">该角色下暂无关联用户</el-text></div>
</template>
</el-dialog>
<!-- 角色编辑新增 -->
<role-dialog ref="roleDialogRef" @refresh="getDataList()" />
<!-- 导入角色 -->
@@ -101,8 +217,10 @@
</template>
<script setup lang="ts" name="systemRole">
import { computed, reactive, ref } from 'vue';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { pageList, delObj } from '/@/api/admin/role';
import { list, delObj, batchUpdateRoleGroup, assignUsersToRole, getUsersByRoleId, unassignUserFromRole } from '/@/api/admin/role';
import { pageList as userPageList } from '/@/api/admin/user';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
@@ -121,15 +239,103 @@ const showSearch = ref(true);
const selectObjs = ref([]) as any;
// 是否可以多选
const multiple = ref(true);
// 批量指定分组
const showBatchGroupDialog = ref(false);
const batchGroupName = ref('');
const batchGroupLoading = ref(false);
// 查看角色关联用户
const showRoleUsersDialog = ref(false);
const roleUsersList = ref<any[]>([]);
const roleUsersLoading = ref(false);
const currentRoleForUsers = ref<{ roleName: string; roleCode: string; roleId: string } | null>(null);
// 批量指定关联用户
const showAssignUserDialog = ref(false);
const assignUserType = ref('');
const assignUserKeyword = ref('');
const assignUserList = ref<any[]>([]);
const assignUserLoading = ref(false);
const assignSubmitLoading = ref(false);
const assignSelectedIds = ref<string[]>([]);
const assignCurrentRole = computed(() => {
const id = selectObjs.value && selectObjs.value[0];
if (!id) return null;
return (state.dataList || []).find((r: any) => r.roleId === id) || null;
});
/** 按部门分组的用户树(用于表格树形展示) */
const assignUserTree = computed(() => {
const list = assignUserList.value || [];
const map = new Map<string, any[]>();
list.forEach((u: any) => {
const deptKey = u.deptName && String(u.deptName).trim() ? u.deptName : '未分配部门';
if (!map.has(deptKey)) map.set(deptKey, []);
map.get(deptKey)!.push({ ...u, id: u.userId, _isDept: false });
});
const result: any[] = [];
map.forEach((users, deptName) => {
result.push({
id: `dept_${deptName}`,
label: deptName,
_isDept: true,
children: users,
});
});
result.sort((a, b) => a.label.localeCompare(b.label));
return result;
});
// 列表不分页,显示全部数据
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
roleName: '',
},
pageList: pageList, // H
isPage: false,
pageList: async (params: any) => {
const res = await list(params);
let data = res?.data || [];
if (Array.isArray(data) && params?.roleName) {
const kw = String(params.roleName).trim().toLowerCase();
if (kw) {
data = data.filter(
(r: any) =>
(r.roleName && r.roleName.toLowerCase().includes(kw)) ||
(r.roleCode && r.roleCode.toLowerCase().includes(kw))
);
}
}
return { data };
},
descs: ['create_time'],
});
/** 按分组构建树形数据:分组为父节点,角色为子节点 */
const roleTreeData = computed(() => {
const list = state.dataList || [];
const groupMap = new Map<string, any[]>();
list.forEach((row: any) => {
const groupName = row.roleGroup && String(row.roleGroup).trim() ? String(row.roleGroup).trim() : '未分组';
if (!groupMap.has(groupName)) groupMap.set(groupName, []);
groupMap.get(groupName)!.push(row);
});
const result: any[] = [];
groupMap.forEach((roles, groupName) => {
result.push({
roleId: `group_${groupName}`,
roleName: groupName,
_isGroup: true,
children: roles,
});
});
// 未分组放最后,其余按分组名排序
result.sort((a, b) => {
if (a.roleName === '未分组') return 1;
if (b.roleName === '未分组') return -1;
return a.roleName.localeCompare(b.roleName);
});
return result;
});
const dictType = ref([
{
label: '全部',
@@ -153,8 +359,8 @@ const dictType = ref([
},
]);
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// table hook(无分页,不暴露 currentChangeHandle/sizeChangeHandle
const { getDataList, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
@@ -167,15 +373,15 @@ const exportExcel = () => {
downBlobFile('/admin/role/export',Object.assign(state.queryForm,{ids:selectObjs}), 'role.xlsx');
};
// 是否可以多选
// 是否可以多选(分组行不可选,管理员角色不可选)
const handleSelectable = (row: any) => {
return row.roleId !== '1';
return !row._isGroup && row.roleId !== '1';
};
// 多选事件
// 多选事件(仅角色行可选,排除分组行)
const handleSelectionChange = (objs: { roleId: string }[]) => {
selectObjs.value = objs.map(({ roleId }) => roleId);
multiple.value = !objs.length;
selectObjs.value = objs.map(({ roleId }) => roleId).filter((id: string) => !String(id).startsWith('group_'));
multiple.value = !selectObjs.value.length;
};
// 删除操作
@@ -194,4 +400,129 @@ const handleDelete = async (ids: string[]) => {
useMessage().error(err.msg);
}
};
async function loadRoleUsersList() {
const role = currentRoleForUsers.value;
if (!role) return;
roleUsersLoading.value = true;
try {
const res = await getUsersByRoleId(role.roleId);
const data = res?.data;
roleUsersList.value = Array.isArray(data) ? data : [];
} catch {
roleUsersList.value = [];
} finally {
roleUsersLoading.value = false;
}
}
async function openRoleUsersDialog(row: any) {
currentRoleForUsers.value = { roleName: row.roleName, roleCode: row.roleCode, roleId: row.roleId };
showRoleUsersDialog.value = true;
roleUsersList.value = [];
await loadRoleUsersList();
}
async function handleUnassignUser(row: any) {
const roleId = currentRoleForUsers.value?.roleId;
const userId = row.userId;
if (!roleId || !userId) return;
try {
await useMessageBox().confirm(`确定解除「${row.realName || row.username}」与该角色的关联?`);
} catch {
return;
}
try {
await unassignUserFromRole(roleId, userId);
useMessage().success('已解除关联');
await loadRoleUsersList();
} catch (err: any) {
useMessage().error(err?.msg || '操作失败');
}
}
function openAssignUserDialog() {
if (selectObjs.value.length !== 1) return;
showAssignUserDialog.value = true;
assignUserType.value = '';
assignUserKeyword.value = '';
assignUserList.value = [];
assignSelectedIds.value = [];
}
async function loadAssignUserList() {
if (!assignUserType.value) return;
assignUserLoading.value = true;
try {
const res = await userPageList({
current: 1,
size: 2000,
userType: assignUserType.value,
realName: assignUserKeyword.value ? assignUserKeyword.value.trim() : undefined,
});
const records = res?.data?.records ?? res?.records ?? (Array.isArray(res?.data) ? res.data : []);
assignUserList.value = Array.isArray(records) ? records : [];
} catch (e) {
assignUserList.value = [];
} finally {
assignUserLoading.value = false;
}
}
function handleAssignUserSelectionChange(rows: any[]) {
assignSelectedIds.value = (rows || [])
.filter((r: any) => !r._isDept && r.userId)
.map((r: any) => r.userId);
}
async function handleAssignUsersSubmit() {
const roleId = assignCurrentRole.value?.roleId;
if (!roleId || !assignSelectedIds.value.length) {
useMessage().warning('请选择要关联的用户');
return;
}
assignSubmitLoading.value = true;
try {
await assignUsersToRole(roleId, assignSelectedIds.value);
useMessage().success('已关联 ' + assignSelectedIds.value.length + ' 名用户');
showAssignUserDialog.value = false;
} catch (err: any) {
useMessage().error(err?.msg || '操作失败');
} finally {
assignSubmitLoading.value = false;
}
}
// 批量指定分组
const handleBatchGroup = async () => {
const ids = selectObjs.value || [];
if (!ids.length) {
useMessage().warning('请先勾选要设置分组的角色');
return;
}
batchGroupLoading.value = true;
try {
await batchUpdateRoleGroup(ids, batchGroupName.value || '');
useMessage().success('分组已更新');
showBatchGroupDialog.value = false;
batchGroupName.value = '';
getDataList();
} catch (err: any) {
useMessage().error(err?.msg || '操作失败');
} finally {
batchGroupLoading.value = false;
}
};
</script>
<style scoped lang="scss">
.role-group-name {
font-weight: 600;
color: var(--el-text-color-primary);
}
.mb12 { margin-bottom: 12px; }
.mb8 { margin-bottom: 8px; }
.mt8 { margin-top: 8px; }
.assign-user-tip { color: var(--el-text-color-secondary); font-size: 13px; }
.dept-row { font-weight: 600; color: var(--el-text-color-primary); }
</style>

View File

@@ -204,7 +204,8 @@ const dataRules = ref({
{ required: true, message: '学院不能为空', trigger: 'change' }
],
classCode: [
{ required: true, message: '班级代码不能为空', trigger: 'blur' }
{ required: true, message: '班级代码不能为空', trigger: 'blur' },
{ min: 4, message: '班级代码至少4位班号取后4位', trigger: 'blur' }
],
classNo: [
{ required: true, message: '班号不能为空', trigger: 'blur' }
@@ -406,11 +407,11 @@ watch(() => form.enterDate, (newVal) => {
}
})
// 监听班级代码,取后四位为班号
// 监听班级代码,取后四位为班号班级代码至少4位
watch(() => form.classCode, (newVal) => {
if (newVal) {
const length = newVal.length
if (length > 4) {
if (length >= 4) {
// 只在新增模式下自动填充班号,编辑模式下如果班号为空才填充
if (!form.id || !form.classNo) {
form.classNo = newVal.substring(length - 4, length)

View File

@@ -196,7 +196,7 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="200">
<el-table-column label="操作" align="center" fixed="right" width="260">
<template #default="scope">
<el-button
icon="View"

View File

@@ -262,13 +262,25 @@
<template #default="scope" v-if="col.prop === 'gender'">
<GenderTag :gender="scope.row.gender" />
</template>
<template #default="scope" v-else-if="col.prop === 'education'">
<el-tag v-if="getEducationLabel(scope.row.education)" size="small" type="info" effect="plain">
{{ getEducationLabel(scope.row.education) }}
</el-tag>
<span v-else>-</span>
</template>
<template #default="scope" v-else-if="col.prop === 'isDorm'">
<el-tag v-if="scope.row.isDorm === 1 || scope.row.isDorm === '1'" size="small" type="success" effect="plain"></el-tag>
<el-tag v-else-if="scope.row.isDorm === 0 || scope.row.isDorm === '0'" size="small" type="info" effect="plain"></el-tag>
<span v-else>-</span>
</template>
<template #default="scope" v-else-if="col.prop === 'enrollStatus'">
<el-tag v-if="scope.row.enrollStatus" size="small" type="info" effect="plain">{{ scope.row.enrollStatus }}</el-tag>
<el-tag
v-if="getEnrollStatusLabel(scope.row.enrollStatus)"
size="small"
type="info"
effect="plain">
{{ getEnrollStatusLabel(scope.row.enrollStatus) }}
</el-tag>
<span v-else>-</span>
</template>
<template #default="scope" v-else-if="col.prop === 'stuStatus'">
@@ -427,7 +439,6 @@ import {
editIsleader,
updateInout,
updateStuSimpleInfo,
getStuStatus,
prePrint
} from "/@/api/basic/basicstudent";
import { getDeptList, getClassListByRole } from "/@/api/basic/basicclass";
@@ -456,6 +467,7 @@ const deptList = ref<any[]>([])
const classList = ref<any[]>([])
const statusList = ref<any[]>([])
const stuStatusList = ref<any[]>([])
const educationList = ref<any[]>([])
const selectedRows = ref<any[]>([])
const importCertificateDialogVisible = ref(false)
const uploadLoading = ref(false)
@@ -781,9 +793,17 @@ const getClassListData = async () => {
// 获取学籍状态列表
const getStatusListData = async () => {
try {
const res = await getStuStatus()
const res = await getDicts('enroll_status')
if (res.data) {
statusList.value = Array.isArray(res.data) ? res.data : []
if (Array.isArray(res.data)) {
// 确保数据格式统一为 {label, value}
statusList.value = res.data.map((item: any) => ({
label: item.label || item.name || item.dictLabel || item.text || '',
value: String(item.value || item.code || item.dictValue || item.id || '')
})).filter((item: any) => item.label && item.value !== undefined && item.value !== null && item.value !== '')
} else {
statusList.value = []
}
}
} catch (err) {
console.error('获取学籍状态列表失败', err)
@@ -791,6 +811,24 @@ const getStatusListData = async () => {
}
}
// 获取文化程度字典
const getEducationListData = async () => {
try {
const res = await getDicts('pre_school_education')
if (res.data && Array.isArray(res.data)) {
educationList.value = res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
}))
} else {
educationList.value = []
}
} catch (err) {
console.error('获取文化程度字典失败', err)
educationList.value = []
}
}
// 获取学生状态列表
const getStuStatusListData = async () => {
try {
@@ -813,6 +851,20 @@ const getStuStatusListData = async () => {
}
}
// 根据学籍状态值获取标签
const getEnrollStatusLabel = (value: any) => {
if (value === undefined || value === null || value === '') return ''
const status = statusList.value.find(item => String(item.value) === String(value))
return status ? status.label : ''
}
// 根据文化程度值获取标签
const getEducationLabel = (value: any) => {
if (value === undefined || value === null || value === '') return ''
const item = educationList.value.find(i => String(i.value) === String(value))
return item ? item.label : ''
}
// 根据学生状态值获取标签
const getStuStatusLabel = (value: any) => {
if (value === undefined || value === null || value === '') return ''
@@ -833,6 +885,7 @@ onMounted(() => {
getDeptListData()
getClassListData()
getStatusListData()
getEducationListData()
getStuStatusListData()
})
</script>

View File

@@ -35,6 +35,14 @@
招标代理管理
</span>
<div class="header-actions">
<el-button
icon="Files"
link
type="primary"
>
代理汇总
</el-button>
<el-button
icon="FolderAdd"
type="primary"
@@ -170,7 +178,7 @@ const handleDelete = async (row: any) => {
}
try {
await delObj(row.id);
await delObj({"id":row.id});
useMessage().success('删除成功');
getDataList();
} catch (err: any) {

View File

@@ -21,12 +21,14 @@
</div>
</template>
<!-- 树形表格 -->
<!-- 树形表格懒加载仅首屏加载根节点展开时再加载子节点 -->
<el-table
ref="tableRef"
:data="state.dataList"
v-loading="state.loading"
stripe
lazy
:load="loadTreeNode"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table"
@@ -88,7 +90,7 @@
<script setup lang="ts" name="PurchasingCategory">
import { ref, reactive, defineAsyncComponent } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { getTree, delObj } from "/@/api/finance/purchasingcategory";
import { getTreeRoots, getTreeChildren, delObj } from "/@/api/finance/purchasingcategory";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { List, Document, DocumentCopy, EditPen } from '@element-plus/icons-vue'
@@ -100,19 +102,40 @@ const tableRef = ref()
const formDialogRef = ref()
/**
* 查询树形数据方法
* @param params - 查询参数
* @returns Promise<any>
* 查询树根节点(懒加载:首屏只加载根节点)
*/
const queryTree = (params?: any) => {
return getTree(params);
const queryTreeRoots = () => {
return getTreeRoots().then((res: any) => {
const list = res?.data ?? [];
return { data: Array.isArray(list) ? list : [] };
});
};
/**
* 懒加载子节点:展开某行时按需请求子节点
* @param row 当前行
* @param treeNode 树节点信息
* @param resolve 回调,传入子节点数组
*/
const loadTreeNode = (row: any, treeNode: any, resolve: (data: any[]) => void) => {
const parentCode = row?.code;
if (!parentCode) {
resolve([]);
return;
}
getTreeChildren(parentCode)
.then((res: any) => {
const list = res?.data ?? [];
resolve(Array.isArray(list) ? list : []);
})
.catch(() => resolve([]));
};
/**
* 定义响应式表格数据
*/
const state: BasicTableProps = reactive<BasicTableProps>({
pageList: queryTree,
pageList: queryTreeRoots,
queryForm: {},
isPage: false, // 树形表格不分页
});

View File

@@ -0,0 +1,317 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="160px" >
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item label="验收方式" prop="acceptType">
<el-radio-group v-model="form.acceptType" :disabled="readonly">
<el-radio label="1" :disabled="!canFill">填写履约验收评价表</el-radio>
<el-radio label="2">上传履约验收评价表</el-radio>
</el-radio-group>
<div v-if="!canFill" class="el-form-item__tip">金额30万仅支持上传模版</div>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="验收日期" prop="acceptDate">
<el-date-picker
v-model="form.acceptDate"
type="date"
placeholder="请选择"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
:disabled="readonly"
/>
</el-form-item>
</el-col>
<!-- 填报方式验收内容表格 -->
<template v-if="form.acceptType === '1' && canFill">
<el-col :span="12" class="mb20">
<el-form-item label="验收内容" prop="acceptContents">
<el-table :data="form.acceptContents" border size="small" class="accept-content-table">
<el-table-column prop="itemName" label="验收项" >
<template #default="{ row }">
<div>
<span>{{row.itemName}}</span>
<el-input
v-if="row.type === 'input'"
v-model="row.remark"
placeholder="请输入"
size="small"
:disabled="readonly"
/>
</div>
</template>
</el-table-column>
<el-table-column prop="isQualified" label="合格/不合格" align="center">
<template #default="{ row }">
<el-radio-group v-model="row.isQualified" size="small" :disabled="readonly">
<el-radio label="1">合格</el-radio>
<el-radio label="0">不合格</el-radio>
</el-radio-group>
</template>
</el-table-column>
</el-table>
</el-form-item>
</el-col>
</template>
<!-- 上传方式 -->
<template v-if="form.acceptType === '2'">
<el-col :span="12">
<el-form-item label="履约验收模版" prop="templateFileIds">
<UploadFile
v-model="templateFileIdsStr"
:limit="1"
:data="{ purchaseId: purchaseId || '', fileType: '110' }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="readonly"
/>
</el-form-item>
</el-col>
</template>
<el-col :span="12" class="mb20">
<el-form-item label="验收地点" prop="acceptAddress">
<el-input v-model="form.acceptAddress" placeholder="请输入验收地点" :disabled="readonly" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="存在问题及改进意见" prop="question">
<el-input
v-model="form.question"
type="textarea"
:rows="2"
placeholder="请输入存在问题及改进意见"
:disabled="readonly"
/>
</el-form-item>
</el-col>
<!-- 验收小组 -->
<el-col :span="12">
<el-form-item label="验收小组" prop="acceptTeam">
<div class="team-list">
<div v-for="(m, idx) in form.acceptTeam" :key="idx" class="team-row">
<el-select
v-model="m.roleType"
placeholder="身份"
size="small"
style="width: 130px; margin-right: 8px"
:disabled="readonly"
@change="(val) => onRoleChange(idx, val as string)"
>
<el-option label="组长(校内)" value="LEADER_IN" />
<el-option label="组长(校外)" value="LEADER_OUT" />
<el-option label="组员(校内)" value="MEMBER_IN" />
<el-option label="组员(校外)" value="MEMBER_OUT" />
</el-select>
<template v-if="m.roleType === 'LEADER_IN' || m.roleType === 'MEMBER_IN'">
<org-selector
v-model:orgList="m.userList"
type="user"
:multiple="false"
@update:orgList="(list: any[]) => onTeamUserChange(idx, list)"
/>
</template>
<template v-else>
<el-input v-model="m.name" placeholder="姓名" size="small" style="width:120px" :disabled="readonly" />
<el-input v-model="m.deptName" placeholder="单位/部门" size="small" style="width:160px" :disabled="readonly" />
</template>
<el-button v-if="!readonly && form.acceptTeam.length > 3" type="danger" link size="small" @click="removeTeam(idx)">删除</el-button>
</div>
<el-button v-if="!readonly" type="primary" link size="small" @click="addTeam">+ 增加成员</el-button>
</div>
<div class="el-form-item__tip">
至少3人且为单数
<template v-if="(previousBatchesTeams || []).length > 0">
<span class="copy-from-inline">
从往期带入
<el-select
v-model="copyFromBatch"
placeholder="同第N期"
clearable
size="small"
style="width: 100px"
@change="onCopyFromBatch"
>
<el-option
v-for="item in (previousBatchesTeams || [])"
:key="item.batch"
:label="`同第${item.batch}期`"
:value="item.batch"
/>
</el-select>
</span>
</template>
</div>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入" :disabled="readonly" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
const props = withDefaults(
defineProps<{
modelValue: Record<string, any>
canFill: boolean
readonly?: boolean
purchaseId?: string
acceptanceItems?: any[]
batchNum?: number
previousBatchesTeams?: { batch: number; team: any[] }[]
}>(),
{ readonly: false, canFill: true, purchaseId: '', batchNum: 1, previousBatchesTeams: () => [] }
)
const emit = defineEmits(['update:modelValue'])
const formRef = ref<FormInstance>()
const templateFileIdsStr = ref('')
const copyFromBatch = ref<number | null>(null)
const form = reactive({
acceptType: '1',
acceptDate: '',
acceptContents: [] as any[],
acceptTeam: [
{ name: '', deptCode: '', deptName: '', roleType: '' },
{ name: '', deptCode: '', deptName: '', roleType: '' },
{ name: '', deptCode: '', deptName: '', roleType: '' },
] as any[],
templateFileIds: [] as string[],
acceptAddress: '',
question: '',
remark: '',
...props.modelValue,
})
watch(() => props.modelValue, (val) => {
Object.assign(form, val || {})
// 金额≥30万时强制为上传方式
if (!props.canFill && form.acceptType === '1') {
form.acceptType = '2'
}
}, { deep: true })
watch(form, () => emit('update:modelValue', { ...form }), { deep: true })
// 金额≥30万时默认选中上传方式
watch(() => props.canFill, (val) => {
if (!val && form.acceptType === '1') {
form.acceptType = '2'
}
}, { immediate: true })
watch(() => props.acceptanceItems, (items) => {
if (items?.length && form.acceptContents.length === 0) {
form.acceptContents = items.map((it: any) => ({
configId: it.id,
itemName: it.itemName,
type: it.type,
isQualified: '1',
remark: '',
}))
}
}, { immediate: true })
watch(templateFileIdsStr, (s) => {
const arr = s ? s.split(',').map((x: string) => x.trim()).filter(Boolean) : []
if (JSON.stringify(form.templateFileIds) !== JSON.stringify(arr)) {
form.templateFileIds = arr
}
})
watch(() => form.templateFileIds, (arr) => {
if (Array.isArray(arr) && arr.length) templateFileIdsStr.value = arr.join(',')
}, { immediate: true, deep: true })
const addTeam = () => {
form.acceptTeam.push({ name: '', deptCode: '', deptName: '', roleType: '' })
}
const removeTeam = (idx: number) => {
form.acceptTeam.splice(idx, 1)
}
const onCopyFromBatch = (n: number | null) => {
if (!n) return
const item = props.previousBatchesTeams?.find((x) => x.batch === n)
if (item?.team?.length) {
form.acceptTeam = item.team.map((m: any) => ({
name: m.name || '',
deptCode: m.deptCode || '',
deptName: m.deptName || '',
roleType: m.roleType || '',
}))
}
copyFromBatch.value = null
}
const rules: FormRules = {
acceptType: [{ required: true, message: '请选择验收方式', trigger: 'change' }],
acceptDate: [{ required: true, message: '请选择验收日期', trigger: 'change' }],
}
const onRoleChange = (idx: number, val: string) => {
const isLeader = val === 'LEADER_IN' || val === 'LEADER_OUT'
if (isLeader) {
const hasOtherLeader = form.acceptTeam.some((m, i) =>
i !== idx && (m.roleType === 'LEADER_IN' || m.roleType === 'LEADER_OUT')
)
if (hasOtherLeader) {
// 只能有一个组长
form.acceptTeam[idx].roleType = ''
return
}
}
}
const onTeamUserChange = (idx: number, list: any[]) => {
const m = form.acceptTeam[idx]
if (!m) return
if (list && list.length) {
const u = list[0]
m.name = u.name || u.realName || ''
m.deptCode = u.deptCode || u.commonDeptCode || ''
m.deptName = u.deptName || u.commonDeptName || ''
m.userList = list
} else {
m.name = ''
m.deptCode = ''
m.deptName = ''
m.userList = []
}
}
const validate = () => formRef.value?.validate()
defineExpose({ validate, form })
</script>
<style scoped>
.mb20 {
margin-bottom: 20px;
}
.copy-from-inline {
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: 4px;
}
.copy-from-inline :deep(.el-select) {
vertical-align: middle;
}
</style>

View File

@@ -0,0 +1,231 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
<el-row :gutter="24">
<el-col :span="8" class="mb20">
<el-form-item label="项目名称">
<el-input :model-value="projectName || form.projectName" readonly placeholder="-" />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="需求部门">
<el-input :model-value="deptName || form.deptName" readonly placeholder="-" />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="是否签订合同" prop="hasContract">
<el-radio-group v-model="form.hasContract">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20" v-if="form.hasContract === '1'">
<el-form-item label="合同" prop="contractId">
<el-select
v-model="form.contractId"
placeholder="请选择合同"
clearable
filterable
style="width: 100%"
:loading="contractLoading"
@visible-change="onContractSelectVisibleChange"
>
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="item.contractName || item.contractNo || item.id"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="是否分期验收" prop="isInstallment">
<el-radio-group v-model="form.isInstallment">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20" v-if="form.isInstallment === '1'">
<el-form-item label="分期次数" prop="totalPhases">
<el-input-number v-model="form.totalPhases" :min="1" :max="99" placeholder="请输入" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="供应商名称" prop="supplierName">
<el-input v-model="form.supplierName" placeholder="选择合同后自动带出" clearable />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="供应商联系人及电话" prop="supplierContact">
<el-input v-model="form.supplierContact" placeholder="请输入" clearable />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="采购人员" prop="purchaserId">
<org-selector v-model:orgList="purchaserList" type="user" :multiple="false" @update:orgList="onPurchaserChange" />
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="资产管理员" prop="assetAdminId">
<org-selector v-model:orgList="assetAdminList" type="user" :multiple="false" @update:orgList="onAssetAdminChange" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive, watch, onMounted } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { getContracts } from '/@/api/finance/purchasingrequisition'
const props = defineProps<{
modelValue: Record<string, any>
projectName?: string
deptName?: string
/** 采购申请ID用于拉取合同列表 */
purchaseId?: string | number
/** 每次打开弹窗时变化,用于强制重置内部 form */
resetKey?: number
}>()
const emit = defineEmits(['update:modelValue'])
const formRef = ref<FormInstance>()
const contractOptions = ref<any[]>([])
const contractLoading = ref(false)
const contractLoaded = ref(false)
const purchaserList = ref<any[]>([])
const assetAdminList = ref<any[]>([])
const form = reactive({
hasContract: '0',
contractId: '',
isInstallment: '0',
totalPhases: 1,
projectName: '',
deptName: '',
supplierName: '',
supplierContact: '',
purchaserId: '',
purchaserName: '',
assetAdminId: '',
assetAdminName: '',
...props.modelValue,
})
const syncFormFromModel = (val: Record<string, any> | undefined) => {
Object.assign(form, val || {})
// 同步采购人员、资产管理员回 org-selector
if (form.purchaserId && form.purchaserName) {
purchaserList.value = [{ id: form.purchaserId, name: form.purchaserName, type: 'user' }]
} else {
purchaserList.value = []
}
if (form.assetAdminId && form.assetAdminName) {
assetAdminList.value = [{ id: form.assetAdminId, name: form.assetAdminName, type: 'user' }]
} else {
assetAdminList.value = []
}
}
const loadContractOptions = async () => {
if (contractLoaded.value || contractLoading.value) return
contractLoading.value = true
try {
const res = await getContracts(props.purchaseId ? { id: props.purchaseId } : {})
const list = res?.data
contractOptions.value = Array.isArray(list) ? list : []
contractLoaded.value = true
} catch (_) {
contractOptions.value = []
} finally {
contractLoading.value = false
}
}
const onContractSelectVisibleChange = (visible: boolean) => {
if (visible && form.hasContract === '1' && contractOptions.value.length === 0) {
loadContractOptions()
}
}
watch(() => props.modelValue, syncFormFromModel, { deep: true, immediate: true })
// resetKey 变化时强制用 modelValue 覆盖内部 form并重置合同列表以便重新拉取
watch(() => props.resetKey, () => {
syncFormFromModel(props.modelValue)
contractLoaded.value = false
contractOptions.value = []
})
watch(form, () => emit('update:modelValue', { ...form }), { deep: true })
watch(() => form.hasContract, (val) => {
if (val === '1') {
contractLoaded.value = false
loadContractOptions()
} else {
contractOptions.value = []
contractLoaded.value = false
}
})
// 选择合同后,自动带出合同供应商名称
watch(
() => form.contractId,
(val) => {
if (!val) {
form.supplierName = ''
return
}
const c = contractOptions.value.find((it: any) => it.id === val)
if (c && c.supplierName) {
form.supplierName = c.supplierName
}
}
)
onMounted(() => {
if (form.hasContract === '1') {
loadContractOptions()
}
})
const onPurchaserChange = (list: any[]) => {
if (list?.length) {
const u = list[0]
form.purchaserId = u.userId || u.id || ''
form.purchaserName = u.name || u.realName || ''
} else {
form.purchaserId = ''
form.purchaserName = ''
}
}
const onAssetAdminChange = (list: any[]) => {
if (list?.length) {
const u = list[0]
form.assetAdminId = u.userId || u.id || ''
form.assetAdminName = u.name || u.realName || ''
} else {
form.assetAdminId = ''
form.assetAdminName = ''
}
}
const rules: FormRules = {
isInstallment: [{ required: true, message: '请选择是否分期验收', trigger: 'change' }],
totalPhases: [{ required: true, message: '请输入分期次数', trigger: 'blur' }],
}
const validate = () => formRef.value?.validate()
defineExpose({ validate, form })
</script>
<style scoped>
.mb20 {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,575 @@
<template>
<el-dialog
v-model="visible"
title="履约验收"
width="75%"
:close-on-click-modal="false"
destroy-on-close
class="purchasing-accept-modal"
@close="handleClose"
>
<div v-loading="loading" class="modal-body" :key="String(purchaseId)">
<div class="main-tabs">
<div class="main-tab-nav">
<div
class="main-tab-item"
:class="{ active: mainTab === 'common' }"
@click="mainTab = 'common'"
>
公共信息
</div>
<div
class="main-tab-item"
:class="{ active: mainTab === 'batch' }"
@click="mainTab = 'batch'"
>
{{ commonForm?.isInstallment === '0' ? '验收' : '分期验收' }}{{ commonForm?.isInstallment !== '0' && batches.length > 0 ? ` (${batches.length})` : '' }}
</div>
</div>
<div class="main-tab-content">
<div v-show="mainTab === 'common'" class="tab-content">
<AcceptCommonForm
:key="`${purchaseId}-${openToken}`"
:reset-key="openToken"
ref="commonFormRef"
v-model="commonForm"
:purchase-id="purchaseId"
:project-name="applyInfo?.projectName"
:dept-name="applyInfo?.deptName"
/>
</div>
<div v-show="mainTab === 'batch'" class="tab-content">
<div v-if="batches.length > 0">
<div v-show="commonForm?.isInstallment !== '0'" class="batch-tabs">
<div
v-for="b in batches"
:key="b.id"
class="batch-tab-item"
:class="{ active: String(b.batch) === activeTab, disabled: !canEditBatch(b.batch) }"
@click="canEditBatch(b.batch) && (activeTab = String(b.batch))"
>
<span>{{ b.batch }}</span>
<el-tag v-if="isBatchCompleted(b)" type="success" size="small">已填</el-tag>
<el-tag v-else-if="!canEditBatch(b.batch)" type="info" size="small">需先完成上一期</el-tag>
</div>
</div>
<div class="batch-panel">
<AcceptBatchForm
v-for="b in batches"
v-show="String(b.batch) === activeTab"
:key="b.id"
:ref="(el) => setBatchFormRef(b.batch, el)"
v-model="batchForms[b.batch]"
:can-fill="canFillForm"
:readonly="false"
:purchase-id="String(purchaseId)"
:acceptance-items="acceptanceItems"
:batch-num="b.batch"
:previous-batches-teams="getPreviousBatchesTeams(b.batch)"
/>
</div>
</div>
<div v-else class="tip-box">
<el-alert type="info" :closable="false" show-icon>
请先在公共信息中填写并点击保存公共配置系统将按分期次数自动生成验收批次
</el-alert>
</div>
</div>
</div>
</div>
</div>
<template #footer>
<span>
<el-button @click="handleClose"> </el-button>
<!-- 下载履约验收模板按钮 -->
<el-dropdown split-button type="primary" @click="handleDownloadTemplate" @command="handleDownloadTemplateCommand">
下载履约模板
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="current" :disabled="!activeBatchId">
下载当前期({{ activeTab }})
</el-dropdown-item>
<el-dropdown-item command="all" :disabled="batches.length === 0">
下载全部期数
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button
v-if="mainTab === 'common' || batches.length === 0"
type="primary"
@click="saveCommonConfig"
:loading="saving"
>
保存公共配置
</el-button>
<el-button
v-else-if="mainTab === 'batch' && activeBatchId"
type="primary"
@click="saveCurrentBatch"
:loading="saving"
>
保存第{{ activeTab }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, nextTick } from 'vue'
import { useMessage } from '/@/hooks/message'
import { handleBlobFile } from '/@/utils/other'
import {
saveCommonConfig as apiSaveCommonConfig,
getCommonConfigWithBatches,
updateBatch,
canFillForm as apiCanFillForm,
getAcceptanceItems,
getDetail,
downloadPerformanceAcceptanceTemplate,
} from '/@/api/purchase/purchasingAccept'
import AcceptCommonForm from './AcceptCommonForm.vue'
import AcceptBatchForm from './AcceptBatchForm.vue'
const emit = defineEmits(['refresh'])
const visible = ref(false)
const loading = ref(false)
const saving = ref(false)
const purchaseId = ref<string | number>('')
const applyInfo = ref<any>(null)
const rowProjectType = ref<string>('A')
const canFillForm = ref(true)
const acceptanceItems = ref<any[]>([])
const batches = ref<any[]>([])
const mainTab = ref('common')
const activeTab = ref('1')
const commonFormRef = ref()
const batchFormRefMap = ref<Record<number, any>>({})
/** 使用 ref 并在每次打开时替换整个对象,确保子组件能感知引用变化并清空 */
const commonForm = ref<Record<string, any>>({})
/** 每次打开自增,用于强制 AcceptCommonForm 重新挂载,确保公共信息彻底清空 */
const openToken = ref(0)
const batchForms = reactive<Record<number, any>>({})
/** 记录哪些期已保存到服务器,用于控制“下一期可填”:只有上一期已保存才允许填下一期 */
const batchSavedFlags = ref<Record<number, boolean>>({})
const setBatchFormRef = (batch: number, el: any) => {
if (el) batchFormRefMap.value[batch] = el
}
const activeBatchId = computed(() => {
const b = batches.value.find((x: any) => String(x.batch) === activeTab.value)
return b?.id || ''
})
const getPreviousBatchesTeams = (batchNum: number) => {
const list: { batch: number; team: any[] }[] = []
for (let n = 1; n < batchNum; n++) {
const team = batchForms[n]?.acceptTeam
if (Array.isArray(team) && team.length > 0) {
list.push({ batch: n, team })
}
}
return list
}
/** 是否允许编辑该期:第 1 期始终可编辑;第 N 期仅当第 1N-1 期均已保存后才可编辑 */
const canEditBatch = (batch: number) => {
if (batch === 1) return true
for (let i = 1; i < batch; i++) {
if (!batchSavedFlags.value[i]) return false
}
return true
}
/** 该期是否已保存(用于 tab 上显示“已填”标签) */
const isBatchCompleted = (b: any) => {
return !!batchSavedFlags.value[b.batch]
}
const isBatchCompletedByIdx = (batch: number) => {
return !!batchSavedFlags.value[batch]
}
const loadData = async () => {
if (!purchaseId.value) return
const currentId = String(purchaseId.value)
loading.value = true
try {
const [configRes, canFillRes] = await Promise.all([
getCommonConfigWithBatches(currentId),
apiCanFillForm(currentId),
])
// 防止快速切换:若已打开其他申请单,忽略本次结果
if (String(purchaseId.value) !== currentId) return
const config = configRes?.data
canFillForm.value = !!canFillRes?.data
if (config?.common) {
applyInfo.value = config.common
// 仅当存在已保存批次时,才用接口数据回填公共信息;否则保持 open() 中的默认清空值
if (config?.batches?.length) {
Object.assign(commonForm.value, {
hasContract: config.common.hasContract || '0',
contractId: config.common.contractId || '',
isInstallment: config.common.isInstallment || '0',
totalPhases: config.common.totalPhases || 1,
supplierName: config.common.supplierName || '',
supplierContact: config.common.supplierContact || '',
})
}
}
const projectType = applyInfo.value?.projectType || rowProjectType.value || 'A'
const typeMap: Record<string, string> = { A: 'A', B: 'B', C: 'C' }
const at = typeMap[projectType] || 'A'
const itemsRes = await getAcceptanceItems(at)
if (String(purchaseId.value) !== currentId) return
acceptanceItems.value = itemsRes?.data || []
if (config?.batches?.length) {
batches.value = config.batches.sort((a: any, b: any) => (a.batch || 0) - (b.batch || 0))
activeTab.value = String(batches.value[0]?.batch || '1')
mainTab.value = 'batch'
for (const b of batches.value) {
if (!batchForms[b.batch]) batchForms[b.batch] = {}
}
await loadBatchDetails()
if (String(purchaseId.value) !== currentId) return
} else {
batches.value = []
}
} catch (e: any) {
useMessage().error(e?.msg || '加载失败')
} finally {
loading.value = false
}
}
const loadBatchDetails = async () => {
for (const b of batches.value) {
batchSavedFlags.value[b.batch] = false
}
for (const b of batches.value) {
try {
const res = await getDetail(String(purchaseId.value), b.batch)
const d = res?.data
if (d?.accept) {
// 仅当该期在服务端有验收日期(且验收方式)时才视为已保存,避免空结构被当成“已填”
const hasSaved = !!(d.accept.acceptDate && d.accept.acceptType)
batchSavedFlags.value[b.batch] = hasSaved
const itemMap = (acceptanceItems.value || []).reduce((acc: any, it: any) => {
acc[it.id] = it
return acc
}, {})
batchForms[b.batch] = {
acceptType: d.accept.acceptType || '1',
acceptDate: d.accept.acceptDate || '',
acceptAddress: d.accept.acceptAddress || '',
question: d.accept.question || '',
remark: d.accept.remark || '',
templateFileIds: d.accept.templateFileIds || [],
acceptContents: (d.contents || []).map((c: any) => {
const cfg = itemMap[c.configId]
return {
configId: c.configId,
itemName: cfg?.itemName || '',
type: cfg?.type,
isQualified: c.isQualified || '1',
remark: c.remark || '',
}
}),
acceptTeam: (d.team || []).map((t: any) => ({
name: t.name,
deptCode: t.deptCode,
deptName: t.deptName,
roleType: t.roleType || '',
})),
}
if (batchForms[b.batch].acceptTeam.length < 3) {
while (batchForms[b.batch].acceptTeam.length < 3) {
batchForms[b.batch].acceptTeam.push({ name: '', deptCode: '', deptName: '' })
}
}
if (acceptanceItems.value.length && (!batchForms[b.batch].acceptContents || batchForms[b.batch].acceptContents.length === 0)) {
batchForms[b.batch].acceptContents = acceptanceItems.value.map((it: any) => ({
configId: it.id,
itemName: it.itemName,
type: it.type,
isQualified: '1',
remark: '',
}))
}
}
} catch (_) {}
}
}
const saveCommonConfig = async () => {
const formRef = commonFormRef.value
const valid = await formRef?.validate?.().catch(() => false)
if (!valid) return
// 直接从子组件 form 读取,确保拿到用户填写的最新值(避免 v-model 同步延迟)
const form = formRef?.form || commonForm.value
const isInstallment = form.isInstallment === '1' || form.isInstallment === 1
if (isInstallment && (!form.totalPhases || form.totalPhases < 1)) {
useMessage().error('请填写分期次数')
return
}
saving.value = true
try {
await apiSaveCommonConfig({
purchaseId: String(purchaseId.value),
hasContract: form.hasContract ?? '0',
contractId: form.contractId ?? '',
isInstallment: form.isInstallment ?? '0',
totalPhases: isInstallment ? (Number(form.totalPhases) || 1) : 1,
supplierName: String(form.supplierName ?? ''),
supplierContact: String(form.supplierContact ?? ''),
purchaserId: String(form.purchaserId ?? ''),
purchaserName: String(form.purchaserName ?? ''),
assetAdminId: String(form.assetAdminId ?? ''),
assetAdminName: String(form.assetAdminName ?? ''),
})
useMessage().success('保存成功')
await loadData()
} catch (e: any) {
useMessage().error(e?.msg || '保存失败')
} finally {
saving.value = false
}
}
const saveCurrentBatch = async () => {
const curBatch = Number(activeTab.value)
const batchFormRef = batchFormRefMap.value[curBatch]
const valid = await batchFormRef?.validate?.().catch(() => false)
if (!valid) return
const b = batches.value.find((x: any) => String(x.batch) === activeTab.value)
if (!b?.id) return
const form = batchForms[curBatch]
if (!form) return
if (!form.acceptType) {
useMessage().error('请选择验收方式')
return
}
if (!form.acceptDate) {
useMessage().error('请选择验收日期')
return
}
const team = (form.acceptTeam || []).filter((m: any) => m?.name)
if (team.length < 3 || team.length % 2 === 0) {
useMessage().error('验收小组至少3人且为单数')
return
}
saving.value = true
try {
await updateBatch({
id: b.id,
purchaseId: String(purchaseId.value),
acceptType: form.acceptType,
acceptDate: form.acceptDate,
acceptAddress: form.acceptAddress,
question: form.question,
remark: form.remark,
templateFileIds: form.templateFileIds || [],
acceptContents: form.acceptContents || [],
acceptTeam: team,
})
useMessage().success('保存成功')
batchSavedFlags.value[curBatch] = true
await loadData()
} catch (e: any) {
useMessage().error(e?.msg || '保存失败')
} finally {
saving.value = false
}
}
const handleClose = () => {
visible.value = false
emit('refresh')
}
// 下载履约验收模板
const handleDownloadTemplate = async () => {
if (!purchaseId.value) {
useMessage().error('请先选择采购项目')
return
}
// 默认下载当前期
await downloadTemplateForBatch(Number(activeTab.value))
}
// 处理下拉菜单命令
const handleDownloadTemplateCommand = async (command: string) => {
if (!purchaseId.value) {
useMessage().error('请先选择采购项目')
return
}
if (command === 'current') {
await downloadTemplateForBatch(Number(activeTab.value))
} else if (command === 'all') {
// 下载全部期数的模板
for (const batch of batches.value) {
await downloadTemplateForBatch(batch.batch)
}
useMessage().success(`已触发${batches.value.length}期模板下载`)
}
}
// 为指定批次下载模板
const downloadTemplateForBatch = async (batchNum: number) => {
try {
const response = await downloadPerformanceAcceptanceTemplate(String(purchaseId.value), batchNum)
// 使用项目中现有的工具函数处理文件下载
const fileName = `履约验收表-${purchaseId.value}-第${batchNum}期-${new Date().getTime()}.docx`;
handleBlobFile(response, fileName)
useMessage().success(`${batchNum}期履约模板下载成功`)
} catch (error: any) {
console.error('下载模板失败:', error)
useMessage().error(error?.msg || '下载履约模板失败')
}
}
const DEFAULT_COMMON_FORM = {
hasContract: '0',
contractId: '',
isInstallment: '0',
totalPhases: 1,
supplierName: '',
supplierContact: '',
purchaserId: '',
purchaserName: '',
assetAdminId: '',
assetAdminName: '',
}
/** 将弹窗内所有内容恢复为初始空值(替换整个对象以确保引用变化) */
const resetAllToDefault = () => {
openToken.value++
commonForm.value = { ...DEFAULT_COMMON_FORM }
applyInfo.value = null
mainTab.value = 'common'
activeTab.value = '1'
batchFormRefMap.value = {}
batches.value = []
acceptanceItems.value = []
canFillForm.value = true
Object.keys(batchForms).forEach((k) => delete batchForms[Number(k)])
batchSavedFlags.value = {}
}
const open = async (row: any) => {
purchaseId.value = row?.id ?? ''
rowProjectType.value = row?.projectType || 'A'
// 1. 先将弹窗内所有内容恢复为初始空值
resetAllToDefault()
// 2. 显示弹窗并开启 loading避免接口返回前展示旧数据
visible.value = true
loading.value = true
// 3. 等待 Vue 完成渲染,确保子组件已接收并展示空值
await nextTick()
await nextTick()
// 4. 再进行接口查询并覆盖
await loadData()
}
defineExpose({ open })
</script>
<style scoped>
.modal-body {
padding: 0;
max-height: 70vh;
overflow-y: auto;
overflow-x: hidden;
}
.main-tab-nav {
display: flex;
gap: 4px;
margin-bottom: 16px;
border-bottom: 1px solid var(--el-border-color);
}
.main-tab-item {
padding: 12px 20px;
cursor: pointer;
color: var(--el-text-color-regular);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 0.2s;
}
.main-tab-item:hover {
color: var(--el-color-primary);
}
.main-tab-item.active {
color: var(--el-color-primary);
font-weight: 600;
border-bottom-color: var(--el-color-primary);
}
.main-tab-content {
padding-top: 4px;
}
.tab-content {
min-height: 200px;
display: block;
}
.tip-box {
padding: 20px 0;
}
.batch-tabs {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.batch-tab-item {
padding: 8px 16px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.batch-tab-item:hover:not(.disabled) {
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
.batch-tab-item.active {
background: var(--el-color-primary);
border-color: var(--el-color-primary);
color: #fff;
}
.batch-tab-item.disabled {
cursor: not-allowed;
opacity: 0.6;
}
.batch-panel {
min-height: 200px;
}
</style>
<style>
/* 弹窗横向滚动修复,需非 scoped 以影响 el-dialog */
.purchasing-accept-modal .el-dialog__body {
overflow-x: hidden;
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,355 @@
<template>
<div class="implement-page">
<div class="implement-form">
<el-form-item label="实施采购方式" required>
<el-radio-group v-model="implementType">
<el-radio label="1">自行组织采购</el-radio>
<el-radio label="2">委托代理采购</el-radio>
</el-radio-group>
</el-form-item>
<!-- 采购文件版本列表保留原文件多版本分别显示 -->
<el-divider content-position="left">采购文件版本</el-divider>
<div v-if="purchaseFileVersions.length" class="file-versions mb-2">
<el-table :data="purchaseFileVersions" border size="small" max-height="280">
<el-table-column type="index" label="版本" width="70" align="center">
<template #default="{ $index }">V{{ $index + 1 }}</template>
</el-table-column>
<el-table-column prop="fileTitle" label="文件名" min-width="180" show-overflow-tooltip />
<el-table-column prop="createBy" label="上传人" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="createTime" label="上传时间" width="165" align="center">
<template #default="{ row }">{{ formatCreateTime(row.createTime) }}</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleDownloadVersion(row)">下载</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="mb-2">可继续上传新版本保留原文件格式 doc/docx/pdf单文件不超过 5MB</div>
<upload-file
v-model="implementFileIds"
:limit="5"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: PURCHASE_FILE_TYPE }"
upload-file-url="/purchase/purchasingfiles/upload"
/>
<!-- 仅部门审核角色显示采购代表相关 -->
<template v-if="isDeptAuditRole">
<el-divider content-position="left">采购代表</el-divider>
<div class="mb-2">需求部门初审需指定采购代表人请选择一种方式</div>
<el-radio-group v-model="representorMode" class="mb-2">
<el-radio label="single">指定采购代表人单人</el-radio>
<el-radio label="multi">部门多人由系统自动抽取</el-radio>
</el-radio-group>
<el-form-item v-if="representorMode === 'single'" label="采购代表人">
<el-select v-model="representorTeacherNo" placeholder="请选择" clearable filterable style="width: 100%">
<el-option v-for="m in deptMembers" :key="m.userId || m.teacherNo || m.id" :label="m.realName || m.name || m.teacherNo" :value="m.userId || m.teacherNo || m.id" />
</el-select>
</el-form-item>
<el-form-item v-else label="部门多人">
<el-select v-model="representorsMulti" placeholder="请选择多人,系统将自动抽取一人" clearable filterable multiple style="width: 100%">
<el-option v-for="m in deptMembers" :key="m.userId || m.teacherNo || m.id" :label="m.realName || m.name || m.teacherNo" :value="m.userId || m.teacherNo || m.id" />
</el-select>
</el-form-item>
</template>
</div>
<div class="implement-footer">
<el-button @click="handleClose">取消</el-button>
<template v-if="implementHasPurchaseFiles && !applyRow?.fileFlowInstId">
<el-button type="primary" :loading="implementSubmitting" @click="handleImplementSubmit">保存实施采购</el-button>
<el-button v-if="canStartFileFlow" type="success" :loading="startFileFlowSubmitting" @click="handleStartFileFlow">发起采购文件审批</el-button>
</template>
<template v-else>
<el-button type="primary" :loading="implementSubmitting" @click="handleImplementSubmit">确定</el-button>
</template>
</div>
</div>
</template>
<script setup lang="ts" name="PurchasingImplement">
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { implementApply, getApplyFiles, startFileFlow, getDeptMembers, getObj } from '/@/api/finance/purchasingrequisition'
import { useMessage } from '/@/hooks/message'
import other from '/@/utils/other'
import UploadFile from '/@/components/Upload/index.vue'
import { Session } from '/@/utils/storage'
import * as orderVue from '/@/api/order/order-key-vue'
/** 部门审核角色编码:仅该角色下显示采购代表相关页面和功能,流转至部门审核时需填写采购代表 */
const PURCHASE_DEPT_AUDIT_ROLE_CODE = 'PURCHASE_DEPT_AUDIT'
/** 采购中心角色编码:可保存/发起实施采购,但不出现采购代表相关内容和接口 */
const PURCHASE_CENTER_ROLE_CODE = 'PURCHASE_CENTER'
const roleCode = computed(() => Session.getRoleCode() || '')
const isDeptAuditRole = computed(() => roleCode.value === PURCHASE_DEPT_AUDIT_ROLE_CODE)
const isPurchaseCenterRole = computed(() => roleCode.value === PURCHASE_CENTER_ROLE_CODE)
/** 可发起采购文件审批:部门审核(需填采购代表)、采购中心(不填采购代表) */
const canStartFileFlow = computed(() => isDeptAuditRole.value || isPurchaseCenterRole.value)
// 与编辑界面一致:支持流程 dynamic-link 传入 currJob/currElTab申请单 ID 优先取 currJob.orderId
const props = defineProps({
currJob: { type: Object, default: null },
currElTab: { type: Object, default: null }
})
const emit = defineEmits(['handleJob'])
/** 是否被流程 handle 页面通过 dynamic-link 嵌入 */
const isFlowEmbed = computed(() => !!props.currJob)
const route = useRoute()
const PURCHASE_FILE_TYPE = '130'
/** 申请单 ID数值用于 getObj 等):与 add 一致,优先流程 currJob.orderId否则 route.query.id */
const applyId = computed(() => {
const raw = applyIdRaw.value
if (raw == null || raw === '') return null
const n = Number(raw)
return Number.isNaN(n) ? null : n
})
/** 申请单 ID 原始字符串(用于 getApplyFiles 的 purchaseId与编辑页一致避免类型/精度问题) */
const applyIdRaw = computed(() => {
if (props.currJob?.orderId != null && props.currJob?.orderId !== '') {
return String(props.currJob.orderId)
}
const id = route.query.id
return id != null && id !== '' ? String(id) : ''
})
const applyRow = ref<any>(null)
/** 已有采购文件版本列表(按 createTime 排序,用于展示与提交时一并关联) */
const purchaseFileVersions = ref<{ id: string; fileTitle?: string; createBy?: string; createTime?: string; remark?: string }[]>([])
/** 本次新上传的采购文件(仅新版本,不与已有版本混在一起) */
const implementFileIds = ref<string | string[]>([])
const implementType = ref<string>('1')
const implementSubmitting = ref(false)
const representorMode = ref<'single' | 'multi'>('single')
const representorTeacherNo = ref<string>('')
const representorsMulti = ref<string[]>([])
const deptMembers = ref<any[]>([])
const startFileFlowSubmitting = ref(false)
const implementHasPurchaseFiles = computed(() => {
if (purchaseFileVersions.value.length > 0) return true
const raw = implementFileIds.value
if (Array.isArray(raw)) return raw.length > 0
return !!raw
})
function formatCreateTime(t?: string) {
if (!t) return '-'
const d = new Date(t)
return isNaN(d.getTime()) ? t : d.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
}
function handleDownloadVersion(file: { remark?: string; fileTitle?: string }) {
if (!file?.remark) {
useMessage().warning('无法获取文件路径')
return
}
const url = `/purchase/purchasingfiles/download?fileName=${encodeURIComponent(file.remark)}&fileTitle=${encodeURIComponent(file.fileTitle || '采购文件')}`
other.downBlobFile(url, {}, file.fileTitle || '采购文件')
}
const isInIframe = () => typeof window !== 'undefined' && window.self !== window.top
const postMessage = (type: string, payload?: any) => {
if (typeof window !== 'undefined' && window.parent) {
window.parent.postMessage({ type, ...payload }, '*')
}
}
const loadData = async () => {
const id = applyId.value
if (!id) {
useMessage().warning('缺少申请单ID')
return
}
const needDeptMembers = isDeptAuditRole.value
try {
const idStr = applyIdRaw.value || String(id)
const requests: [ReturnType<typeof getObj>, ReturnType<typeof getApplyFiles>, ReturnType<typeof getDeptMembers>?] = [
getObj(id),
getApplyFiles(idStr)
]
if (needDeptMembers) requests.push(getDeptMembers())
const results = await Promise.all(requests)
const detailRes = results[0]
const filesRes = results[1]
const membersRes = needDeptMembers ? results[2] : null
applyRow.value = detailRes?.data ? { ...detailRes.data, id: detailRes.data.id ?? id } : { id }
const row = applyRow.value
if (row?.implementType) implementType.value = row.implementType
// 回显需求部门初审-采购代表人方式与人员(与发起采购文件审批时保存的一致)
if (row?.representorTeacherNo) {
representorMode.value = 'single'
representorTeacherNo.value = row.representorTeacherNo ?? ''
representorsMulti.value = []
} else if (row?.representors) {
representorMode.value = 'multi'
representorTeacherNo.value = ''
const parts = typeof row.representors === 'string' ? row.representors.split(',') : []
representorsMulti.value = parts.map((s: string) => s.trim()).filter(Boolean)
} else {
representorTeacherNo.value = ''
representorsMulti.value = []
}
const list = filesRes?.data || []
const purchaseFiles = list.filter((f: any) => f.fileType === PURCHASE_FILE_TYPE)
purchaseFileVersions.value = purchaseFiles.map((f: any) => ({
id: String(f.id),
fileTitle: f.fileTitle || f.file_title || '采购文件',
createBy: f.createBy ?? f.create_by ?? '-',
createTime: f.createTime || f.create_time,
remark: f.remark
}))
deptMembers.value = needDeptMembers && membersRes?.data ? membersRes.data : []
} catch (_) {
applyRow.value = { id }
purchaseFileVersions.value = []
deptMembers.value = []
}
}
const handleClose = () => {
postMessage('purchasingimplement:close')
if (!isInIframe()) {
window.history.back()
}
}
const handleImplementSubmit = async () => {
const row = applyRow.value
if (!row?.id && !applyId.value) return
const id = row?.id ?? applyId.value
if (!id) return
if (!implementType.value) {
useMessage().warning('请选择实施采购方式')
return
}
const existingIds = purchaseFileVersions.value.map((f) => f.id)
const raw = implementFileIds.value
const newIds: string[] = Array.isArray(raw)
? raw.map((x: any) => (typeof x === 'object' && x?.id ? x.id : x)).filter(Boolean)
: raw ? [String(raw)] : []
const fileIds = [...existingIds, ...newIds]
if (fileIds.length === 0) {
useMessage().warning('请至少上传一个采购文件')
return
}
// 仅部门审核角色提交采购代表;采购中心保存时不传采购代表
const single = isDeptAuditRole.value && representorMode.value === 'single' ? representorTeacherNo.value : undefined
const multi = isDeptAuditRole.value && representorMode.value === 'multi' && representorsMulti.value?.length ? representorsMulti.value.join(',') : undefined
implementSubmitting.value = true
try {
await implementApply(id, fileIds, implementType.value, single, multi)
useMessage().success('实施采购已保存')
implementFileIds.value = []
await loadData()
postMessage('purchasingimplement:saved')
// 流程嵌入场景:通知流程当前 Tab 已保存,避免审批时提示“未保存”
if (isFlowEmbed.value && props.currJob && props.currElTab?.id) {
orderVue.currElTabIsSave(props.currJob, props.currElTab.id, true, emit)
}
} catch (err: any) {
useMessage().error(err?.msg || '实施采购失败')
} finally {
implementSubmitting.value = false
}
}
const handleStartFileFlow = async () => {
const row = applyRow.value
const id = row?.id ?? applyId.value
if (!id) return
// 部门审核角色必须填写采购代表;采购中心不填采购代表
if (isDeptAuditRole.value) {
if (representorMode.value === 'single') {
if (!representorTeacherNo.value) {
useMessage().warning('请选择采购代表人')
return
}
} else {
if (!representorsMulti.value?.length) {
useMessage().warning('请选择部门多人')
return
}
}
}
startFileFlowSubmitting.value = true
try {
const single = isDeptAuditRole.value && representorMode.value === 'single' ? representorTeacherNo.value : undefined
const multi = isDeptAuditRole.value && representorMode.value === 'multi' ? representorsMulti.value.join(',') : undefined
await startFileFlow(id, single, multi)
useMessage().success('已发起采购文件审批流程')
postMessage('purchasingimplement:submitSuccess')
await loadData()
} catch (err: any) {
useMessage().error(err?.msg || '发起失败')
} finally {
startFileFlowSubmitting.value = false
}
}
/** 流程嵌入时供 handle.vue 调用的“保存”回调:与页面按钮保存逻辑保持一致 */
async function flowSubmitForm() {
await handleImplementSubmit()
}
// 流程切换工单时重新加载数据(与 add 编辑页一致)
watch(
() => props.currJob?.orderId ?? props.currJob?.id,
(newVal, oldVal) => {
if (newVal !== oldVal && applyId.value) {
loadData()
}
}
)
onMounted(async () => {
await loadData()
if (isInIframe()) {
document.documentElement.classList.add('iframe-mode')
document.body.classList.add('iframe-mode')
}
// 流程嵌入:注册 tab 保存回调,供审批页调用(与采购申请编辑页保持一致)
if (isFlowEmbed.value && props.currJob && props.currElTab?.id) {
orderVue.currElTabIsExist(props.currJob, props.currElTab.id)
await orderVue.currElTabIsView({}, props.currJob, props.currElTab.id, flowSubmitForm)
}
})
</script>
<style scoped lang="scss">
.implement-page {
padding: 20px;
min-height: 100%;
display: flex;
flex-direction: column;
}
.implement-form {
flex: 1;
.mb-2 {
margin-bottom: 8px;
}
}
.implement-form-tip {
margin-top: 12px;
padding: 8px 0;
}
.implement-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--el-border-color-lighter);
display: flex;
gap: 12px;
flex-wrap: wrap;
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<el-dialog
v-model="visible"
title="实施采购"
width="50%"
:close-on-click-modal="false"
destroy-on-close
class="implement-iframe-dialog"
@close="handleClose"
>
<div class="implement-iframe-content">
<iframe
ref="iframeRef"
:src="iframeSrc"
frameborder="0"
class="implement-iframe"
/>
</div>
</el-dialog>
</template>
<script setup lang="ts" name="ImplementForm">
import { ref, computed, watch } from 'vue'
const emit = defineEmits<{
(e: 'refresh'): void
}>()
const visible = ref(false)
const iframeRef = ref<HTMLIFrameElement>()
const applyId = ref<string | number>('')
const iframeSrc = computed(() => {
const baseUrl = window.location.origin + window.location.pathname
return `${baseUrl}#/finance/purchasingrequisition/implement?id=${applyId.value}`
})
const handleClose = () => {
visible.value = false
window.removeEventListener('message', handleMessage)
}
const handleMessage = (event: MessageEvent) => {
if (event.data?.type === 'purchasingimplement:submitSuccess') {
handleClose()
emit('refresh')
} else if (event.data?.type === 'purchasingimplement:close') {
handleClose()
} else if (event.data?.type === 'purchasingimplement:saved') {
emit('refresh')
}
}
const openDialog = (row: { id: string | number }) => {
applyId.value = row?.id ?? ''
visible.value = true
window.addEventListener('message', handleMessage)
}
watch(visible, (val) => {
if (!val) {
window.removeEventListener('message', handleMessage)
}
})
defineExpose({
openDialog,
})
</script>
<style scoped lang="scss">
.implement-iframe-content {
width: 100%;
height: 65vh;
min-height: 480px;
max-height: calc(100vh - 180px);
position: relative;
overflow: hidden;
.implement-iframe {
width: 100%;
height: 100%;
min-height: 480px;
border: none;
display: block;
}
}
</style>
<style>
.implement-iframe-dialog .el-dialog__body {
padding: 16px;
overflow: hidden;
}
</style>

View File

@@ -43,12 +43,11 @@
placeholder="请选择状态"
clearable
style="width: 200px">
<el-option label="撤回" value="-2" />
<el-option label="暂存" value="-1" />
<el-option label="运行中" value="0" />
<el-option label="完成" value="1" />
<el-option label="作废" value="2" />
<el-option label="终止" value="3" />
</el-select>
</el-form-item>
<el-form-item label="是否集采" prop="isCentralized">
@@ -77,6 +76,14 @@
采购申请管理
</span>
<div class="header-actions">
<el-button
icon="Files"
link
type="primary"
>
采购申请汇总
</el-button>
<el-button
icon="FolderAdd"
type="primary"
@@ -102,7 +109,7 @@
<el-icon><List /></el-icon>
</template>
</el-table-column>
<el-table-column prop="code" label="申请单编号" min-width="140" show-overflow-tooltip>
<el-table-column prop="purchaseNo" label="申请单编号" min-width="140" show-overflow-tooltip>
<template #header>
<el-icon><DocumentCopy /></el-icon>
<span style="margin-left: 4px">申请单编号</span>
@@ -160,8 +167,10 @@
<span style="margin-left: 4px">是否特殊</span>
</template>
<template #default="scope">
<el-tag v-if="scope.row.isSpecial === '1' || scope.row.isSpecial === 1" type="warning"></el-tag>
<el-tag v-else-if="scope.row.isSpecial === '0' || scope.row.isSpecial === 0" type="info"></el-tag>
<el-tag v-if="String(scope.row.isSpecial) === '1'" type="warning">紧急</el-tag>
<el-tag v-else-if="String(scope.row.isSpecial) === '2'" type="danger">单一</el-tag>
<el-tag v-else-if="String(scope.row.isSpecial) === '3'" type="info">进口</el-tag>
<el-tag v-else-if="String(scope.row.isSpecial) === '0'" type="info"></el-tag>
<span v-else>-</span>
</template>
</el-table-column>
@@ -171,8 +180,8 @@
<span style="margin-left: 4px">是否集采</span>
</template>
<template #default="scope">
<el-tag v-if="scope.row.isCentralized === '1'" type="success"></el-tag>
<el-tag v-else-if="scope.row.isCentralized === '0'" type="info"></el-tag>
<el-tag v-if="String(scope.row.isCentralized) === '1'" type="success"></el-tag>
<el-tag v-else-if="String(scope.row.isCentralized) === '0'" type="info"></el-tag>
<span v-else>-</span>
</template>
</el-table-column>
@@ -182,6 +191,40 @@
<span style="margin-left: 4px">审核状态</span>
</template>
<template #default="scope">
<el-tooltip v-if="scope.row.flowInstId" content="点击查看审批过程" placement="top">
<el-tag
v-if="scope.row.status === '-2'"
type="info"
class="status-tag-clickable"
@click="handleShowFlowComment(scope.row)">撤回</el-tag>
<el-tag
v-else-if="scope.row.status === '-1'"
type="warning"
class="status-tag-clickable"
@click="handleShowFlowComment(scope.row)">暂存</el-tag>
<el-tag
v-else-if="scope.row.status === '0'"
type="primary"
class="status-tag-clickable"
@click="handleShowFlowComment(scope.row)">运行中</el-tag>
<el-tag
v-else-if="scope.row.status === '1'"
type="success"
class="status-tag-clickable"
@click="handleShowFlowComment(scope.row)">完成</el-tag>
<el-tag
v-else-if="scope.row.status === '2'"
type="danger"
class="status-tag-clickable"
@click="handleShowFlowComment(scope.row)">作废</el-tag>
<el-tag
v-else-if="scope.row.status === '3'"
type="info"
class="status-tag-clickable"
@click="handleShowFlowComment(scope.row)">终止</el-tag>
<span v-else>-</span>
</el-tooltip>
<template v-else>
<el-tag v-if="scope.row.status === '-2'" type="info">撤回</el-tag>
<el-tag v-else-if="scope.row.status === '-1'" type="warning">暂存</el-tag>
<el-tag v-else-if="scope.row.status === '0'" type="primary">运行中</el-tag>
@@ -190,32 +233,69 @@
<el-tag v-else-if="scope.row.status === '3'" type="info">终止</el-tag>
<span v-else>-</span>
</template>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="300">
<el-table-column prop="fileFlowStatus" label="文件审批状态" width="110" align="center">
<template #header>
<el-icon><DocumentChecked /></el-icon>
<span style="margin-left: 4px">文件审批状态</span>
</template>
<template #default="scope">
<el-button
icon="View"
link
<template v-if="scope.row.fileFlowInstId">
<el-tooltip content="点击查看审批过程" placement="top">
<el-tag
v-if="scope.row.fileFlowStatus === '-2'"
type="info"
class="status-tag-clickable"
@click="handleShowFileFlowComment(scope.row)">撤回</el-tag>
<el-tag
v-else-if="scope.row.fileFlowStatus === '-1'"
type="warning"
class="status-tag-clickable"
@click="handleShowFileFlowComment(scope.row)">暂存</el-tag>
<el-tag
v-else-if="scope.row.fileFlowStatus === '0'"
type="primary"
@click="formDialogRef.openDialog('view', scope.row)">
class="status-tag-clickable"
@click="handleShowFileFlowComment(scope.row)">运行中</el-tag>
<el-tag
v-else-if="scope.row.fileFlowStatus === '1'"
type="success"
class="status-tag-clickable"
@click="handleShowFileFlowComment(scope.row)">完成</el-tag>
<el-tag
v-else-if="scope.row.fileFlowStatus === '2'"
type="danger"
class="status-tag-clickable"
@click="handleShowFileFlowComment(scope.row)">作废</el-tag>
<el-tag
v-else-if="scope.row.fileFlowStatus === '3'"
type="info"
class="status-tag-clickable"
@click="handleShowFileFlowComment(scope.row)">终止</el-tag>
<span v-else class="status-tag-clickable" @click="handleShowFileFlowComment(scope.row)">-</span>
</el-tooltip>
</template>
<template v-else>
<span style="color: #909399;"></span>
</template>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="150">
<template #default="scope">
<div class="op-cell">
<el-button
type="primary"
link
icon="View"
@click="handleView(scope.row)">
查看
</el-button>
<el-button
v-if="scope.row.status === '-1'"
icon="Edit"
link
type="primary"
@click="formDialogRef.openDialog('edit', scope.row)">
编辑
</el-button>
<el-button
v-if="scope.row.status === '-1'"
icon="Delete"
link
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
<ActionDropdown
:items="getActionMenuItems(scope.row)"
@command="(command) => handleMoreCommand(command, scope.row)"
/>
</div>
</template>
</el-table-column>
</el-table>
@@ -226,52 +306,56 @@
:total="state.pagination.total"
:current="state.pagination.current"
:size="state.pagination.size"
@pagination="getDataList"
@sizeChange="sizeChangeHandle"
@currentChange="currentChangeHandle"
/>
</el-card>
</div>
<!-- 新增页面 iframe 对话框 -->
<el-dialog
v-model="showAddIframe"
title="新增采购申请"
width="90%"
:style="{ maxWidth: '1600px' }"
:close-on-click-modal="false"
:close-on-press-escape="true"
destroy-on-close
class="iframe-dialog"
@close="closeAddIframe">
<div class="iframe-dialog-content">
<iframe
ref="addIframeRef"
:src="addIframeSrc"
frameborder="0"
class="add-iframe"
/>
</div>
</el-dialog>
<!-- 编辑新增表单对话框 -->
<!-- 新增/编辑/查看统一使用 form.vue 弹窗iframe 引入 add.vue -->
<FormDialog
ref="formDialogRef"
:dict-data="dictData"
@refresh="getDataList" />
<!-- 履约验收弹窗 -->
<PurchasingAcceptModal ref="acceptModalRef" @refresh="getDataList" />
<!-- 查看审批过程申请单审批 / 文件审批 -->
<el-dialog
v-model="showFlowComment"
v-if="showFlowComment"
:title="currFlowCommentType === 'file' ? '查看文件审批过程' : '查看审批过程'"
top="20px"
width="90%"
append-to-body
destroy-on-close
@close="currFlowJob = null; currFlowCommentType = 'apply'">
<FlowCommentTimeline v-if="currFlowJob" :key="String(currFlowJob.flowInstId) + currFlowCommentType" :curr-job="currFlowJob" />
</el-dialog>
<!-- 实施采购iframe 嵌入 implement.vue供列表与流程页面使用 -->
<ImplementForm ref="implementFormRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="PurchasingRequisition">
import { ref, reactive, defineAsyncComponent, onUnmounted, onMounted } from 'vue'
import { ref, reactive, defineAsyncComponent, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { getPage, delObj } from "/@/api/finance/purchasingrequisition";
import { getPage, delObj, submitObj, getArchiveDownloadUrl, getApplyTemplateDownloadUrl, getFileApplyTemplateDownloadUrl } from "/@/api/finance/purchasingrequisition";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { getDicts } from '/@/api/admin/dict';
import { getTree } from '/@/api/finance/purchasingcategory';
import { List, Document, DocumentCopy, Search, Collection, Money, CircleCheck, InfoFilled, Calendar, OfficeBuilding, Warning } from '@element-plus/icons-vue'
import { List, Document, DocumentCopy, Search, Collection, Money, CircleCheck, InfoFilled, Calendar, OfficeBuilding, Warning, DocumentChecked, Edit, Delete, Upload, FolderOpened, Download } from '@element-plus/icons-vue'
import other from '/@/utils/other'
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const ImplementForm = defineAsyncComponent(() => import('./implementForm.vue'));
const ActionDropdown = defineAsyncComponent(() => import('/@/components/tools/action-dropdown.vue'));
const PurchasingAcceptModal = defineAsyncComponent(() => import('./accept/PurchasingAcceptModal.vue'));
const FlowCommentTimeline = defineAsyncComponent(() => import('/@/views/jsonflow/comment/timeline.vue'));
// 字典数据和品目树数据
const dictData = ref({
@@ -288,11 +372,15 @@ const dictData = ref({
const router = useRouter()
const tableRef = ref()
const formDialogRef = ref()
const acceptModalRef = ref()
const searchFormRef = ref()
const showSearch = ref(true)
const showAddIframe = ref(false)
const addIframeRef = ref<HTMLIFrameElement>()
const addIframeSrc = ref('')
/** 审批过程弹窗:是否显示、当前行对应的流程 job供 Comment 组件用)、类型(申请单/文件) */
const showFlowComment = ref(false)
const currFlowJob = ref<{ id?: number; flowInstId?: number } | null>(null)
const currFlowCommentType = ref<'apply' | 'file'>('apply')
const implementFormRef = ref()
/**
* 定义响应式表格数据
@@ -312,7 +400,7 @@ const state: BasicTableProps = reactive<BasicTableProps>({
/**
* 使用 useTable 定义表格相关操作
*/
const { getDataList, tableStyle } = useTable(state);
const { getDataList, tableStyle, sizeChangeHandle, currentChangeHandle } = useTable(state);
/**
* 重置搜索表单
@@ -323,42 +411,67 @@ const handleReset = () => {
};
/**
* 新增采购申请 - 在 iframe 中展示
* 新增采购申请 - 统一通过 form.vue 弹窗iframe 引入 add.vue
*/
const handleAdd = () => {
// 构建 iframe 的 src使用当前页面的 hash 路由
const baseUrl = window.location.origin + window.location.pathname
addIframeSrc.value = `${baseUrl}#/finance/purchasingrequisition/add`
showAddIframe.value = true
// 监听来自 iframe 的消息
window.addEventListener('message', handleIframeMessage)
formDialogRef.value?.openDialog('add')
};
/**
* 关闭新增 iframe
*/
const closeAddIframe = () => {
showAddIframe.value = false
// 移除消息监听器
window.removeEventListener('message', handleIframeMessage)
};
/**
* 处理 iframe 发送的消息
* 点击审核状态:若有流程实例则打开「查看审批过程」弹窗(参考 hi-job.vue
* @param row - 当前行数据(需含 flowInstId
*/
const handleIframeMessage = (event: MessageEvent) => {
// 验证消息来源(可选,根据实际需求)
// if (event.origin !== window.location.origin) return
if (event.data && event.data.type === 'purchasingrequisition:submitSuccess') {
// 提交成功,关闭 iframe 并刷新列表
closeAddIframe()
getDataList()
useMessage().success('提交成功')
} else if (event.data && event.data.type === 'purchasingrequisition:close') {
// 关闭 iframe
closeAddIframe()
/** 点击审核状态:打开申请单审批过程 */
const handleShowFlowComment = (row: any) => {
if (!row?.flowInstId) {
useMessage().info('暂存状态无审批过程');
return;
}
currFlowCommentType.value = 'apply';
currFlowJob.value = { id: row.id, flowInstId: row.flowInstId };
showFlowComment.value = true;
};
/** 点击文件审批状态:打开文件审批过程 */
const handleShowFileFlowComment = (row: any) => {
if (!row?.fileFlowInstId) {
useMessage().info('未发起文件审批流程');
return;
}
currFlowCommentType.value = 'file';
const flowInstId = typeof row.fileFlowInstId === 'string' ? parseInt(row.fileFlowInstId, 10) : row.fileFlowInstId;
currFlowJob.value = { id: row.id, flowInstId: Number.isNaN(flowInstId) ? row.fileFlowInstId : flowInstId };
showFlowComment.value = true;
};
/**
* 打开查看对话框
* @param row - 当前行数据
*/
const handleView = (row: any) => {
formDialogRef.value?.openDialog('view', row);
};
/**
* 打开编辑对话框
* @param row - 当前行数据
*/
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row);
};
/**
* 履约验收
* @param row - 当前行数据
*/
const handleAccept = (row: any) => {
acceptModalRef.value?.open(row);
};
/** 打开实施采购(仅暂存状态可点;通过 iframe 嵌入 implement.vue */
const handleImplement = (row: any) => {
implementFormRef.value?.openDialog(row);
};
/**
@@ -381,6 +494,142 @@ const handleDelete = async (row: any) => {
}
};
/** 暂存状态下提交采购申请(启动流程) */
const handleSubmit = async (row: any) => {
try {
await useMessageBox().confirm('确定要提交该采购申请并启动流程吗?');
} catch {
return;
}
try {
await submitObj({ id: row.id });
useMessage().success('提交成功');
getDataList();
} catch (err: any) {
useMessage().error(err?.msg || '提交失败');
}
};
/** 操作栏「更多」菜单项配置 */
const getActionMenuItems = (row: any) => {
const isTemp = row?.status === '-1';
return [
{
command: 'edit',
label: '编辑',
icon: Edit,
visible: () => isTemp,
},
{
command: 'submit',
label: '提交',
icon: Upload,
visible: () => isTemp,
},
{
command: 'delete',
label: '删除',
icon: Delete,
visible: () => isTemp,
},
{
command: 'accept',
label: '履约验收',
icon: DocumentChecked,
visible: () => true,
},
{
command: 'implement',
label: '实施采购',
icon: Upload
},
{
command: 'archive',
label: '文件归档',
icon: FolderOpened,
visible: () => true,
},
{
command: 'downloadApply',
label: '下载审批表',
icon: Download,
visible: () => true,
},
{
command: 'downloadFileApply',
label: '下载文件审批表',
icon: Download,
visible: () => true,
},
];
};
/** 处理更多操作下拉菜单命令 */
const handleMoreCommand = (command: string, row: any) => {
switch (command) {
case 'edit':
handleEdit(row);
break;
case 'submit':
handleSubmit(row);
break;
case 'delete':
handleDelete(row);
break;
case 'accept':
handleAccept(row);
break;
case 'implement':
handleImplement(row);
break;
case 'archive':
handleArchive(row);
break;
case 'downloadApply':
handleDownloadApply(row);
break;
case 'downloadFileApply':
handleDownloadFileApply(row);
break;
}
};
/** 下载审批表 */
const handleDownloadApply = (row: any) => {
const id = row?.id ?? row?.purchaseId;
if (id == null || id === '') {
useMessage().warning('无法获取申请单ID');
return;
}
const url = getApplyTemplateDownloadUrl(id);
const fileName = `审批表_${row?.purchaseNo || id}.docx`;
other.downBlobFile(url, {}, fileName);
};
/** 下载文件审批表 */
const handleDownloadFileApply = (row: any) => {
const id = row?.id ?? row?.purchaseId;
if (id == null || id === '') {
useMessage().warning('无法获取申请单ID');
return;
}
const url = getFileApplyTemplateDownloadUrl(id);
const fileName = `文件审批表_${row?.purchaseNo || id}.docx`;
other.downBlobFile(url, {}, fileName);
};
/** 文件归档:按文件类型打包下载该申请单下所有附件 */
const handleArchive = (row: any) => {
const id = row?.id ?? row?.purchaseId;
if (id == null || id === '') {
useMessage().warning('无法获取申请单ID');
return;
}
const url = getArchiveDownloadUrl(id);
const fileName = `归档_${row?.purchaseNo || id}.zip`;
other.downBlobFile(url, {}, fileName);
};
// 获取字典数据和品目树数据
const loadDictData = async () => {
try {
@@ -526,53 +775,19 @@ onMounted(() => {
loadDictData();
});
// 组件卸载时清理事件监听器
onUnmounted(() => {
window.removeEventListener('message', handleIframeMessage)
});
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
.iframe-dialog-content {
width: 100%;
height: 70vh;
min-height: 500px;
max-height: calc(100vh - 200px);
position: relative;
overflow: hidden;
.add-iframe {
width: 100%;
height: 100%;
min-height: 500px;
border: none;
display: block;
}
}
:deep(.iframe-dialog) {
.el-dialog {
.op-cell {
display: flex;
flex-direction: column;
max-height: 90vh;
margin-top: 5vh !important;
align-items: center;
justify-content: center;
}
.el-dialog__header {
flex-shrink: 0;
padding: 20px 20px 10px;
}
.el-dialog__body {
padding: 20px;
overflow-y: auto;
overflow-x: hidden;
flex: 1;
min-height: 0;
max-height: calc(100vh - 200px);
}
.status-tag-clickable {
cursor: pointer;
}
</style>

View File

@@ -161,14 +161,14 @@
</el-timeline-item>
</el-timeline>
</div>
<footer class="el-dialog__footer" style="text-align: center;">
<span class="dialog-footer">
<el-button type="primary" @click="printForm">{{
t('jfI18n.print')
}}
</el-button>
</span>
</footer>
<!-- <footer class="el-dialog__footer" style="text-align: center;">-->
<!-- <span class="dialog-footer">-->
<!-- <el-button type="primary" @click="printForm">{{-->
<!-- t('jfI18n.print')-->
<!-- }}-->
<!-- </el-button>-->
<!-- </span>-->
<!-- </footer>-->
<el-drawer
v-if="data.showHiJob"

View File

@@ -0,0 +1,121 @@
<template>
<el-dialog v-model="visible" :title="title" width="600" append-to-body>
<!-- <el-alert-->
<!-- type="warning"-->
<!-- :closable="false"-->
<!-- show-icon-->
<!-- style="margin-bottom: 20px;">-->
<!-- <template #title>-->
<!-- <span>下载模板</span>-->
<!-- </template>-->
<!-- </el-alert>-->
<div style="text-align: center; margin-bottom: 20px">
<el-button type="success" :icon="Download" @click="handleDownloadTemplate">下载模板</el-button>
</div>
<el-upload
ref="uploadRef"
class="upload-demo"
:action="uploadUrl"
:headers="headers"
:accept="'.xls,.xlsx'"
:on-success="handleUploadSuccess"
:on-error="handleAvatarError"
:limit="1"
drag
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">只能上传 .xls .xlsx 格式的 Excel 文件</div>
</template>
</el-upload>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue';
import { ElNotification } from 'element-plus';
import { Download, UploadFilled } from '@element-plus/icons-vue';
import { Session } from '/@/utils/storage';
import { downBlobFile } from '/@/utils/other';
const title = ref('');
// 响应式数据
const visible = ref(false);
// 计算属性
const headers = computed(() => {
return {
Authorization: 'Bearer ' + Session.getToken(),
TENANT_ID: Session.getTenant()
};
});
const uploadUrl = ref('')
const currentType = ref('')
const uploadRef = ref<{ clearFiles?: () => void }>()
const titleMap: Record<string, string> = {
titleRelation: '职称信息导入',
quaRelation: '职业资格信息导入',
cerRelation: '教师资格证信息导入',
eduDegree: '学历学位信息导入',
partyChange: '党组织变动信息导入',
honor: '综合表彰信息导入'
}
// 方法
const init = (type: any) => {
currentType.value = type
uploadUrl.value = '/professional/file/importTeacherOtherInfo?type=' + type
title.value = titleMap[type] || '信息导入'
visible.value = true
nextTick(() => {
uploadRef.value?.clearFiles()
})
}
// Emits
const emit = defineEmits<{
(e: 'refreshData'): void
}>()
const handleUploadSuccess = () => {
visible.value = false;
ElNotification({
title: '成功',
message: '导入成功',
type: 'success',
});
emit('refreshData')
};
const handleAvatarError = (err: any) => {
const result = JSON.parse(err.message);
if (result.code == '1') {
ElNotification.error({
title: '错误',
message: result.msg,
duration: 30000,
});
}
};
const handleDownloadTemplate = () => {
downBlobFile('/professional/file/exportTeacherInfoTemplate', { type: currentType.value || 'titleRelation' }, title.value+'模板.xlsx')
}
// 暴露方法给父组件
defineExpose({
init,
});
</script>
<style scoped lang="scss">
.upload-demo {
:deep(.el-upload-dragger) {
width: 100%;
}
}
</style>

View File

@@ -37,6 +37,20 @@
</template>
</search-form>
<!-- 操作按钮 -->
<el-row>
<div class="mb15" style="width: 100%;">
<el-button
v-auth="'professional_teacherinfo_import'"
type="primary"
plain
icon="UploadFilled"
:loading="exportLoading"
@click="handleImportDialog"
>导入信息</el-button>
</div>
</el-row>
<!-- 表格 -->
<el-table
ref="tableRef"
@@ -77,6 +91,9 @@
@current-change="currentChangeHandle"
@size-change="sizeChangeHandle"
/>
<!-- 子组件 -->
<import-teacher-other-info ref="importTeacherOtherInfoRef" @refreshData="handleFilter" />
</div>
</div>
</template>
@@ -88,6 +105,7 @@ import { BasicTableProps, useTable } from '/@/hooks/table'
import { fetchList } from '/@/api/professional/professionaluser/professionalpartychange'
const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNameNo/index.vue'))
const ImportTeacherOtherInfo = defineAsyncComponent(() => import('/@/views/professional/common/import-teacher-other-info.vue'))
// 表格引用
const tableRef = ref()
@@ -100,6 +118,10 @@ const search = reactive({
realName: ''
})
// 导入加载状态
const exportLoading = ref(false)
const importTeacherOtherInfoRef = ref()
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
pageList: async (params: any) => {
@@ -130,6 +152,11 @@ const resetQuery = () => {
getDataList()
}
// 打开导入弹窗
const handleImportDialog = () => {
importTeacherOtherInfoRef.value?.init('partyChange')
}
// 表格数据由 useTablecreatedIsNeed 默认 true在挂载时自动请求
</script>

View File

@@ -73,6 +73,14 @@
@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
@@ -207,6 +215,7 @@
<!-- 子组件 -->
<DataForm ref="dataFormRef" @refreshData="handleFilter" />
<ProfessionalBackResaon ref="backReasonRef" @refreshData="handleFilter" />
<import-teacher-other-info ref="importTeacherOtherInfoRef" @refreshData="handleFilter" />
</div>
</template>
@@ -220,11 +229,10 @@ import { useDict } from '/@/hooks/dict'
import {
fetchList,
examObj,
delObj,
exportExcel
} from '/@/api/professional/professionaluser/professionalqualificationrelation'
delObj} from '/@/api/professional/professionaluser/professionalqualificationrelation'
import { getLevelList } from '/@/api/professional/rsbase/professionalqualificationconfig'
import { getWorkTypeList } from '/@/api/professional/rsbase/professionalworktype'
import { makeExportTeacherInfoByTypeTask } from '/@/api/professional/professionalfile';
import { PROFESSIONAL_AUDIT_STATE_OPTIONS } from '/@/config/global'
import { defineAsyncComponent } from 'vue'
import { Medal } from '@element-plus/icons-vue'
@@ -234,6 +242,7 @@ const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/i
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'))
// 审核状态选项
const auditStateOptions = PROFESSIONAL_AUDIT_STATE_OPTIONS
@@ -265,8 +274,9 @@ const search = reactive({
// 材料预览
const imgUrl = ref<Array<{ title: string; url: string }>>([])
// 导出加载状态
// 导出/导入加载状态
const exportLoading = ref(false)
const importTeacherOtherInfoRef = ref()
// 资格等级和工种列表
const qualificationLevelList = ref<any[]>([])
@@ -380,27 +390,15 @@ const handleDel = (row: any) => {
// 导出
const handleDownLoadWord = async () => {
exportLoading.value = true
try {
const response: any = await exportExcel(search)
const blob = new Blob([response as BlobPart])
const fileName = '职业资格信息.xls'
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
message.success('导出成功')
} catch (error) {
message.error('导出失败')
} finally {
exportLoading.value = false
}
}
exportLoading.value = true;
let params = Object.assign(search, { type: 'P20003' });
makeExportTeacherInfoByTypeTask(params).then((res: any) => {
message.success('后台下载进行中,请稍后查看任务列表');
});
setTimeout(() => {
exportLoading.value = false;
}, 3000); // 5分钟后自动关闭
};
// 获取资格等级名称
const getQualificationLevelName = (id: string | number) => {
@@ -414,6 +412,11 @@ const getWorkTypeName = (id: string | number) => {
return item ? item.workName : '-'
}
// 打开导入弹窗
const handleImportDialog = () => {
importTeacherOtherInfoRef.value?.init('quaRelation')
}
// 加载字典数据
const loadDictData = async () => {
try {

View File

@@ -84,10 +84,10 @@
/>
</el-form-item>
<el-form-item label="证书编" prop="certificateNumber">
<el-form-item label="证书编" prop="certificateNumber">
<el-input
v-model="dataForm.certificateNumber"
placeholder="请输入证书编(仅支持英文和数字)"
placeholder="请输入证书编(仅支持英文和数字)"
clearable
show-word-limit
maxlength="100"
@@ -215,8 +215,8 @@ const formRules = computed(() => {
{ required: true, message: '请输入所学专业', trigger: 'blur' }
],
certificateNumber: [
{ required: true, message: '请输入证书编', trigger: 'blur' },
{ pattern: /^[A-Za-z0-9]+$/, message: '证书编只能包含英文和数字', trigger: 'blur' }
{ required: true, message: '请输入证书编', trigger: 'blur' },
{ pattern: /^[A-Za-z0-9]+$/, message: '证书编只能包含英文和数字', trigger: 'blur' }
]
}

View File

@@ -69,6 +69,14 @@
@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>
</el-row>
@@ -218,6 +226,7 @@
<!-- 子组件 -->
<DataForm ref="dataFormRef" @refreshData="handleFilter" />
<ProfessionalBackResaon ref="backReasonRef" @refreshData="handleFilter" />
<import-teacher-other-info ref="importTeacherOtherInfoRef" @refreshData="handleFilter" />
</div>
</div>
</template>
@@ -232,19 +241,19 @@ import { useDict } from '/@/hooks/dict'
import {
fetchList,
examObj,
delObj,
exportExcel
} from '/@/api/professional/professionaluser/professionalteacheracademicrelation'
delObj} from '/@/api/professional/professionaluser/professionalteacheracademicrelation'
import { getDegreeList } from '/@/api/professional/rsbase/professionalacademicdegreeconfig'
import { getQualificationList } from '/@/api/professional/rsbase/academicqualificationsconfig'
import { getAllTypeList } from '/@/api/professional/rsbase/professionalacademiceducationtypeconfig'
import { PROFESSIONAL_AUDIT_STATE_OPTIONS } from '/@/config/global'
import { defineAsyncComponent } from 'vue'
import {makeExportTeacherInfoByTypeTask} from "/@/api/professional/professionalfile";
const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNameNo/index.vue'))
const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/index.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'))
// 审核状态选项
const auditStateOptions = PROFESSIONAL_AUDIT_STATE_OPTIONS
@@ -279,6 +288,7 @@ const imgUrl = ref<Array<{ title: string; url: string }>>([])
// 导出加载状态
const exportLoading = ref(false)
const importTeacherOtherInfoRef = ref()
// 学位、学历和教育类型列表
const degreeList = ref<any[]>([])
@@ -401,26 +411,14 @@ const handleDel = (row: any) => {
// 导出
const handleDownLoadWord = async () => {
exportLoading.value = true
try {
const response: any = await exportExcel(search)
const blob = new Blob([response as BlobPart])
const fileName = '学历学位信息.xls'
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
message.success('导出成功')
} catch (error) {
message.error('导出失败')
} finally {
exportLoading.value = false
}
exportLoading.value = true;
let params = Object.assign(search, { type: 'P20005' });
makeExportTeacherInfoByTypeTask(params).then((res: any) => {
message.success('后台下载进行中,请稍后查看任务列表');
});
setTimeout(() => {
exportLoading.value = false;
}, 3000); // 5分钟后自动关闭
}
// 获取学位名称
@@ -442,6 +440,11 @@ const getEducationTypeName = (id: string | number | undefined) => {
return item ? item.name : '-'
}
// 打开导入弹窗
const handleImportDialog = () => {
importTeacherOtherInfoRef.value?.init('eduDegree')
}
// 加载字典数据
const loadDictData = async () => {
try {

View File

@@ -69,6 +69,14 @@
@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>
</el-row>
@@ -188,6 +196,7 @@
<!-- 子组件 -->
<DataForm ref="dataFormRef" @refreshData="handleFilter" />
<ProfessionalBackResaon ref="backReasonRef" @refreshData="handleFilter" />
<import-teacher-other-info ref="importTeacherOtherInfoRef" @refreshData="handleFilter" />
</div>
</div>
</template>
@@ -203,9 +212,10 @@ import { getTeacherCertificateList } from '/@/api/professional/rsbase/profession
import {
fetchList,
examObj,
delObj,
exportExcel
delObj
} from '/@/api/professional/professionaluser/professionalteachercertificaterelation'
import { makeExportTeacherInfoByTypeTask } from '/@/api/professional/professionalfile';
import { PROFESSIONAL_AUDIT_STATE_OPTIONS } from '/@/config/global'
import { defineAsyncComponent } from 'vue'
const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNameNo/index.vue'))
@@ -213,6 +223,7 @@ const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/i
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'))
// 审核状态选项(独立定义,防止其他页面修改时被波及)
import type { StateOption } from '/@/components/AuditState/index.vue'
@@ -247,6 +258,7 @@ const imgUrl = ref<Array<{ title: string; url: string }>>([])
// 导出加载状态
const exportLoading = ref(false)
const importTeacherOtherInfoRef = ref()
// 证书列表
const certificateList = ref<any[]>([])
@@ -357,27 +369,15 @@ const handleDel = (row: any) => {
// 导出
const handleDownLoadWord = async () => {
exportLoading.value = true
try {
const response: any = await exportExcel(search)
const blob = new Blob([response as BlobPart])
const fileName = '教师资格证信息.xls'
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
message.success('导出成功')
} catch (error) {
message.error('导出失败')
} finally {
exportLoading.value = false
}
}
exportLoading.value = true;
let params = Object.assign(search, { type: 'P20004' });
makeExportTeacherInfoByTypeTask(params).then((res: any) => {
message.success('后台下载进行中,请稍后查看任务列表');
});
setTimeout(() => {
exportLoading.value = false;
}, 3000); // 5分钟后自动关闭
};
// 获取证书名称
const getCertificateName = (id: string | number) => {
@@ -385,6 +385,11 @@ const getCertificateName = (id: string | number) => {
return item ? item.cretificateName : '-'
}
// 打开导入弹窗
const handleImportDialog = () => {
importTeacherOtherInfoRef.value?.init('cerRelation')
}
// 加载字典数据
const loadDictData = async () => {
try {

View File

@@ -68,6 +68,14 @@
@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>
</el-row>
@@ -216,6 +224,7 @@
<!-- 子组件 -->
<ProfessionalBackResaon ref="backReasonRef" @refreshData="handleFilter" />
<DataForm ref="dataFormRef" @refreshData="handleFilter" />
<import-teacher-other-info ref="importTeacherOtherInfoRef" @refreshData="handleFilter" />
</div>
</div>
</template>
@@ -234,6 +243,7 @@ import {
} from '/@/api/professional/professionaluser/professionalteacherhonor'
import { PROFESSIONAL_AUDIT_STATE_OPTIONS, getStatusConfig } from '/@/config/global'
import { defineAsyncComponent } from 'vue'
import {makeExportTeacherInfoByTypeTask} from "/@/api/professional/professionalfile";
const TeacherNameNo = defineAsyncComponent(() => import('/@/components/TeacherNameNo/index.vue'))
const AuditState = defineAsyncComponent(() => import('/@/components/AuditState/index.vue'))
const ClickableTag = defineAsyncComponent(() => import('/@/components/ClickableTag/index.vue'))
@@ -241,6 +251,7 @@ const DetailPopover = defineAsyncComponent(() => import('/@/components/DetailPop
const ProfessionalBackResaon = defineAsyncComponent(() => import('/@/views/professional/common/professional-back-resaon.vue'))
const DataForm = defineAsyncComponent(() => import('./form.vue'))
const previewFile = defineAsyncComponent(() => import('/@/components/tools/preview-file.vue'))
const ImportTeacherOtherInfo = defineAsyncComponent(() => import('/@/views/professional/common/import-teacher-other-info.vue'))
// 消息提示 hooks
const message = useMessage()
@@ -281,6 +292,7 @@ const imgUrl = ref<Array<{ title: string; url: string }>>([])
// 导出加载状态
const exportLoading = ref(false)
const importTeacherOtherInfoRef = ref()
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
@@ -382,29 +394,19 @@ const handleDel = (row: any) => {
// 导出
const handleDownLoadWord = async () => {
exportLoading.value = true
try {
const response = await fetchList({
current: 1,
size: 999999,
...search
})
const data = response.data.records || []
const tHeader = ['工号', '姓名', '荣誉', '表彰单位', '年份']
const filterVal = ['teacherNo', 'teacherName', 'honor', 'honorCompany', 'year']
// 动态导入导出工具
const { export_json_to_excel } = await import('/@/excel/Export2Excel.js')
const exportData = data.map((v: any) => filterVal.map((j: string) => v[j]))
export_json_to_excel(tHeader, exportData, '综合表彰.xls')
message.success('导出成功')
} catch (error) {
message.error('导出失败')
} finally {
exportLoading.value = false
exportLoading.value = true;
let params = Object.assign(search, { type: 'P20006' });
makeExportTeacherInfoByTypeTask(params).then((res: any) => {
message.success('后台下载进行中,请稍后查看任务列表');
});
setTimeout(() => {
exportLoading.value = false;
}, 3000); // 5分钟后自动关闭
}
// 打开导入弹窗
const handleImportDialog = () => {
importTeacherOtherInfoRef.value?.init('honor')
}
// 表格数据由 useTablecreatedIsNeed 默认 true在挂载时自动请求

View File

@@ -6,23 +6,25 @@
show-icon
style="margin-bottom: 20px;">
<template #title>
<span>导入前请先下载字典文件部分字段需严格按照字典值填写</span>
<span> 可先导出教职工信息,按照导出信息的模板填入职工数据,再执行导入</span>
</template>
</el-alert>
<div style="text-align: center; margin-bottom: 20px;">
<a href="excel/dictlist.xlsx" rel="external nofollow" download="职工信息字典下载" style="text-decoration: none;">
<el-button type="success" :icon="Download">下载字典文件</el-button>
</a>
</div>
<!-- <div style="text-align: center; margin-bottom: 20px;">-->
<!-- <a href="excel/dictlist.xlsx" rel="external nofollow" download="职工信息字典下载" style="text-decoration: none;">-->
<!-- <el-button type="success" :icon="Download">下载字典文件</el-button>-->
<!-- </a>-->
<!-- </div>-->
<el-upload
ref="uploadRef"
class="upload-demo"
action="/professional/file/makeImportTeacherInfoSimpleTask"
:headers="headers"
:accept="'.xls,.xlsx'"
:on-success="handleUploadSuccess"
:on-error="handleAvatarError"
:limit="1"
drag>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
@@ -38,7 +40,7 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, nextTick } from 'vue'
import { ElNotification } from 'element-plus'
import { Download, UploadFilled } from '@element-plus/icons-vue'
import { Session } from '/@/utils/storage'
@@ -53,9 +55,14 @@
}
})
const uploadRef = ref<{ clearFiles?: () => void }>()
// 方法
const init = () => {
visible.value = true
nextTick(() => {
uploadRef.value?.clearFiles?.()
})
}
const handleUploadSuccess = () => {

View File

@@ -1024,7 +1024,7 @@
</el-table-column>
<el-table-column prop="graduateSchool" label="毕业学校" min-width="180" align="center" show-overflow-tooltip />
<el-table-column prop="major" label="所学专业" min-width="150" align="center" show-overflow-tooltip />
<el-table-column prop="certificateNumber" label="证书编" min-width="120" align="center" />
<el-table-column prop="certificateNumber" label="证书编" min-width="120" align="center" />
<el-table-column label="学历证书" min-width="150" align="center">
<template #default="scope">
<el-button

View File

@@ -1,225 +0,0 @@
<template>
<div>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="120px"
v-loading="loading"
:disabled="operType === 'view'"
>
<el-row :gutter="24">
<el-col :span="12" class="mb20" v-if="!hiddenFields.projectName">
<el-form-item label="采购项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入采购项目名称" :disabled="disabledFields.projectName" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.projectType">
<el-form-item label="项目类别" prop="projectType">
<el-select v-model="form.projectType" placeholder="请选择" :disabled="disabledFields.projectType" clearable style="width: 100%">
<el-option label="货物" value="A" />
<el-option label="工程" value="B" />
<el-option label="服务" value="C" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20" v-if="!hiddenFields.projectContent">
<el-form-item label="采购内容" prop="projectContent">
<el-input v-model="form.projectContent" type="textarea" :rows="3" placeholder="请输入采购内容" :disabled="disabledFields.projectContent" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.applyDate">
<el-form-item label="填报日期" prop="applyDate">
<el-date-picker
v-model="form.applyDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
:disabled="disabledFields.applyDate"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.fundSource">
<el-form-item label="资金来源" prop="fundSource">
<el-input v-model="form.fundSource" placeholder="资金来源" :disabled="disabledFields.fundSource" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.budget">
<el-form-item label="预算金额(元)" prop="budget">
<el-input-number v-model="form.budget" :min="0" :precision="2" :disabled="disabledFields.budget" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.isCentralized">
<el-form-item label="是否集采" prop="isCentralized">
<el-select v-model="form.isCentralized" placeholder="请选择" :disabled="disabledFields.isCentralized" clearable style="width: 100%">
<el-option label="否" value="0" />
<el-option label="是" value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.isSpecial">
<el-form-item label="是否特殊情况" prop="isSpecial">
<el-select v-model="form.isSpecial" placeholder="请选择" :disabled="disabledFields.isSpecial" clearable style="width: 100%">
<el-option label="否" value="0" />
<el-option label="紧急" value="1" />
<el-option label="单一" value="2" />
<el-option label="进口" value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.purchaseMode">
<el-form-item label="采购形式" prop="purchaseMode">
<el-input v-model="form.purchaseMode" placeholder="采购形式" :disabled="disabledFields.purchaseMode" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!hiddenFields.purchaseType">
<el-form-item label="采购方式" prop="purchaseType">
<el-input v-model="form.purchaseType" placeholder="采购方式" :disabled="disabledFields.purchaseType" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20" v-if="!hiddenFields.remark">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注" :disabled="disabledFields.remark" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template v-if="data.submitBtn">
<footer class="el-dialog__footer">
<span class="dialog-footer">
<el-button type="primary" @click="submitForm" :disabled="loading">提交</el-button>
</span>
</footer>
</template>
<template v-else>
<footer class="el-dialog__footer">
<span class="dialog-footer" />
</footer>
</template>
</div>
</template>
<script setup lang="ts" name="PurchaseApplyFlow">
import { useMessage } from '/@/hooks/message'
import * as purchaseApply from '/@/api/order/purchase-apply'
import * as orderVue from '/@/api/order/order-key-vue'
import { handleCustomFormPerm, handleFormPrint } from '/@/flow/utils/form-perm'
import { deepClone } from '/@/utils/other'
import { initCustomFormMethods } from '/@/views/order/index'
const emits = defineEmits(['handleJob'])
const dataFormRef = ref()
const loading = ref(false)
const operType = ref<'view' | 'flow'>('flow')
const props = defineProps({
currJob: { type: Object, default: null },
currElTab: { type: Object, default: () => ({}) }
})
const form = reactive<Record<string, any>>({
id: null,
code: '',
flowKey: 'PURCHASE_APPLY',
flowInstId: null,
purchaseNo: '',
projectName: '',
projectType: '',
projectContent: '',
applyDate: '',
fundSource: '',
budget: null,
isCentralized: '',
isSpecial: '',
purchaseMode: '',
purchaseSchool: '',
purchaseType: '',
categoryCode: '',
fileIds: [],
remark: '',
runJobId: '',
flowVarUser: null
})
const dataRules = ref({
projectContent: [{ required: true, message: '请输入采购内容', trigger: 'blur' }],
budget: [{ required: true, message: '请输入预算金额', trigger: 'blur' }]
})
const fieldsPerm = {
projectName: false,
projectType: false,
projectContent: false,
applyDate: false,
fundSource: false,
budget: false,
isCentralized: false,
isSpecial: false,
purchaseMode: false,
purchaseType: false,
remark: false
}
const hiddenFields = reactive({ ...fieldsPerm })
const disabledFields = reactive(deepClone(fieldsPerm))
const data = reactive({
submitBtn: true,
elTab: null as any
})
const methods = initCustomFormMethods(data, disabledFields, operType)
function initJobData() {
if (props.currJob?.orderId) handleGetObj(props.currJob.orderId)
}
function handleGetObj(id: string | number) {
purchaseApply.getObj(id).then(async (resp: any) => {
const formData = resp?.data ?? {}
Object.assign(form, formData)
form.runJobId = props.currJob?.id ?? ''
await initFormPermPrint()
})
}
async function initFormPermPrint() {
const elTab = orderVue.currElTabIsExist(props.currJob, props.currElTab?.id)
const res = await handleCustomFormPerm(props, hiddenFields, disabledFields, elTab)
await orderVue.currElTabIsView(methods, props.currJob, props.currElTab?.id, submitForm, res?.callback)
await handleFormPrint(form, elTab?.type, elTab?.id, '1')
data.elTab = elTab
}
async function submitForm() {
try {
loading.value = true
await purchaseApply.putObj(form)
orderVue.currElTabIsSave(props.currJob, props.currElTab?.id, true, emits)
useMessage().success(form.id ? '修改成功' : '保存成功')
} catch (err: any) {
useMessage().error(err?.msg ?? '操作失败')
} finally {
loading.value = false
}
}
watch(
() => props.currJob?.id,
() => { initJobData() }
)
onMounted(() => {
initJobData()
})
</script>
<style lang="scss" scoped>
.el-dialog__footer {
text-align: center;
.dialog-footer {
text-align: center;
}
}
</style>

View File

@@ -11,6 +11,26 @@
:rules="dataRules"
label-width="120px"
v-loading="loading">
<!-- 新增时先选取用户选后自动带出姓名工号 -->
<el-form-item label="选取用户" prop="userId">
<org-selector
v-model:orgList="userList"
type="user"
:multiple="false"
@update:orgList="handleUserChange" />
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input
v-model="form.name"
placeholder="选择用户后自动填充"
readonly />
</el-form-item>
<el-form-item label="用户工号" prop="username">
<el-input
v-model="form.username"
placeholder="选择用户后自动填充"
readonly />
</el-form-item>
<el-form-item label="部门" prop="deptId">
<org-selector
v-model:orgList="deptList"
@@ -21,26 +41,7 @@
<el-form-item label="部门名称" prop="deptName">
<el-input
v-model="form.deptName"
placeholder="选择部门后自动填充"
readonly />
</el-form-item>
<el-form-item label="分管负责人" prop="userId">
<org-selector
v-model:orgList="userList"
type="user"
:multiple="false"
@update:orgList="handleUserChange" />
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input
v-model="form.name"
placeholder="请选择用户后自动填充"
readonly />
</el-form-item>
<el-form-item label="用户工号" prop="username">
<el-input
v-model="form.username"
placeholder="请选择用户后自动填充"
placeholder="选择部门后自动填充"
readonly />
</el-form-item>
<el-form-item label="备注" prop="remark">
@@ -74,7 +75,7 @@ const emit = defineEmits(['refresh']);
const dataFormRef = ref();
const deptList = ref<any[]>([]);
const userList = ref<any[]>([]);
const dataForm = reactive({
const form = reactive({
id: '',
deptId: '',
deptName: '',
@@ -87,17 +88,17 @@ const visible = ref(false);
const loading = ref(false);
const dataRules = ref({
userId: [
{ required: true, message: '请选取用户(分管负责人)', trigger: 'change' }
],
deptId: [
{ required: true, message: '请选择部门', trigger: 'change' }
],
userId: [
{ required: true, message: '请选择分管负责人', trigger: 'change' }
],
name: [
{ required: true, message: '姓名不能为空', trigger: 'blur' }
{ required: true, message: '请先选取用户', trigger: 'blur' }
],
username: [
{ required: true, message: '用户工号不能为空', trigger: 'blur' }
{ required: true, message: '请先选取用户', trigger: 'blur' }
],
});
@@ -105,38 +106,38 @@ const dataRules = ref({
const handleDeptChange = (list: any[]) => {
if (list && list.length > 0) {
const dept = list[0];
dataForm.deptId = dept.deptId || dept.id || '';
dataForm.deptName = dept.name || dept.deptName || '';
form.deptId = dept.deptId || dept.id || '';
form.deptName = dept.name || dept.deptName || '';
} else {
dataForm.deptId = '';
dataForm.deptName = '';
form.deptId = '';
form.deptName = '';
}
};
// 处理用户选择变化
// 处理用户选择变化(选取用户后自动带出姓名、工号)
const handleUserChange = (list: any[]) => {
if (list && list.length > 0) {
const user = list[0];
dataForm.userId = user.userId || user.id || '';
dataForm.username = user.username || user.userName || '';
dataForm.name = user.name || user.realName || '';
form.userId = user.userId || user.id || '';
form.username = user.username || user.userName || '';
form.name = user.name || user.realName || '';
} else {
dataForm.userId = '';
dataForm.username = '';
dataForm.name = '';
form.userId = '';
form.username = '';
form.name = '';
}
};
// 打开弹窗
const openDialog = async (id?: string) => {
visible.value = true;
dataForm.id = '';
dataForm.deptId = '';
dataForm.deptName = '';
dataForm.userId = '';
dataForm.username = '';
dataForm.name = '';
dataForm.remark = '';
form.id = '';
form.deptId = '';
form.deptName = '';
form.userId = '';
form.username = '';
form.name = '';
form.remark = '';
deptList.value = [];
userList.value = [];
@@ -148,7 +149,7 @@ const openDialog = async (id?: string) => {
getObj({ id }).then((res: any) => {
if (res.data && res.data.length > 0) {
const data = res.data[0];
Object.assign(dataForm, {
Object.assign(form, {
id: data.id || '',
deptId: data.deptId || '',
deptName: data.deptName || '',
@@ -201,17 +202,17 @@ const onSubmit = async () => {
return false;
}
if (dataForm.id) {
await putObj(dataForm);
if (form.id) {
await putObj(form);
useMessage().success('编辑成功');
} else {
await addObj(dataForm);
await addObj(form);
useMessage().success('新增成功');
}
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg || (dataForm.id ? '编辑失败' : '新增失败'));
useMessage().error(err.msg || (form.id ? '编辑失败' : '新增失败'));
} finally {
loading.value = false;
}

View File

@@ -46,7 +46,7 @@
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
业务分管管理
业务分管部门及人员
</span>
<div class="header-actions">
<el-button
@@ -56,15 +56,15 @@
v-auth="'purchase_purchasingBusinessDept_add'">
新增
</el-button>
<el-button
plain
icon="UploadFilled"
type="primary"
class="ml10"
@click="excelUploadRef.show()"
v-auth="'purchase_purchasingBusinessDept_add'">
导入
</el-button>
<!-- <el-button -->
<!-- plain -->
<!-- icon="UploadFilled" -->
<!-- type="primary" -->
<!-- class="ml10" -->
<!-- @click="excelUploadRef.show()" -->
<!-- v-auth="'purchase_purchasingBusinessDept_add'">-->
<!-- 导入-->
<!-- </el-button>-->
<el-button
plain
:disabled="multiple"
@@ -134,14 +134,14 @@
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="150">
<template #default="scope">
<el-button
icon="Edit"
link
type="primary"
v-auth="'purchase_purchasingBusinessDept_edit'"
@click="formDialogRef.openDialog(scope.row.id)">
编辑
</el-button>
<!-- <el-button -->
<!-- icon="Edit" -->
<!-- link -->
<!-- type="primary" -->
<!-- v-auth="'purchase_purchasingBusinessDept_edit'"-->
<!-- @click="formDialogRef.openDialog(scope.row.id)">-->
<!-- 编辑-->
<!-- </el-button>-->
<el-button
icon="Delete"
link

View File

@@ -93,13 +93,13 @@
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="150">
<template #default="scope">
<el-button
icon="Edit"
link
type="primary"
@click="formDialogRef.openDialog('edit', scope.row)">
编辑
</el-button>
<!-- <el-button -->
<!-- icon="Edit" -->
<!-- link -->
<!-- type="primary" -->
<!-- @click="formDialogRef.openDialog('edit', scope.row)">-->
<!-- 编辑-->
<!-- </el-button>-->
<el-button
icon="Delete"
link

View File

@@ -0,0 +1,120 @@
<template>
<el-dialog v-model="visible" :title="title" width="600" append-to-body>
<div style="text-align: center; margin-bottom: 20px" v-if="currentType!='R10003'">
<el-button type="success" :icon="Download" @click="handleDownloadTemplate">下载模板</el-button>
</div>
<el-alert
v-if="currentType=='R10003'"
type="warning"
:closable="false"
show-icon
style="margin-bottom: 20px;">
<template #title>
<span> 请从中招平台导出数据后导入</span>
</template>
</el-alert>
<el-upload
ref="uploadRef"
class="upload-demo"
:action="uploadUrl"
:headers="headers"
:accept="'.xls,.xlsx'"
:on-success="handleUploadSuccess"
:on-error="handleAvatarError"
:limit="1"
drag
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">只能上传 .xls .xlsx 格式的 Excel 文件</div>
</template>
</el-upload>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue';
import { ElNotification } from 'element-plus';
import { Download, UploadFilled } from '@element-plus/icons-vue';
import { Session } from '/@/utils/storage';
import { downBlobFile } from '/@/utils/other';
const title = ref('');
// 响应式数据
const visible = ref(false);
// 计算属性
const headers = computed(() => {
return {
Authorization: 'Bearer ' + Session.getToken(),
TENANT_ID: Session.getTenant()
};
});
const uploadUrl = ref('')
const currentType = ref('')
const uploadRef = ref<{ clearFiles?: () => void }>()
const titleMap: Record<string, string> = {
R10001: '计划专业导入',
R10002: '地区分数导入',
R10003: '中招平台数据导入'
}
// 方法
const init = (type: any) => {
currentType.value = type
uploadUrl.value = '/api/recruit/file/importRecruitInfo?type=' + type
title.value = titleMap[type] || '信息导入'
visible.value = true
nextTick(() => {
uploadRef.value?.clearFiles()
})
}
// Emits
const emit = defineEmits<{
(e: 'refreshDataList'): void
}>()
const handleUploadSuccess = () => {
visible.value = false;
ElNotification({
title: '成功',
message: '导入成功',
type: 'success',
});
emit('refreshDataList')
};
const handleAvatarError = (err: any) => {
const result = JSON.parse(err.message);
if (result.code == '1') {
ElNotification.error({
title: '错误',
message: result.msg,
duration: 30000,
});
}
};
const handleDownloadTemplate = () => {
downBlobFile('/recruit/file/exportRecruitTemplate', { type: currentType.value || 'planMajor' }, title.value+'模板.xlsx')
}
// 暴露方法给父组件
defineExpose({
init
});
</script>
<style scoped lang="scss">
.upload-demo {
:deep(.el-upload-dragger) {
width: 100%;
}
}
</style>

View File

@@ -22,22 +22,12 @@
<el-form :model="queryForm" inline ref="searchFormRef">
<el-form-item label="招生计划" prop="groupId">
<el-select v-model="queryForm.groupId" filterable clearable placeholder="请选择招生计划">
<el-option
v-for="item in planList"
:key="item.id"
:label="item.groupName"
:value="item.id"
/>
<el-option v-for="item in planList" :key="item.id" :label="item.groupName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="学院" prop="deptCode">
<el-select v-model="queryForm.deptCode" filterable clearable placeholder="请选择">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode"
/>
<el-option v-for="item in deptList" :key="item.deptCode" :label="item.deptName" :value="item.deptCode" />
</el-select>
</el-form-item>
<el-form-item label="专业序号" prop="majorCode">
@@ -57,13 +47,16 @@
<!-- 操作按钮 -->
<div class="mb15">
<el-button v-if="hasAuth('recruit_recruitplanmajor_add')" type="primary" icon="FolderAdd" @click="addOrUpdateHandle"> </el-button>
<el-button
v-if="hasAuth('recruit_recruitplanmajor_add')"
v-auth="'recruit_major_import'"
type="primary"
icon="FolderAdd"
@click="addOrUpdateHandle"
>
plain
icon="UploadFilled"
:loading="exportLoading"
@click="handleImportDialog"
>导入信息
</el-button>
</div>
@@ -101,14 +94,7 @@
<el-table-column prop="sm" label="色盲不可录" align="center" width="120">
<template #default="scope">
<el-switch
v-model="scope.row.sm"
active-text=""
inactive-text=""
active-value="1"
inactive-value="0"
@change="changeSm(scope.row)"
/>
<el-switch v-model="scope.row.sm" active-text="是" inactive-text="否" active-value="1" inactive-value="0" @change="changeSm(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="stuworkMajorCode" label="正式专业代码" align="center" show-overflow-tooltip>
@@ -124,21 +110,8 @@
<!-- <el-table-column prop="sort" label="排序" align="center" show-overflow-tooltip /> -->
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="scope">
<el-button
type="primary"
link
icon="EditPen"
@click="addOrUpdateHandle(scope.row)"
>
修改
</el-button>
<el-button
v-if="hasAuth('recruit_recruitplanmajor_del')"
type="danger"
link
icon="Delete"
@click="deleteHandle(scope.row.id)"
>
<el-button type="primary" link icon="EditPen" @click="addOrUpdateHandle(scope.row)"> 修改 </el-button>
<el-button v-if="hasAuth('recruit_recruitplanmajor_del')" type="danger" link icon="Delete" @click="deleteHandle(scope.row.id)">
删除
</el-button>
</template>
@@ -146,47 +119,47 @@
</el-table>
<!-- 分页 -->
<pagination
v-bind="state.pagination"
@current-change="currentChangeHandle"
@size-change="sizeChangeHandle"
/>
<pagination v-bind="state.pagination" @current-change="currentChangeHandle" @size-change="sizeChangeHandle" />
<!-- 弹窗, 新增 / 修改 -->
<table-form ref="addOrUpdateRef" @refreshDataList="getDataList" />
<import-recruit-info ref="ImportRecruitInfoRef" @refreshDataList="getDataList"></import-recruit-info>
</div>
</div>
</template>
<script setup lang="ts" name="recruitplanmajor">
import { ref, reactive, onMounted, nextTick, defineAsyncComponent } from 'vue'
import { useAuth } from '/@/hooks/auth'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { useMessage, useMessageBox } from '/@/hooks/message'
import { useDict } from '/@/hooks/dict'
import { getList } from '/@/api/recruit/recruitstudentplangroup'
import { fetchList, delObj, editQuickField } from '/@/api/recruit/recruitstudentplan'
import { getDeptList } from '/@/api/basic/basicclass'
import { getMajorNameList } from '/@/api/basic/major'
import { ref, reactive, onMounted, nextTick, defineAsyncComponent } from 'vue';
import { useAuth } from '/@/hooks/auth';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useDict } from '/@/hooks/dict';
import { getList } from '/@/api/recruit/recruitstudentplangroup';
import { fetchList, delObj, editQuickField } from '/@/api/recruit/recruitstudentplan';
import { getDeptList } from '/@/api/basic/basicclass';
import { getMajorNameList } from '/@/api/basic/major';
const TableForm = defineAsyncComponent(() => import('./detaiform.vue'))
const { hasAuth } = useAuth()
const TableForm = defineAsyncComponent(() => import('./detaiform.vue'));
const ImportRecruitInfo = defineAsyncComponent(() => import('/@/views/recruit/common/import-recruit-info.vue'));
const ImportRecruitInfoRef=ref<any>();
const { hasAuth } = useAuth();
// 消息提示 hooks
const message = useMessage()
const messageBox = useMessageBox()
const message = useMessage();
const messageBox = useMessageBox();
// 表格引用
const tableRef = ref()
const searchFormRef = ref()
const addOrUpdateRef = ref()
const tableRef = ref();
const searchFormRef = ref();
const addOrUpdateRef = ref();
// 字典数据
const { yes_no_type } = useDict('yes_no_type')
const { yes_no_type } = useDict('yes_no_type');
// 数据
const planList = ref<any[]>([])
const deptList = ref<any[]>([])
const offcialZydmList = ref<any[]>([])
const planList = ref<any[]>([]);
const deptList = ref<any[]>([]);
const offcialZydmList = ref<any[]>([]);
// 查询表单
const queryForm = reactive({
@@ -194,125 +167,128 @@ const queryForm = reactive({
deptCode: '',
majorCode: '',
majorName: '',
learnYear: ''
})
learnYear: '',
});
// 获取计划名称
const getPlanName = (groupId: string) => {
const item = planList.value.find(item => item.id === groupId)
return item ? item.groupName : ''
}
const item = planList.value.find((item) => item.id === groupId);
return item ? item.groupName : '';
};
// 获取学院名称
const getDeptName = (deptCode: string) => {
const item = deptList.value.find(item => item.deptCode === deptCode)
return item ? item.deptName : ''
}
const item = deptList.value.find((item) => item.deptCode === deptCode);
return item ? item.deptName : '';
};
// 获取是/否标签
const getYesNoLabel = (value: string) => {
const item = yes_no_type.value.find((item: any) => item.value === value)
return item ? item.label : ''
}
const item = yes_no_type.value.find((item: any) => item.value === value);
return item ? item.label : '';
};
// 获取专业代码名称
const getMajorCodeName = (majorCode: string) => {
const item = offcialZydmList.value.find(item => item.majorCode === majorCode)
return item ? item.majorCodeAndName : ''
}
const item = offcialZydmList.value.find((item) => item.majorCode === majorCode);
return item ? item.majorCodeAndName : '';
};
// 表格状态
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: queryForm,
pageList: async (params: any) => {
const response = await fetchList(params)
const response = await fetchList(params);
return {
data: {
records: response.data.records,
total: response.data.total
}
}
total: response.data.total,
},
createdIsNeed: false
})
};
},
createdIsNeed: false,
});
// 使用 table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state)
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
// 初始化
const init = async () => {
try {
// 查询二级学院信息
const deptData = await getDeptList()
deptList.value = deptData.data || []
const deptData = await getDeptList();
deptList.value = deptData.data || [];
// 获取招生计划列表
const planData = await getList()
planList.value = planData.data || []
const planData = await getList();
planList.value = planData.data || [];
if (planList.value.length > 0) {
queryForm.groupId = planList.value[0].id
queryForm.groupId = planList.value[0].id;
}
// 获取专业名称列表
const majorData = await getMajorNameList()
offcialZydmList.value = majorData.data || []
const majorData = await getMajorNameList();
offcialZydmList.value = majorData.data || [];
getDataList()
getDataList();
} catch (error) {
console.log(error)
}
console.log(error);
}
};
// 修改开关
const changeSm = async (row: any) => {
try {
let parmas={id:row.id,sm:row.sm}
await editQuickField(parmas)
message.success('修改成功')
let parmas = { id: row.id, sm: row.sm };
await editQuickField(parmas);
message.success('修改成功');
} catch (error: any) {
console.log(error)
}
console.log(error);
}
};
// 新增 / 修改
const addOrUpdateHandle = (row?: any) => {
nextTick(() => {
addOrUpdateRef.value?.init(row.id || null)
})
}
addOrUpdateRef.value?.init(row.id || null);
});
};
// 删除
const deleteHandle = async (id: string) => {
try {
await messageBox.confirm('是否确认删除本条数据?请谨慎操作')
await delObj(id)
message.success('删除成功')
getDataList()
await messageBox.confirm('是否确认删除本条数据?请谨慎操作');
await delObj(id);
message.success('删除成功');
getDataList();
} catch {
// 用户取消
}
}
};
// 重置查询
const resetQuery = () => {
searchFormRef.value?.resetFields()
queryForm.groupId = ''
queryForm.deptCode = ''
queryForm.majorCode = ''
queryForm.majorName = ''
queryForm.learnYear = ''
searchFormRef.value?.resetFields();
queryForm.groupId = '';
queryForm.deptCode = '';
queryForm.majorCode = '';
queryForm.majorName = '';
queryForm.learnYear = '';
if (planList.value.length > 0) {
queryForm.groupId = planList.value[0].id
}
getDataList()
queryForm.groupId = planList.value[0].id;
}
getDataList();
};
const exportLoading = ref(false);
const handleImportDialog = () => {
ImportRecruitInfoRef.value?.init("R10001");
};
onMounted(() => {
init()
})
init();
});
</script>
<style lang="scss" scoped>
</style>
<style lang="scss" scoped></style>

View File

@@ -29,6 +29,15 @@
>
</el-button>
<el-button
v-auth="'recruit_areascore_import'"
type="primary"
plain
icon="UploadFilled"
:loading="exportLoading"
@click="handleImportDialog"
>导入信息
</el-button>
</div>
<!-- 表格 -->
@@ -83,6 +92,9 @@
<!-- 弹窗, 新增 / 修改 -->
<table-form ref="addOrUpdateRef" @refreshDataList="getDataList" />
<import-recruit-info ref="ImportRecruitInfoRef" @refreshDataList="getDataList"></import-recruit-info>
</div>
</div>
</template>
@@ -94,6 +106,8 @@ import { BasicTableProps, useTable } from '/@/hooks/table'
import { useMessage, useMessageBox } from '/@/hooks/message'
import { getList } from '/@/api/recruit/recruitstudentplangroup'
import { fetchList, delObj } from '/@/api/recruit/recruitstudentplancorrectscoreconfig'
const ImportRecruitInfo = defineAsyncComponent(() => import('/@/views/recruit/common/import-recruit-info.vue'));
const ImportRecruitInfoRef=ref<any>();
const TableForm = defineAsyncComponent(() => import('./detaiform.vue'))
const { hasAuth } = useAuth()
@@ -183,6 +197,12 @@ const resetQuery = () => {
getDataList()
}
const exportLoading = ref(false);
const handleImportDialog = () => {
ImportRecruitInfoRef.value?.init("R10002");
};
onMounted(() => {
init()
})

View File

@@ -281,19 +281,28 @@
@click="handleAddData">新增
</el-button>
<el-button
v-if="hasAuth('zipExport')"
type="warning"
v-auth="'recruit_zzpt_import'"
type="primary"
plain
icon="Download"
@click="downZip()">招生名单打包导出
</el-button>
<el-button
class="ml10"
type="warning"
plain
icon="Download"
@click="handleExport()">名单导出
icon="UploadFilled"
:loading="exportLoading"
@click="handleImportDialog"
>导入中招平台数据
</el-button>
<!-- <el-button-->
<!-- v-if="hasAuth('zipExport')"-->
<!-- type="warning"-->
<!-- plain-->
<!-- icon="Download"-->
<!-- @click="downZip()">招生名单打包导出-->
<!-- </el-button>-->
<!-- <el-button -->
<!-- class="ml10"-->
<!-- type="warning"-->
<!-- plain-->
<!-- icon="Download"-->
<!-- @click="handleExport()">名单导出-->
<!-- </el-button>-->
</div>
</el-row>
@@ -634,6 +643,8 @@
<AdmissionNoticeDialog ref="admissionNoticeDialogRef" @refresh="getDataList"></AdmissionNoticeDialog>
<InterviewForm ref="interviewFormRef" @refresh="getDataList"></InterviewForm>
<import-recruit-info ref="ImportRecruitInfoRef" @refreshDataList="getDataList"></import-recruit-info>
</div>
</div>
</template>
@@ -681,6 +692,8 @@ const InterviewForm = defineAsyncComponent(() => import('/@/views/recruit/recrui
const PayQrcodeDialog = defineAsyncComponent(() => import('./PayQrcodeDialog.vue'))
const AdmissionNoticeDialog = defineAsyncComponent(() => import('./AdmissionNoticeDialog.vue'))
const ActionDropdown = defineAsyncComponent(() => import('/@/components/tools/action-dropdown.vue'))
const ImportRecruitInfo = defineAsyncComponent(() => import('/@/views/recruit/common/import-recruit-info.vue'));
const ImportRecruitInfoRef=ref<any>();
const { hasAuth } = useAuth()
// 消息提示 hooks
const message = useMessage()
@@ -1131,6 +1144,10 @@ watch(() => dataForm.groupId, () => {
}
})
const handleImportDialog = () => {
ImportRecruitInfoRef.value?.init("R10003");
};
onMounted(() => {
init()
})

View File

@@ -4,25 +4,56 @@
v-model="visible"
:close-on-click-modal="false"
draggable
width="800px">
<div v-loading="loading" class="detail-container" v-if="detailData">
<el-descriptions :column="2" border>
width="960px">
<div class="detail-container">
<!-- 活动主信息来自列表行 -->
<el-descriptions v-if="mainInfo.activityTheme" :column="2" border class="mb16">
<el-descriptions-item label="活动主题" :span="2">
{{ detailData.activityTheme || '-' }}
{{ mainInfo.activityTheme || '-' }}
</el-descriptions-item>
<el-descriptions-item label="活动说明" :span="2">
{{ detailData.remarks || '-' }}
{{ mainInfo.remarks || '-' }}
</el-descriptions-item>
<el-descriptions-item label="活动兼报数">
{{ detailData.maxSub !== undefined && detailData.maxSub !== null ? detailData.maxSub : '-' }}
{{ mainInfo.maxSub !== undefined && mainInfo.maxSub !== null ? mainInfo.maxSub : '-' }}
</el-descriptions-item>
<el-descriptions-item label="开始时间">
{{ parseTime(detailData.startTime, '{y}-{m}-{d}') }}
{{ mainInfo.startTime ? parseTime(mainInfo.startTime, '{y}-{m}-{d}') : '-' }}
</el-descriptions-item>
<el-descriptions-item label="结束时间">
{{ parseTime(detailData.endTime, '{y}-{m}-{d}') }}
{{ mainInfo.endTime ? parseTime(mainInfo.endTime, '{y}-{m}-{d}') : '-' }}
</el-descriptions-item>
</el-descriptions>
<!-- 子项目列表接口getActivityInfoSubList -->
<div class="sub-title">子项目列表</div>
<el-table
:data="subList"
v-loading="loading"
border
size="small"
max-height="400"
style="width: 100%">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="subTitle" label="子项目名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="deptName" label="学院" width="110" show-overflow-tooltip />
<el-table-column prop="classNo" label="班号" width="80" align="center" />
<el-table-column prop="classMasterName" label="班主任" width="90" show-overflow-tooltip />
<el-table-column prop="startTime" label="开始时间" width="155" align="center">
<template #default="scope">
{{ scope.row.startTime ? parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}') : '-' }}
</template>
</el-table-column>
<el-table-column prop="endTime" label="结束时间" width="155" align="center">
<template #default="scope">
{{ scope.row.endTime ? parseTime(scope.row.endTime, '{y}-{m}-{d} {h}:{i}') : '-' }}
</template>
</el-table-column>
<el-table-column prop="maxNum" label="人数限制" width="88" align="center" />
<el-table-column prop="applyNums" label="已报名" width="78" align="center" />
<el-table-column prop="position" label="地点" min-width="100" show-overflow-tooltip />
<el-table-column prop="projectDescription" label="项目描述" min-width="180" show-overflow-tooltip />
</el-table>
<el-empty v-if="!loading && subList.length === 0" description="暂无子项目" :image-size="80" />
</div>
<template #footer>
<span class="dialog-footer">
@@ -33,47 +64,40 @@
</template>
<script setup lang="ts" name="ActivityInfoDetailDialog">
import { ref } from 'vue'
import { getDetail } from '/@/api/stuwork/activityinfo'
import { ref, reactive } from 'vue'
import { getActivityInfoSubList } from '/@/api/stuwork/activityinfosub'
import { parseTime } from '/@/utils/formatTime'
import { useMessage } from '/@/hooks/message'
// 定义变量内容
const visible = ref(false)
const loading = ref(false)
const detailData = ref<any>({})
const mainInfo = reactive<Record<string, any>>({})
const subList = ref<any[]>([])
// 打开弹窗
const openDialog = async (id: string) => {
/**
* 打开弹窗:使用接口 getActivityInfoSubList(activityInfoId) 获取详情子项目列表
* @param activityInfoId 活动信息ID
* @param row 列表行数据,用于展示活动主题等主信息
*/
const openDialog = async (activityInfoId: string, row?: any) => {
visible.value = true
loading.value = true
detailData.value = {}
subList.value = []
Object.keys(mainInfo).forEach((k) => delete mainInfo[k])
if (row && typeof row === 'object') {
Object.assign(mainInfo, row)
}
try {
const res = await getDetail(id)
if (res.data) {
// 根据接口文档,返回的数据可能是 { records: [...], total: ... } 格式
// 如果是列表格式,取第一条;如果是对象,直接使用
if (res.data.records && Array.isArray(res.data.records) && res.data.records.length > 0) {
detailData.value = res.data.records[0]
} else if (res.data.records && Array.isArray(res.data.records)) {
// 列表为空
useMessage().warning('未找到详情数据')
visible.value = false
} else {
// 直接是对象
detailData.value = res.data
}
}
} catch (err: any) {
useMessage().error(err.msg || '获取详情失败')
visible.value = false
const res = await getActivityInfoSubList(activityInfoId)
const data = res.data
subList.value = Array.isArray(data) ? data : []
} catch (_err) {
subList.value = []
} finally {
loading.value = false
}
}
// 暴露方法
defineExpose({
openDialog
})
@@ -81,7 +105,15 @@ defineExpose({
<style scoped lang="scss">
.detail-container {
padding: 20px 0;
padding: 8px 0;
}
.mb16 {
margin-bottom: 16px;
}
.sub-title {
margin-bottom: 8px;
font-weight: 600;
color: #303133;
}
</style>

View File

@@ -250,9 +250,9 @@ const {
tableStyle: _tableStyle
} = useTable(state)
// 查看详情
// 查看详情接口getActivityInfoSubList传入活动ID与行数据用于展示
const handleView = (row: any) => {
detailDialogRef.value?.openDialog(row.id)
detailDialogRef.value?.openDialog(row.id, row)
}
// 编辑

View File

@@ -48,6 +48,17 @@
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="默认扣分值" prop="score">
<el-input-number
v-model="form.score"
:min="0"
:max="999"
:precision="0"
placeholder="请输入默认扣分值"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="备注" prop="remarks">
<el-input
@@ -86,12 +97,13 @@ const loading = ref(false)
const operType = ref('add')
const categoryList = ref<any[]>([])
// 提交表单数据
// 提交表单数据与接口文档一致edit 含 score 默认扣分值)
const form = reactive({
id: '',
categortyId: '',
pointName: '',
standard: '',
score: undefined as number | undefined,
remarks: ''
})
@@ -117,6 +129,7 @@ const openDialog = async (type: string = 'add', row?: any) => {
form.categortyId = ''
form.pointName = ''
form.standard = ''
form.score = undefined
form.remarks = ''
// 编辑时填充数据
@@ -125,20 +138,21 @@ const openDialog = async (type: string = 'add', row?: any) => {
form.categortyId = row.categortyId || ''
form.pointName = row.pointName || ''
form.standard = row.standard || ''
form.score = row.score !== undefined && row.score !== null ? Number(row.score) : undefined
form.remarks = row.remarks || ''
// 如果需要获取详情
if (row.id && (!row.pointName || !row.standard)) {
// 如果需要获取详情(含 score
if (row.id && (!row.pointName || row.standard === undefined || row.score === undefined)) {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.categortyId = res.data.categortyId || ''
form.pointName = res.data.pointName || ''
form.standard = res.data.standard || ''
form.score = res.data.score !== undefined && res.data.score !== null ? Number(res.data.score) : undefined
form.remarks = res.data.remarks || ''
}
}).catch((err) => {
}).finally(() => {
}).catch(() => {}).finally(() => {
loading.value = false
})
}
@@ -155,10 +169,12 @@ const onSubmit = async () => {
loading.value = true
try {
// 与接口文档一致categortyId, pointName, standard, score, remarks
const submitData = {
categortyId: form.categortyId,
pointName: form.pointName,
standard: form.standard || '',
score: form.score !== undefined && form.score !== null ? Number(form.score) : 0,
remarks: form.remarks || ''
}

View File

@@ -17,6 +17,21 @@
:inline="true"
@keyup.enter="getDataList"
class="search-form">
<el-form-item label="考核项" prop="categortyId">
<el-select
v-model="state.queryForm.categortyId"
placeholder="请选择考核项"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in categoryList"
:key="item.id"
:label="item.category"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="指标名称" prop="pointName">
<el-input
v-model="state.queryForm.pointName"
@@ -155,9 +170,10 @@
</template>
<script setup lang="ts" name="AssessmentPoint">
import { reactive, ref } from 'vue'
import { reactive, ref, onMounted } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObj } from "/@/api/stuwork/assessmentpoint";
import { getList as getAssessmentCategoryList } from "/@/api/stuwork/assessmentcategory";
import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue'
@@ -169,6 +185,7 @@ const formDialogRef = ref()
const searchFormRef = ref()
const columnControlRef = ref()
const showSearch = ref(true)
const categoryList = ref<any[]>([])
// 表格列配置
const tableColumns = [
@@ -194,9 +211,10 @@ const tableStyle = {
headerCellStyle: { background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }
}
// 配置 useTable
// 配置 useTable(分页接口支持 categortyId、pointName与考核项对应
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
categortyId: '',
pointName: ''
},
pageList: fetchList,
@@ -215,13 +233,28 @@ const {
tableStyle: _tableStyle
} = useTable(state)
// 获取考核项列表与表单考核项对应接口assessmentcategory/list
const getCategoryList = async () => {
try {
const res = await getAssessmentCategoryList()
categoryList.value = res.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
categoryList.value = []
}
}
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.categortyId = ''
state.queryForm.pointName = ''
getDataList()
}
onMounted(() => {
getCategoryList()
})
// 编辑
const handleEdit = (row: any) => {
formDialogRef.value?.openDialog('edit', row)

View File

@@ -69,8 +69,7 @@
v-model="form.score"
:precision="0"
:step="1"
:min="0"
placeholder="请输入分数"
placeholder="请输入分数(可为负数)"
style="width: 100%" />
</el-form-item>
</el-col>

View File

@@ -12,6 +12,53 @@
label-width="100px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item label="学年" prop="schoolYear">
<el-select
v-model="form.schoolYear"
placeholder="请选择学年"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in schoolYearList"
:key="item.year"
:label="item.year"
:value="item.year" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="学期" prop="schoolTerm">
<el-select
v-model="form.schoolTerm"
placeholder="请选择学期"
clearable
style="width: 100%">
<el-option
v-for="item in schoolTermList"
:key="item.value"
:label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="班号" prop="classCode">
<el-select
v-model="form.classCode"
placeholder="请选择班号"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in classList"
:key="item.classCode"
:label="item.classNo"
:value="item.classCode" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="标题" prop="title">
<el-input
@@ -52,9 +99,12 @@
</template>
<script setup lang="ts" name="ClassPlanFormDialog">
import { ref, reactive, nextTick } from 'vue'
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, editObj, getDetail } from '/@/api/stuwork/classplan'
import { queryAllSchoolYear } from '/@/api/basic/basicyear'
import { getClassListByRole } from '/@/api/basic/basicclass'
import { getDicts } from '/@/api/admin/dict'
import Editor from '/@/components/Editor/index.vue'
const emit = defineEmits(['refresh'])
@@ -64,10 +114,16 @@ const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
const classList = ref<any[]>([])
// 提交表单数据
const form = reactive({
id: '',
schoolYear: '',
schoolTerm: '',
classCode: '',
title: '',
content: '',
remarks: ''
@@ -75,6 +131,15 @@ const form = reactive({
// 定义校验规则
const dataRules = {
schoolYear: [
{ required: true, message: '请选择学年', trigger: 'change' }
],
schoolTerm: [
{ required: true, message: '请选择学期', trigger: 'change' }
],
classCode: [
{ required: true, message: '请选择班号', trigger: 'change' }
],
title: [
{ required: true, message: '请输入标题', trigger: 'blur' }
],
@@ -92,6 +157,9 @@ const openDialog = async (type: string = 'add', row?: any) => {
nextTick(() => {
dataFormRef.value?.resetFields()
form.id = ''
form.schoolYear = ''
form.schoolTerm = ''
form.classCode = ''
form.title = ''
form.content = ''
form.remarks = ''
@@ -99,6 +167,9 @@ const openDialog = async (type: string = 'add', row?: any) => {
// 编辑时填充数据
if (type === 'edit' && row) {
form.id = row.id
form.schoolYear = row.schoolYear || ''
form.schoolTerm = row.schoolTerm || ''
form.classCode = row.classCode || ''
form.title = row.title || ''
form.content = row.content || ''
form.remarks = row.remarks || ''
@@ -108,6 +179,9 @@ const openDialog = async (type: string = 'add', row?: any) => {
loading.value = true
getDetail(row.id).then((res: any) => {
if (res.data) {
form.schoolYear = res.data.schoolYear || form.schoolYear
form.schoolTerm = res.data.schoolTerm || form.schoolTerm
form.classCode = res.data.classCode || form.classCode
form.content = res.data.content || ''
form.remarks = res.data.remarks || ''
}
@@ -119,6 +193,43 @@ const openDialog = async (type: string = 'add', row?: any) => {
})
}
// 学年列表(班级管理-学年接口)
const getSchoolYearList = async () => {
try {
const res = await queryAllSchoolYear()
schoolYearList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
schoolYearList.value = []
}
}
// 学期字典(系统通用)
const getSchoolTermDict = async () => {
try {
const res = await getDicts('school_term')
if (res?.data && Array.isArray(res.data)) {
schoolTermList.value = res.data.map((item: any) => ({
label: item.label ?? item.dictLabel ?? item.name,
value: item.value ?? item.dictValue ?? item.code
}))
} else {
schoolTermList.value = []
}
} catch (err) {
schoolTermList.value = []
}
}
// 班级列表(班级管理目录下)
const getClassListData = async () => {
try {
const res = await getClassListByRole()
classList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
classList.value = []
}
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
@@ -129,6 +240,9 @@ const onSubmit = async () => {
loading.value = true
try {
const submitData = {
schoolYear: form.schoolYear,
schoolTerm: form.schoolTerm,
classCode: form.classCode,
title: form.title,
content: form.content,
remarks: form.remarks
@@ -154,6 +268,13 @@ const onSubmit = async () => {
})
}
// 初始化:加载学年、学期、班级
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
getClassListData()
})
// 暴露方法
defineExpose({
openDialog

View File

@@ -79,7 +79,7 @@
<script setup lang="ts" name="ClassroomBaseArrangeDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { editObj } from '/@/api/stuwork/classroombase'
import { addClassRoomAssign } from '/@/api/stuwork/teachclassroomassign'
import { queryAllClass } from '/@/api/basic/basicclass'
import { getClassRoomList } from '/@/api/stuwork/teachclassroom'
import { getBuildingList } from '/@/api/stuwork/teachbuilding'
@@ -150,17 +150,16 @@ const onSubmit = async () => {
loading.value = true
try {
await editObj({
id: form.id,
await addClassRoomAssign({
buildingNo: form.buildingNo,
classCode: form.classCode,
position: form.position
position: form.position,
classCode: form.classCode
})
useMessage().success('教室安排成功')
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || '教室安排失败')
} catch (_err) {
// 错误由 request 拦截器统一提示
} finally {
loading.value = false
}

View File

@@ -0,0 +1,327 @@
<template>
<el-dialog
title="教室公物编辑"
v-model="visible"
:width="800"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="120px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item label="楼号" prop="buildingNo">
<el-input
v-model="form.buildingNo"
placeholder="楼号"
disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="学院" prop="deptName">
<el-input
v-model="form.deptName"
placeholder="学院"
disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="班级" prop="classCode">
<el-input
v-model="form.classNo"
placeholder="班级"
disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="教室位置" prop="position">
<el-input
v-model="form.position"
placeholder="教室位置"
disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="讲台类型" prop="platformType">
<el-select
v-model="form.platformType"
placeholder="请选择讲台类型"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in platformTypeList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="投影类型" prop="tyType">
<el-select
v-model="form.tyType"
placeholder="请选择投影类型"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in tyTypeList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="电视机" prop="tvType">
<el-select
v-model="form.tvType"
placeholder="请选择电视机"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in tvTypeList"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="方凳数量" prop="chairCnt">
<el-input-number
v-model="form.chairCnt"
:min="0"
placeholder="请输入方凳数量"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="课桌数量" prop="tableCnt">
<el-input-number
v-model="form.tableCnt"
:min="0"
placeholder="请输入课桌数量"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="备注" prop="remarks">
<el-input
v-model="form.remarks"
type="textarea"
:rows="3"
placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="ClassroomAssetsDialog">
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { editAssets } from '/@/api/stuwork/classassets'
import { getDicts } from '/@/api/admin/dict'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const platformTypeList = ref<any[]>([])
const tyTypeList = ref<any[]>([])
const tvTypeList = ref<any[]>([])
// 提交表单数据
const form = reactive({
id: '',
buildingNo: '',
deptName: '',
deptCode: '',
classCode: '',
classNo: '',
position: '',
platformType: '',
tyType: '',
tvType: '',
chairCnt: 0,
tableCnt: 0,
remarks: ''
})
// 定义校验规则
const dataRules = {
platformType: [
{ required: true, message: '请选择讲台类型', trigger: 'change' }
],
tyType: [
{ required: true, message: '请选择投影类型', trigger: 'change' }
],
tvType: [
{ required: true, message: '请选择电视机', trigger: 'change' }
],
chairCnt: [
{ required: true, message: '请填写方凳数量', trigger: 'blur' }
],
tableCnt: [
{ required: true, message: '请填写课桌数量', trigger: 'blur' }
]
}
// 打开弹窗
const openDialog = async (row: any) => {
visible.value = true
resetForm()
// 填充现有数据
if (row) {
form.id = row.id || ''
form.buildingNo = row.buildingNo || ''
form.deptName = row.deptName || ''
form.deptCode = row.deptCode || ''
form.classCode = row.classCode || ''
form.classNo = row.classNo || ''
form.position = row.position || ''
form.platformType = row.platformType || ''
form.tyType = row.tyType || ''
form.tvType = row.tvType || ''
form.chairCnt = row.chairCnt || 0
form.tableCnt = row.tableCnt || 0
form.remarks = row.remarks || ''
}
nextTick(() => {
dataFormRef.value?.clearValidate()
})
}
// 重置表单
const resetForm = () => {
form.id = ''
form.buildingNo = ''
form.deptName = ''
form.deptCode = ''
form.classCode = ''
form.classNo = ''
form.position = ''
form.platformType = ''
form.tyType = ''
form.tvType = ''
form.chairCnt = 0
form.tableCnt = 0
form.remarks = ''
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
const submitData = {
id: form.id,
buildingNo: form.buildingNo,
deptName: form.deptName,
deptCode: form.deptCode,
classCode: form.classCode,
classNo: form.classNo,
position: form.position,
platformType: form.platformType,
tyType: form.tyType,
tvType: form.tvType,
chairCnt: form.chairCnt,
tableCnt: form.tableCnt,
remarks: form.remarks
}
await editAssets(submitData)
useMessage().success('保存成功')
visible.value = false
emit('refresh')
} catch (_err) {
// 错误由 request 拦截器统一提示
} finally {
loading.value = false
}
})
}
// 获取讲台类型字典
const getPlatformTypeDict = async () => {
try {
const res = await getDicts('platform_type')
if (res.data) {
platformTypeList.value = Array.isArray(res.data) ? res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
})) : []
}
} catch (err) {
platformTypeList.value = []
}
}
// 获取投影类型字典
const getTyTypeDict = async () => {
try {
const res = await getDicts('ty_type')
if (res.data) {
tyTypeList.value = Array.isArray(res.data) ? res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
})) : []
}
} catch (err) {
tyTypeList.value = []
}
}
// 获取电视机类型字典
const getTvTypeDict = async () => {
try {
const res = await getDicts('tv_type')
if (res.data) {
tvTypeList.value = Array.isArray(res.data) ? res.data.map((item: any) => ({
label: item.label || item.dictLabel || item.name,
value: item.value || item.dictValue || item.code
})) : []
}
} catch (err) {
tvTypeList.value = []
}
}
// 初始化
onMounted(() => {
getPlatformTypeDict()
getTyTypeDict()
getTvTypeDict()
})
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
</style>

View File

@@ -181,19 +181,43 @@
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="120" align="center" fixed="right">
<el-table-column label="操作" width="380" align="center" fixed="right">
<template #header>
<el-icon><Setting /></el-icon>
<span style="margin-left: 4px">操作</span>
</template>
<template #default="scope">
<el-button
v-if="!scope.row.classCode || !scope.row.position"
icon="Setting"
link
type="primary"
@click="handleArrange(scope.row)">
教室安排
</el-button>
<el-button
icon="Close"
link
type="danger"
@click="handleCancelArrange(scope.row)">
取消教室安排
</el-button>
<el-button
icon="Collection"
link
type="success"
@click="handleAssets(scope.row)"
class="ml10">
教室公物
</el-button>
<el-button
icon="Lock"
link
type="warning"
@click="handlePassword(scope.row)"
class="ml10">
门锁密码
</el-button>
</template>
</el-table-column>
<template #empty>
@@ -213,6 +237,10 @@
<!-- 教室安排表单弹窗 -->
<arrange-dialog ref="arrangeDialogRef" @refresh="getDataList" />
<!-- 教室公物编辑弹窗 -->
<assets-dialog ref="assetsDialogRef" @refresh="getDataList" />
<!-- 门锁密码编辑弹窗 -->
<password-dialog ref="passwordDialogRef" @refresh="getDataList" />
</div>
</template>
@@ -226,7 +254,10 @@ import { getDicts } from "/@/api/admin/dict";
import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import ArrangeDialog from './arrange.vue'
import { List, OfficeBuilding, CircleCheck, Location, UserFilled, Collection, Setting, Menu, Calendar, Search, Document } from '@element-plus/icons-vue'
import AssetsDialog from './assets.vue'
import PasswordDialog from './password.vue'
import { delClassRoomAssign } from '/@/api/stuwork/teachclassroomassign'
import { List, OfficeBuilding, CircleCheck, Location, UserFilled, Collection, Setting, Menu, Calendar, Search, Document, Close, Lock } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
// 定义变量内容
@@ -240,6 +271,8 @@ const platformTypeList = ref<any[]>([])
const tyTypeList = ref<any[]>([])
const tvTypeList = ref<any[]>([])
const arrangeDialogRef = ref()
const assetsDialogRef = ref()
const passwordDialogRef = ref()
// 表格列配置
const tableColumns = [
@@ -416,6 +449,33 @@ const handleArrange = (row: any) => {
arrangeDialogRef.value?.openDialog(row)
}
// 取消教室安排
const handleCancelArrange = async (row: any) => {
const { confirm } = useMessageBox()
try {
await confirm('确定要取消教室安排吗?')
await delClassRoomAssign(row)
useMessage().success('取消成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
if (!err?._messageShown) {
useMessage().error(err?.msg || '取消失败')
}
}
}
}
// 教室公物
const handleAssets = (row: any) => {
assetsDialogRef.value?.openDialog(row)
}
// 门锁密码
const handlePassword = (row: any) => {
passwordDialogRef.value?.openDialog(row)
}
// 获取系部列表
const getDeptListData = async () => {
try {

View File

@@ -0,0 +1,156 @@
<template>
<el-dialog
title="门锁密码"
v-model="visible"
:width="500"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="120px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="楼号">
<el-input
v-model="form.buildingNo"
placeholder="楼号"
disabled />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="教室位置">
<el-input
v-model="form.position"
placeholder="教室位置"
disabled />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="门锁密码" prop="password">
<el-input
v-model="form.password"
type="password"
show-password
placeholder="请输入门锁密码"
clearable />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="ClassroomPasswordDialog">
import { ref, reactive, nextTick } from 'vue'
import { useMessage } from '/@/hooks/message'
import { editAssets } from '/@/api/stuwork/classassets'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
// 提交表单数据
const form = reactive({
id: '',
buildingNo: '',
deptName: '',
deptCode: '',
classCode: '',
classNo: '',
position: '',
password: ''
})
// 定义校验规则
const dataRules = {
password: [
{ required: true, message: '请输入门锁密码', trigger: 'blur' }
]
}
// 打开弹窗
const openDialog = async (row: any) => {
visible.value = true
resetForm()
// 填充现有数据
if (row) {
form.id = row.id || ''
form.buildingNo = row.buildingNo || ''
form.deptName = row.deptName || ''
form.deptCode = row.deptCode || ''
form.classCode = row.classCode || ''
form.classNo = row.classNo || ''
form.position = row.position || ''
form.password = row.password || ''
}
nextTick(() => {
dataFormRef.value?.clearValidate()
})
}
// 重置表单
const resetForm = () => {
form.id = ''
form.buildingNo = ''
form.deptName = ''
form.deptCode = ''
form.classCode = ''
form.classNo = ''
form.position = ''
form.password = ''
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
// 只提交必要的字段用于修改门锁密码
const submitData = {
id: form.id,
buildingNo: form.buildingNo,
deptName: form.deptName,
deptCode: form.deptCode,
classCode: form.classCode,
classNo: form.classNo,
position: form.position,
password: form.password
}
await editAssets(submitData)
useMessage().success('保存成功')
visible.value = false
emit('refresh')
} catch (_err) {
// 错误由 request 拦截器统一提示
} finally {
loading.value = false
}
})
}
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
</style>

View File

@@ -495,7 +495,9 @@ const confirmCheck = async () => {
checkDialogVisible.value = false
getDataList()
} catch (err: any) {
useMessage().error(err.msg || '考核失败')
if (!err?._messageShown) {
useMessage().error(err?.msg || '考核失败')
}
}
}
@@ -512,7 +514,9 @@ const handleDelete = async (ids: string[]) => {
getDataList()
useMessage().success('删除成功')
} catch (err: any) {
useMessage().error(err.msg || '删除失败')
if (!err?._messageShown) {
useMessage().error(err?.msg || '删除失败')
}
}
}

View File

@@ -45,40 +45,6 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="form.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="班级" prop="classNo">
<el-select
v-model="form.classNo"
placeholder="请选择班级"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in classList"
:key="item.classCode"
:label="item.classNo"
:value="item.classNo">
</el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
@@ -95,8 +61,6 @@ import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { initObj } from '/@/api/stuwork/classtheme'
import { queryAllSchoolYear } from '/@/api/basic/basicyear'
import { getClassListByRole } from '/@/api/basic/basicclass'
import { getDeptList } from '/@/api/basic/basicclass'
import { getDicts } from '/@/api/admin/dict'
const emit = defineEmits(['refresh'])
@@ -107,15 +71,11 @@ const visible = ref(false)
const loading = ref(false)
const schoolYearList = ref<any[]>([])
const schoolTermList = ref<any[]>([])
const deptList = ref<any[]>([])
const classList = ref<any[]>([])
// 提交表单数据
const form = reactive({
schoolYear: '',
schoolTerm: '',
deptCode: '',
classNo: ''
schoolTerm: ''
})
// 定义校验规则
@@ -125,12 +85,6 @@ const dataRules = {
],
schoolTerm: [
{ required: true, message: '请选择学期', trigger: 'change' }
],
deptCode: [
{ required: true, message: '请选择学院', trigger: 'change' }
],
classNo: [
{ required: true, message: '请选择班级', trigger: 'change' }
]
}
@@ -141,8 +95,6 @@ const openDialog = () => {
dataFormRef.value?.resetFields()
form.schoolYear = ''
form.schoolTerm = ''
form.deptCode = ''
form.classNo = ''
})
}
@@ -192,34 +144,10 @@ const getSchoolTermDict = async () => {
}
}
// 获取学院列表
const getDeptListData = async () => {
try {
const res = await getDeptList()
if (res.data && Array.isArray(res.data)) {
deptList.value = res.data
}
} catch (err) {
}
}
// 获取班级列表
const getClassListData = async () => {
try {
const res = await getClassListByRole()
if (res.data && Array.isArray(res.data)) {
classList.value = res.data
}
} catch (err) {
}
}
// 初始化
onMounted(() => {
getSchoolYearList()
getSchoolTermDict()
getDeptListData()
getClassListData()
})
// 暴露方法

View File

@@ -17,7 +17,19 @@
</el-select>
</el-form-item>
<el-form-item label="宿舍号" prop="roomNo">
<el-input v-model="form.roomNo" placeholder="请输入宿舍号" />
<el-select
v-model="form.roomNo"
placeholder="请选择宿舍号"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in roomList"
:key="item.roomNo"
:label="item.roomNo"
:value="item.roomNo">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="记录日期" prop="recordDate">
<el-date-picker
@@ -50,6 +62,7 @@ import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, putObj, getObj } from '/@/api/stuwork/dormhygienedaily'
import { getBuildingList } from '/@/api/stuwork/dormbuilding'
import { getRoomList } from '/@/api/stuwork/dormroom'
const emit = defineEmits(['refresh'])
@@ -59,6 +72,7 @@ const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
const buildingList = ref<any[]>([])
const roomList = ref<any[]>([])
// 提交表单数据
const form = reactive({
@@ -75,7 +89,7 @@ const dataRules = {
{ required: true, message: '请选择楼号', trigger: 'change' }
],
roomNo: [
{ required: true, message: '请输入宿舍号', trigger: 'blur' }
{ required: true, message: '请选择宿舍号', trigger: 'change' }
],
recordDate: [
{ required: true, message: '请选择记录日期', trigger: 'change' }
@@ -159,9 +173,22 @@ const getBuildingListData = async () => {
}
}
// 获取宿舍号列表
const getRoomListData = async () => {
try {
const res = await getRoomList()
if (res.data) {
roomList.value = Array.isArray(res.data) ? res.data : []
}
} catch (err) {
roomList.value = []
}
}
// 初始化
onMounted(() => {
getBuildingListData()
getRoomListData()
})
// 暴露方法给父组件

View File

@@ -2,7 +2,19 @@
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" :width="600" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="100px" v-loading="loading">
<el-form-item label="房间号" prop="roomNo">
<el-input v-model="form.roomNo" placeholder="请输入房间号" />
<el-select
v-model="form.roomNo"
placeholder="请选择房间号"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in roomList"
:key="item.roomNo"
:label="item.roomNo"
:value="item.roomNo">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="整改时间" prop="reformDate">
<el-date-picker
@@ -31,9 +43,10 @@
</template>
<script setup lang="ts" name="DormReformFormDialog">
import { ref, reactive, nextTick } from 'vue'
import { ref, reactive, nextTick, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj, putObj } from '/@/api/stuwork/dormreform'
import { getRoomList } from '/@/api/stuwork/dormroom'
const emit = defineEmits(['refresh'])
@@ -42,6 +55,7 @@ const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const operType = ref('add') // add 或 edit
const roomList = ref<any[]>([])
// 提交表单数据
const form = reactive({
@@ -54,7 +68,7 @@ const form = reactive({
// 定义校验规则
const dataRules = {
roomNo: [
{ required: true, message: '请输入房间号', trigger: 'blur' }
{ required: true, message: '请选择房间号', trigger: 'change' }
],
reformDate: [
{ required: true, message: '请选择整改时间', trigger: 'change' }
@@ -90,6 +104,23 @@ const openDialog = (type: string = 'add', row?: any) => {
})
}
// 获取房间号列表
const getRoomListData = async () => {
try {
const res = await getRoomList()
if (res.data) {
roomList.value = Array.isArray(res.data) ? res.data : []
}
} catch (err) {
roomList.value = []
}
}
// 初始化
onMounted(() => {
getRoomListData()
})
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return

View File

@@ -89,9 +89,11 @@
style="width: 100%">
<el-option
v-for="item in bedNoList"
:key="item"
:label="item"
:value="item">
:key="item.bedNo"
:value="item.bedNo">
<span :class="{ 'bed-option-occupied': item.haveStudent }">
{{ item.bedNo }}{{ item.haveStudent ? ' (有人)' : '' }}
</span>
</el-option>
</el-select>
</el-form-item>
@@ -138,7 +140,8 @@ const loading = ref(false)
const classList = ref<any[]>([])
const studentList = ref<any[]>([])
const roomList = ref<any[]>([])
const bedNoList = ref<string[]>([])
// 床位列表:支持 haveStudent 标记true=有人false=无人)
const bedNoList = ref<Array<{ bedNo: string; haveStudent: boolean }>>([])
// 提交表单数据
const form = reactive({
@@ -206,7 +209,7 @@ const handleStudentChange = (stuNo: string) => {
}
}
// 房间号变化时获取床位号列表
// 房间号变化时获取床位号列表(支持 haveStudenttrue=有人false=无人)
const handleRoomChange = async (roomNo: string) => {
if (!roomNo) {
bedNoList.value = []
@@ -214,20 +217,25 @@ const handleRoomChange = async (roomNo: string) => {
return
}
const toBedItem = (item: any): { bedNo: string; haveStudent: boolean } => {
if (typeof item === 'number' || typeof item === 'string') {
return { bedNo: String(item), haveStudent: false }
}
return {
bedNo: String(item?.bedNo ?? item?.value ?? item ?? ''),
haveStudent: !!(item && item.haveStudent === true)
}
}
try {
const res = await fearchRoomStuNum(roomNo)
if (res.data) {
// 根据返回的数据结构处理床位号列表
// 假设返回的是数字数组或对象数组
if (Array.isArray(res.data)) {
bedNoList.value = res.data.map((item: any) => {
if (typeof item === 'number' || typeof item === 'string') {
return String(item)
}
return String(item.bedNo || item.value || item)
})
bedNoList.value = res.data.map(toBedItem).filter((b) => b.bedNo)
} else if (res.data.bedNos && Array.isArray(res.data.bedNos)) {
bedNoList.value = res.data.bedNos.map((item: any) => String(item))
bedNoList.value = res.data.bedNos.map((item: any) =>
toBedItem(typeof item === 'object' ? item : { bedNo: String(item), haveStudent: false })
)
} else {
bedNoList.value = []
}
@@ -321,3 +329,8 @@ defineExpose({
})
</script>
<style scoped lang="scss">
.bed-option-occupied {
color: var(--el-color-warning);
}
</style>

View File

@@ -211,9 +211,13 @@
<el-icon><component :is="columnConfigMap[col.prop || '']?.icon || OfficeBuilding" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- 床位号列特殊模板 -->
<!-- 床位号列haveStudent true 时变色标记有人 -->
<template v-if="col.prop === 'bedNo'" #default="scope">
<el-tag v-if="scope.row.bedNo" size="small" type="info" effect="plain">
<el-tag
v-if="scope.row.bedNo"
size="small"
:type="scope.row.haveStudent ? 'warning' : 'info'"
effect="plain">
{{ scope.row.bedNo }}
</el-tag>
<span v-else>-</span>
@@ -274,6 +278,12 @@
<!-- 转宿弹窗 -->
<TransferDialog ref="transferDialogRef" @refresh="getDataList" />
<!-- 宿舍互换弹窗 -->
<SwapDialog ref="swapDialogRef" @refresh="getDataList" />
<!-- 打印宿舍卡弹窗 -->
<PrintCardDialog ref="printCardDialogRef" />
</div>
</template>
@@ -281,7 +291,7 @@
import { reactive, ref, onMounted, computed, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, delObjs } from "/@/api/stuwork/dormroomstudent";
import { fetchList, delObjs, exportEmptyPeopleRoomExcel } from "/@/api/stuwork/dormroomstudent";
import { getDeptList } from "/@/api/basic/basicclass";
import { getBuildingList } from "/@/api/stuwork/dormbuilding";
import { fetchDormRoomTreeList } from "/@/api/stuwork/dormroom";
@@ -289,6 +299,8 @@ import { useMessage, useMessageBox } from "/@/hooks/message";
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import FormDialog from './form.vue';
import TransferDialog from './transfer.vue';
import SwapDialog from './swap.vue';
import PrintCardDialog from './printCard.vue';
import TreeSelect from '/@/components/TreeSelect/index.vue';
import { List, OfficeBuilding, House, Grid, UserFilled, Phone, CreditCard, Avatar, User, Setting, Menu, Search, Document } from '@element-plus/icons-vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
@@ -301,6 +313,8 @@ const route = useRoute()
const formDialogRef = ref()
const columnControlRef = ref<any>()
const transferDialogRef = ref()
const swapDialogRef = ref()
const printCardDialogRef = ref()
const searchFormRef = ref()
const showSearch = ref(true)
const deptList = ref<any[]>([])
@@ -425,29 +439,91 @@ const handleDormDataTypeChange = (dormdataType: string) => {
getDormRoomTreeListData(dormdataType)
}
// 打印宿舍卡
// 打印宿舍卡(按房间号查询后打印)
const handlePrintCard = () => {
useMessage().warning('功能开发中')
printCardDialogRef.value?.openDialog()
}
// 宿舍互换
// 宿舍互换(两名学生互换宿舍)
const handleRoomSwap = () => {
useMessage().warning('功能开发中')
const query = {
deptCode: searchForm.deptCode,
buildingNo: searchForm.buildingNo,
gender: searchForm.gender,
roomNo: searchForm.roomNo || searchForm.roomNoInput,
classNo: searchForm.classNo,
stuNo: searchForm.stuNo,
realName: searchForm.realName
}
swapDialogRef.value?.openDialog(query)
}
// 导出
const handleExport = () => {
useMessage().warning('功能开发中')
// 导出:空 n 人宿舍导出(按当前筛选条件传参)
const handleExport = async () => {
try {
const params = {
deptCode: searchForm.deptCode,
buildingNo: searchForm.buildingNo,
gender: searchForm.gender,
dormdataType: searchForm.dormdataType,
roomNo: searchForm.roomNo || searchForm.roomNoInput,
classNo: searchForm.classNo,
stuNo: searchForm.stuNo,
realName: searchForm.realName
}
const res = await exportEmptyPeopleRoomExcel(params)
const blob = res instanceof Blob ? res : new Blob([res as BlobPart], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `空宿舍导出_${Date.now()}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
useMessage().success('导出成功')
} catch (err: any) {
useMessage().error(err?.msg || '导出失败')
}
}
// 名单导出
const handleExportList = () => {
useMessage().warning('功能开发中')
// 名单导出:与导出共用空 n 人宿舍导出接口,文件名区分
const handleExportList = async () => {
try {
const params = {
deptCode: searchForm.deptCode,
buildingNo: searchForm.buildingNo,
gender: searchForm.gender,
dormdataType: searchForm.dormdataType,
roomNo: searchForm.roomNo || searchForm.roomNoInput,
classNo: searchForm.classNo,
stuNo: searchForm.stuNo,
realName: searchForm.realName
}
const res = await exportEmptyPeopleRoomExcel(params)
const blob = res instanceof Blob ? res : new Blob([res as BlobPart], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `住宿学生名单_${Date.now()}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
useMessage().success('导出成功')
} catch (err: any) {
useMessage().error(err?.msg || '导出失败')
}
}
// 编辑(与转宿共用接口 edit修改房间/床位/是否舍长)
const handleEdit = (row: any) => {
transferDialogRef.value?.openDialog(row)
}
// 转宿
const handleTransfer = (row: any) => {
transferDialogRef.value.openDialog(row)
transferDialogRef.value?.openDialog(row)
}
// 退宿

View File

@@ -0,0 +1,152 @@
<template>
<el-dialog
title="打印宿舍卡"
v-model="visible"
:close-on-click-modal="false"
width="640px"
destroy-on-close
@closed="onClosed">
<div v-loading="loading">
<el-form :inline="true" class="mb16">
<el-form-item label="房间号">
<el-input v-model="roomNo" placeholder="请输入房间号" clearable style="width: 180px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleFetch">查询并预览</el-button>
</el-form-item>
</el-form>
<div v-if="printData.length" ref="printAreaRef" class="print-area">
<div v-for="(room, idx) in printData" :key="idx" class="room-card mb16">
<div class="room-title">房间号{{ room.roomNo }}</div>
<table class="print-table">
<thead>
<tr>
<th>姓名</th>
<th>学号</th>
<th>床位</th>
<th>是否舍长</th>
<th>班级</th>
</tr>
</thead>
<tbody>
<tr v-for="(s, i) in (room.dormRoomStudentVOList || [])" :key="i">
<td>{{ s.realName }}</td>
<td>{{ s.stuNo }}</td>
<td>{{ s.bedNo }}</td>
<td>{{ s.isLeader === '1' ? '是' : '否' }}</td>
<td>{{ s.className || s.classNo }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="handlePrint" :disabled="!printData.length"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="DormRoomStudentPrintCard">
import { ref } from 'vue'
import { useMessage } from '/@/hooks/message'
import { printDormRoomData } from '/@/api/stuwork/dormroomstudent'
const visible = ref(false)
const loading = ref(false)
const roomNo = ref('')
const printData = ref<any[]>([])
const printAreaRef = ref<HTMLElement>()
const openDialog = (initialRoomNo?: string) => {
visible.value = true
roomNo.value = initialRoomNo || ''
printData.value = []
}
const onClosed = () => {
printData.value = []
}
const handleFetch = async () => {
const no = (roomNo.value || '').trim()
if (!no) {
useMessage().warning('请输入房间号')
return
}
loading.value = true
try {
const res = await printDormRoomData(no)
const data = res?.data ?? res
printData.value = Array.isArray(data) ? data : (data ? [data] : [])
if (!printData.value.length) {
useMessage().info('该房间暂无数据')
}
} catch (err: any) {
useMessage().error(err?.msg || '查询失败')
printData.value = []
} finally {
loading.value = false
}
}
const handlePrint = () => {
if (!printData.value.length) return
const win = window.open('', '_blank')
if (!win) {
useMessage().error('无法打开打印窗口')
return
}
const el = printAreaRef.value
if (el) {
win.document.write(`
<!DOCTYPE html><html><head><meta charset="utf-8"><title>宿舍卡</title>
<style>
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #333; padding: 6px 8px; text-align: center; }
.room-title { font-weight: bold; margin-bottom: 8px; }
</style>
</head><body>${el.innerHTML}</body></html>
`)
win.document.close()
win.focus()
setTimeout(() => {
win.print()
win.close()
}, 300)
}
}
defineExpose({ openDialog })
</script>
<style scoped lang="scss">
.print-area {
max-height: 60vh;
overflow: auto;
}
.room-card {
padding: 12px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
}
.room-title {
margin-bottom: 8px;
}
.print-table {
width: 100%;
border-collapse: collapse;
th,
td {
border: 1px solid var(--el-border-color);
padding: 6px 8px;
text-align: center;
}
}
.mb16 {
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<el-dialog
title="宿舍互换"
v-model="visible"
:close-on-click-modal="false"
draggable
width="520px">
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
:validate-on-rule-change="false"
v-loading="loading">
<el-form-item label="学生一" prop="sourceSutNo">
<el-select
v-model="form.sourceSutNo"
placeholder="请选择学生(学号)"
clearable
filterable
style="width: 100%"
@change="onSourceChange">
<el-option
v-for="item in studentOptions"
:key="item.stuNo"
:label="`${item.realName}${item.stuNo}${item.roomNo || ''}`"
:value="item.stuNo" />
</el-select>
</el-form-item>
<el-form-item label="学生二" prop="targetStuNO">
<el-select
v-model="form.targetStuNO"
placeholder="请选择学生(学号)"
clearable
filterable
style="width: 100%"
@change="onTargetChange">
<el-option
v-for="item in studentOptions"
:key="item.stuNo"
:label="`${item.realName}${item.stuNo}${item.roomNo || ''}`"
:value="item.stuNo" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :loading="submitting"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="DormRoomStudentSwapDialog">
import { ref, reactive } from 'vue'
import { useMessage } from '/@/hooks/message'
import { exchangeRoom } from '/@/api/stuwork/dormroomstudent'
import { fetchList } from '/@/api/stuwork/dormroomstudent'
const emit = defineEmits(['refresh'])
const formRef = ref()
const visible = ref(false)
const loading = ref(false)
const submitting = ref(false)
const studentOptions = ref<any[]>([])
const form = reactive({
sourceSutNo: '',
targetStuNO: ''
})
const rules = {
sourceSutNo: [{ required: true, message: '请选择学生一', trigger: 'change' }],
targetStuNO: [{ required: true, message: '请选择学生二', trigger: 'change' }]
}
const onSourceChange = () => {
if (form.sourceSutNo && form.targetStuNO && form.sourceSutNo === form.targetStuNO) {
form.targetStuNO = ''
}
}
const onTargetChange = () => {
if (form.sourceSutNo && form.targetStuNO && form.sourceSutNo === form.targetStuNO) {
form.sourceSutNo = ''
}
}
const openDialog = async (queryParams?: any) => {
visible.value = true
form.sourceSutNo = ''
form.targetStuNO = ''
loading.value = true
try {
const res = await fetchList({
current: 1,
size: 500,
...queryParams
})
const list = res?.data?.records ?? res?.records ?? []
studentOptions.value = Array.isArray(list) ? list : []
} catch (err) {
studentOptions.value = []
} finally {
loading.value = false
}
}
const onSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
if (form.sourceSutNo === form.targetStuNO) {
useMessage().warning('请选择两名不同学生')
return
}
submitting.value = true
try {
await exchangeRoom({
sourceSutNo: form.sourceSutNo,
targetStuNO: form.targetStuNO
})
useMessage().success('互换成功')
visible.value = false
emit('refresh')
} catch (err: any) {
console.error(err)
} finally {
submitting.value = false
}
})
}
defineExpose({ openDialog })
</script>

View File

@@ -41,9 +41,11 @@
style="width: 100%">
<el-option
v-for="item in bedNoList"
:key="item"
:label="item"
:value="item">
:key="item.bedNo"
:value="item.bedNo">
<span :class="{ 'bed-option-occupied': item.haveStudent }">
{{ item.bedNo }}{{ item.haveStudent ? ' (有人)' : '' }}
</span>
</el-option>
</el-select>
</el-form-item>
@@ -95,7 +97,8 @@ const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const roomList = ref<any[]>([])
const bedNoList = ref<string[]>([])
// 床位列表:支持 haveStudent 标记true=有人false=无人)
const bedNoList = ref<Array<{ bedNo: string; haveStudent: boolean }>>([])
// 提交表单数据
const form = reactive({
@@ -119,7 +122,7 @@ const dataRules = {
]
}
// 房间号变化时获取床位号列表
// 房间号变化时获取床位号列表(支持 haveStudenttrue=有人false=无人)
const handleRoomChange = async (roomNo: string) => {
if (!roomNo) {
bedNoList.value = []
@@ -127,18 +130,25 @@ const handleRoomChange = async (roomNo: string) => {
return
}
const toBedItem = (item: any): { bedNo: string; haveStudent: boolean } => {
if (typeof item === 'number' || typeof item === 'string') {
return { bedNo: String(item), haveStudent: false }
}
return {
bedNo: String(item?.bedNo ?? item?.value ?? item ?? ''),
haveStudent: !!(item && item.haveStudent === true)
}
}
try {
const res = await fearchRoomStuNum(roomNo)
if (res.data) {
if (Array.isArray(res.data)) {
bedNoList.value = res.data.map((item: any) => {
if (typeof item === 'number' || typeof item === 'string') {
return String(item)
}
return String(item.bedNo || item.value || item)
})
bedNoList.value = res.data.map(toBedItem).filter((b) => b.bedNo)
} else if (res.data.bedNos && Array.isArray(res.data.bedNos)) {
bedNoList.value = res.data.bedNos.map((item: any) => String(item))
bedNoList.value = res.data.bedNos.map((item: any) =>
toBedItem(typeof item === 'object' ? item : { bedNo: String(item), haveStudent: false })
)
} else {
bedNoList.value = []
}
@@ -157,7 +167,7 @@ const openDialog = async (row: any) => {
visible.value = true
// 重置表单数据
nextTick(() => {
await nextTick()
dataFormRef.value?.resetFields()
form.id = row.id || ''
form.roomNo = row.roomNo || ''
@@ -166,11 +176,11 @@ const openDialog = async (row: any) => {
form.isLeader = row.isLeader || '0'
bedNoList.value = []
// 如果有房间号,取床位列表
// 如果有房间号,先拉取床位列表再回填床位号(避免 handleRoomChange 清空 bedNo
if (form.roomNo) {
handleRoomChange(form.roomNo)
await handleRoomChange(form.roomNo)
form.bedNo = row.bedNo || ''
}
})
}
// 提交表单
@@ -193,7 +203,8 @@ const onSubmit = async () => {
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg || '转宿失败')
// 统一交给全局拦截器处理错误提示,避免在这里重复弹出一次
console.error('转宿失败', err)
} finally {
loading.value = false
}
@@ -223,3 +234,8 @@ defineExpose({
})
</script>
<style scoped lang="scss">
.bed-option-occupied {
color: var(--el-color-warning);
}
</style>

View File

@@ -0,0 +1,216 @@
<template>
<el-dialog
title="新增宿舍点名"
v-model="visible"
:width="600"
:close-on-click-modal="false"
draggable>
<el-form
ref="dataFormRef"
:model="form"
:rules="dataRules"
label-width="100px"
v-loading="loading">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="点名类型" prop="type">
<el-select
v-model="form.type"
placeholder="请选择点名类型"
clearable
style="width: 100%">
<el-option label="普通住宿点名" value="1" />
<el-option label="留宿点名" value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="楼号" prop="buildId">
<el-select
v-model="form.buildId"
placeholder="请选择楼号"
clearable
filterable
style="width: 100%">
<el-option
v-for="item in buildingList"
:key="item.buildingNo"
:label="item.buildingNo"
:value="item.buildingNo" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="房间号" prop="roomNo">
<el-input
v-model="form.roomNo"
placeholder="请输入房间号"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="学生列表" prop="list">
<el-input
v-model="studentNosInput"
type="textarea"
:rows="5"
placeholder="请输入学号,多个学号用逗号或换行分隔"
style="width: 100%" />
<div class="form-tip">提示多个学号可用逗号或换行分隔</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="DormSignRecordFormDialog">
import { ref, reactive, nextTick, computed, onMounted } from 'vue'
import { useMessage } from '/@/hooks/message'
import { addObj } from '/@/api/stuwork/dormsignrecord'
import { getBuildingList } from '/@/api/stuwork/dormbuilding'
const emit = defineEmits(['refresh'])
// 定义变量内容
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const buildingList = ref<any[]>([])
// 提交表单数据
const form = reactive({
type: '',
buildId: '',
roomNo: '',
list: [] as Array<{ stuNo: string }>
})
// 学生学号输入(用于显示和编辑)
const studentNosInput = ref('')
// 定义校验规则
const dataRules = {
type: [
{ required: true, message: '请选择点名类型', trigger: 'change' }
],
buildId: [
{ required: true, message: '请选择楼号', trigger: 'change' }
],
roomNo: [
{ required: true, message: '请输入房间号', trigger: 'blur' }
],
list: [
{ required: true, message: '请输入至少一个学号', trigger: 'change' },
{
validator: (rule: any, value: any, callback: any) => {
if (!value || value.length === 0) {
callback(new Error('请输入至少一个学号'))
} else {
callback()
}
},
trigger: 'change'
}
]
}
// 打开弹窗
const openDialog = async () => {
visible.value = true
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields()
form.type = ''
form.buildId = ''
form.roomNo = ''
form.list = []
studentNosInput.value = ''
})
}
// 解析学号输入(支持逗号和换行分隔)
const parseStudentNos = (input: string): string[] => {
if (!input || !input.trim()) return []
return input
.split(/[,\n]/)
.map(s => s.trim())
.filter(s => s.length > 0)
}
// 提交表单
const onSubmit = async () => {
if (!dataFormRef.value) return
// 解析学号列表
const stuNos = parseStudentNos(studentNosInput.value)
if (stuNos.length === 0) {
useMessage().warning('请输入至少一个学号')
return
}
form.list = stuNos.map(stuNo => ({ stuNo }))
await dataFormRef.value.validate(async (valid: boolean) => {
if (!valid) return
loading.value = true
try {
await addObj({
type: form.type,
buildId: form.buildId,
roomNo: form.roomNo,
list: form.list
})
useMessage().success('新增成功')
visible.value = false
emit('refresh')
} catch (err: any) {
if (!err?._messageShown) {
useMessage().error(err?.msg || '新增失败')
}
} finally {
loading.value = false
}
})
}
// 获取楼号列表
const getBuildingListData = async () => {
try {
const res = await getBuildingList()
buildingList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
buildingList.value = []
}
}
// 初始化
onMounted(() => {
getBuildingListData()
})
// 暴露方法
defineExpose({
openDialog
})
</script>
<style scoped lang="scss">
.mb20 {
margin-bottom: 20px;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
</style>

View File

@@ -0,0 +1,367 @@
<template>
<div class="modern-page-container">
<div class="page-wrapper">
<!-- 搜索表单卡片 -->
<el-card v-show="showSearch" class="search-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Search /></el-icon>
筛选条件
</span>
</div>
</template>
<el-form
:model="state.queryForm"
ref="searchFormRef"
:inline="true"
@keyup.enter="getDataList"
class="search-form">
<el-form-item label="学院" prop="deptCode">
<el-select
v-model="state.queryForm.deptCode"
placeholder="请选择学院"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in deptList"
:key="item.deptCode"
:label="item.deptName"
:value="item.deptCode">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="楼号" prop="buildNo">
<el-select
v-model="state.queryForm.buildNo"
placeholder="请选择楼号"
clearable
filterable
style="width: 200px">
<el-option
v-for="item in buildingList"
:key="item.buildingNo"
:label="item.buildingNo"
:value="item.buildingNo">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="房间号" prop="roomNo">
<el-input
v-model="state.queryForm.roomNo"
placeholder="请输入房间号"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="学号" prop="stuNo">
<el-input
v-model="state.queryForm.stuNo"
placeholder="请输入学号"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item label="点名类型" prop="type">
<el-select
v-model="state.queryForm.type"
placeholder="请选择点名类型"
clearable
style="width: 200px">
<el-option label="普通住宿点名" value="1" />
<el-option label="留宿点名" value="2" />
</el-select>
</el-form-item>
<el-form-item label="考勤状态" prop="sign">
<el-select
v-model="state.queryForm.sign"
placeholder="请选择考勤状态"
clearable
style="width: 200px">
<el-option label="未到" :value="0" />
<el-option label="已到" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="考勤日期" prop="date">
<el-date-picker
v-model="state.queryForm.date"
type="date"
placeholder="请选择考勤日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Search" @click="getDataList">查询</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 内容卡片 -->
<el-card class="content-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="title-icon"><Document /></el-icon>
宿舍点名管理列表
</span>
<div class="header-actions">
<el-button
icon="FolderAdd"
type="primary"
@click="formDialogRef.openDialog()">
新增点名
</el-button>
<el-button
icon="RefreshLeft"
type="warning"
class="ml10"
@click="handleInit">
初始化
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
</div>
</div>
</template>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.loading"
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
class="modern-table">
<el-table-column type="index" label="序号" width="70" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
<template #default="{ $index }">
{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
</template>
</el-table-column>
<template v-for="col in visibleColumnsSorted" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '') && col.prop !== '操作'"
:prop="col.prop"
:label="col.label"
:width="col.width"
:min-width="col.minWidth"
:show-overflow-tooltip="col.showOverflowTooltip !== false"
:align="col.align || 'center'">
<template #header>
<el-icon v-if="col.icon"><component :is="col.icon" /></el-icon>
<span :style="{ marginLeft: col.icon ? '4px' : '0' }">{{ col.label }}</span>
</template>
<!-- 点名类型列特殊模板 -->
<template v-if="col.prop === 'type'" #default="scope">
<el-tag size="small" :type="scope.row.type === '1' ? 'primary' : 'success'" effect="plain">
{{ scope.row.type === '1' ? '普通住宿点名' : '留宿点名' }}
</el-tag>
</template>
<!-- 考勤状态列特殊模板 -->
<template v-else-if="col.prop === 'sign'" #default="scope">
<el-tag size="small" :type="scope.row.sign === '1' || scope.row.sign === 1 ? 'success' : 'danger'" effect="plain">
{{ scope.row.sign === '1' || scope.row.sign === 1 ? '已到' : '未到' }}
</el-tag>
</template>
<!-- 是否扫过脸列特殊模板 -->
<template v-else-if="col.prop === 'isFace'" #default="scope">
<el-tag size="small" :type="scope.row.isFace === '1' || scope.row.isFace === 1 ? 'success' : 'info'" effect="plain">
{{ scope.row.isFace === '1' || scope.row.isFace === 1 ? '是' : '否' }}
</el-tag>
</template>
<!-- 是否请假列特殊模板 -->
<template v-else-if="col.prop === 'isApply'" #default="scope">
<el-tag size="small" :type="scope.row.isApply === '1' || scope.row.isApply === 1 ? 'warning' : 'info'" effect="plain">
{{ scope.row.isApply === '1' || scope.row.isApply === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
</template>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
v-bind="state.pagination" />
</div>
</el-card>
</div>
<!-- 新增点名弹窗 -->
<form-dialog ref="formDialogRef" @refresh="getDataList" />
</div>
</template>
<script setup lang="ts" name="DormSignRecord">
import { reactive, ref, onMounted } from 'vue'
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList, initDormStuInfoForAttendance } from "/@/api/stuwork/dormsignrecord";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { getDeptList } from "/@/api/basic/basicclass";
import { getBuildingList } from "/@/api/stuwork/dormbuilding";
import FormDialog from './form.vue'
import { Search, Document, List, Menu, RefreshLeft, OfficeBuilding, Grid, User, CreditCard, House, Check, Calendar, UserFilled } from '@element-plus/icons-vue'
import RightToolbar from '/@/components/RightToolbar/index.vue'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import { useTableColumnControl } from '/@/hooks/tableColumn'
import { useRoute } from 'vue-router'
const route = useRoute()
const formDialogRef = ref()
const showSearch = ref(true)
const searchFormRef = ref()
const columnControlRef = ref<any>()
const deptList = ref<any[]>([])
const buildingList = ref<any[]>([])
// 表格列配置
const tableColumns = [
{ prop: 'deptName', label: '学院', minWidth: 120, icon: OfficeBuilding },
{ prop: 'className', label: '班级', minWidth: 120, icon: Grid },
{ prop: 'stuName', label: '姓名', width: 100, icon: User },
{ prop: 'stuNo', label: '学号', width: 120, icon: CreditCard },
{ prop: 'roomNo', label: '房间号', width: 100, icon: House },
{ prop: 'buildId', label: '楼号', width: 80, icon: OfficeBuilding },
{ prop: 'type', label: '点名类型', width: 120, icon: Document },
{ prop: 'sign', label: '考勤状态', width: 100, icon: Check },
{ prop: 'date', label: '考勤日期', width: 120, icon: Calendar },
{ prop: 'isFace', label: '是否扫过脸', width: 100, icon: UserFilled },
{ prop: 'isApply', label: '是否请假', width: 100, icon: Document }
]
// 使用表格列控制
const {
visibleColumns,
visibleColumnsSorted,
checkColumnVisible,
handleColumnChange,
handleColumnOrderChange,
loadSavedConfig
} = useTableColumnControl(tableColumns, { storageKey: route.path })
// 立即加载配置
loadSavedConfig()
// 配置 useTable
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
deptCode: '',
buildNo: '',
roomNo: '',
stuNo: '',
type: '',
sign: '',
date: ''
},
pageList: async (params: any) => {
const res = await fetchList(params)
const data = res?.data
return {
data: {
records: data?.records ?? [],
total: data?.total ?? 0
}
}
},
props: {
item: 'records',
totalCount: 'total'
},
createdIsNeed: true,
isPage: true
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state)
// 重置
const handleReset = () => {
searchFormRef.value?.resetFields()
state.queryForm.deptCode = ''
state.queryForm.buildNo = ''
state.queryForm.roomNo = ''
state.queryForm.stuNo = ''
state.queryForm.type = ''
state.queryForm.sign = ''
state.queryForm.date = ''
getDataList()
}
// 初始化
const handleInit = async () => {
const { confirm } = useMessageBox()
try {
await confirm('确定要初始化宿舍学生信息用于考勤吗?')
await initDormStuInfoForAttendance()
useMessage().success('初始化成功')
getDataList()
} catch (err: any) {
if (err !== 'cancel') {
if (!err?._messageShown) {
useMessage().error(err?.msg || '初始化失败')
}
}
}
}
// 获取学院列表
const getDeptListData = async () => {
try {
const res = await getDeptList()
deptList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
deptList.value = []
}
}
// 获取楼号列表
const getBuildingListData = async () => {
try {
const res = await getBuildingList()
buildingList.value = res?.data && Array.isArray(res.data) ? res.data : []
} catch (err) {
buildingList.value = []
}
}
// 初始化
onMounted(() => {
getDeptListData()
getBuildingListData()
})
</script>
<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

View File

@@ -184,11 +184,24 @@ const TimeRuleTableComponent = defineComponent({
default: ({ row }: any) => {
const timeList = getTimeList(row)
return h('div', { class: 'time-slots-container' },
timeList.map((timeSlot: any, index: number) =>
h('div', { key: index, class: 'time-slot-item' }, [
timeList.map((timeSlot: any, index: number) => {
// 通过数组索引更新,确保响应式
const updateStartTime = (val: string) => {
const list = getTimeList(row)
if (list[index]) {
list[index].startTime = val || ''
}
}
const updateEndTime = (val: string) => {
const list = getTimeList(row)
if (list[index]) {
list[index].endTime = val || ''
}
}
return h('div', { key: index, class: 'time-slot-item' }, [
h(ElTimePicker, {
modelValue: timeSlot.startTime,
'onUpdate:modelValue': (val: string) => { timeSlot.startTime = val },
modelValue: timeSlot.startTime || '',
'onUpdate:modelValue': updateStartTime,
format: 'HH:mm',
valueFormat: 'HH:mm',
placeholder: '开始时间',
@@ -196,8 +209,8 @@ const TimeRuleTableComponent = defineComponent({
}),
h('span', { style: { margin: '0 10px' } }, '至'),
h(ElTimePicker, {
modelValue: timeSlot.endTime,
'onUpdate:modelValue': (val: string) => { timeSlot.endTime = val },
modelValue: timeSlot.endTime || '',
'onUpdate:modelValue': updateEndTime,
format: 'HH:mm',
valueFormat: 'HH:mm',
placeholder: '结束时间',
@@ -213,7 +226,7 @@ const TimeRuleTableComponent = defineComponent({
style: { marginLeft: '10px' }
})
])
)
})
)
}
}),

Some files were not shown because too many files have changed in this diff Show More