解决API文档和路由配置合并冲突

This commit is contained in:
李旭明 committed 2025-08-04 11:19:52 +08:00
commit 79a96a7dfd
662 files changed
+127608 -205195

No files matched your search

+4 -4
View File
@@ -17,6 +17,8 @@
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"Slot": true,
"Slots": true,
"VNode": true,
"WritableComputedRef": true,
"acceptHMRUpdate": true,
@@ -35,6 +37,7 @@
"createInjectionState": true,
"createPinia": true,
"createReactiveFn": true,
"createRef": true,
"createReusableTemplate": true,
"createSharedComposable": true,
"createTemplatePromise": true,
@@ -315,9 +318,6 @@
"watchThrottled": true,
"watchTriggerable": true,
"watchWithFilter": true,
"whenever": true,
"Slot": true,
"Slots": true,
"createRef": true
"whenever": true
}
}
+2
View File
@@ -37,9 +37,11 @@
"element-plus": "2.9.8",
"file-saver": "2.0.5",
"highlight.js": "11.9.0",
"html2canvas": "^1.4.1",
"image-conversion": "2.1.1",
"js-cookie": "3.0.5",
"jsencrypt": "3.3.2",
"jspdf": "^3.0.1",
"nprogress": "0.2.0",
"pinia": "3.0.2",
"screenfull": "6.0.2",
+185
View File
@@ -0,0 +1,185 @@
/**
* 财务审批记录管理 - API服务
*
* @author ruoyi
* @date 2025-01-28
*/
import request from '@/utils/request';
import type { AxiosPromise } from 'axios';
// 审批记录查询参数
export interface CwApprovalRecordQuery {
pageNum?: number;
pageSize?: number;
applicationNo?: string;
projectName?: string;
applicantName?: string;
approvalStatus?: string;
approverName?: string;
currentApprover?: string;
deptId?: string;
submitTimeBegin?: string;
submitTimeEnd?: string;
needApproval?: boolean;
}
// 审批记录VO
export interface CwApprovalRecordVO {
id: string | number;
applicationId: string | number;
applicationNo: string;
projectName: string;
applicantName: string;
deptName: string;
totalInvestment: number;
currentApproverName: string;
approvalStatus: string;
submitTime: string;
approvalTime?: string;
approverName?: string;
approvalComment?: string;
approvalResult?: string;
}
// 审批表单
export interface CwApprovalForm {
applicationId?: string | number;
applicationIds?: Array<string | number>;
approvalResult: string;
approvalComment: string;
nextApprover?: string | number;
}
// 审批历史记录
export interface CwApprovalHistoryVO {
id: string | number;
applicationId: string | number;
approverName: string;
approvalResult: string;
approvalComment: string;
approvalTime: string;
stepName: string;
stepOrder: number;
}
// 查询审批记录列表
export const listApprovalRecord = (query: CwApprovalRecordQuery): AxiosPromise<CwApprovalRecordVO[]> => {
return request({
url: '/cw/approval/list',
method: 'get',
params: query
});
};
// 查询审批记录详情
export const getApprovalRecord = (id: string | number): AxiosPromise<CwApprovalRecordVO> => {
return request({
url: '/cw/approval/' + id,
method: 'get'
});
};
// 审批申报(单个)
export const approveApplication = (data: CwApprovalForm) => {
return request({
url: '/cw/approval/process',
method: 'post',
data: data
});
};
// 批量审批申报
export const batchApproveApplication = (data: CwApprovalForm) => {
return request({
url: '/cw/approval/batchApprove',
method: 'post',
data: data
});
};
// 获取审批历史记录
export const getApprovalHistory = (applicationId: string | number): AxiosPromise<CwApprovalHistoryVO[]> => {
return request({
url: '/cw/approval/byApplication/' + applicationId,
method: 'get'
});
};
// 获取当前用户待审批列表
export const getPendingApprovals = (query: CwApprovalRecordQuery): AxiosPromise<CwApprovalRecordVO[]> => {
return request({
url: '/cw/approval/pending',
method: 'get',
params: query
});
};
// 获取当前用户已审批列表
export const getMyApprovals = (query: CwApprovalRecordQuery): AxiosPromise<CwApprovalRecordVO[]> => {
return request({
url: '/cw/approval/my',
method: 'get',
params: query
});
};
// 获取部门审批列表
export const getDepartmentApprovals = (query: CwApprovalRecordQuery): AxiosPromise<CwApprovalRecordVO[]> => {
return request({
url: '/cw/approval/dept/pending',
method: 'get',
params: query
});
};
// 获取可选审批人列表
export const getAvailableApprovers = (applicationId: string | number): AxiosPromise<any[]> => {
return request({
url: '/cw/approval/approvers/' + applicationId,
method: 'get'
});
};
// 导出审批记录
export const exportApprovalRecord = (query: CwApprovalRecordQuery) => {
return request({
url: '/cw/approval/export',
method: 'post',
data: query
});
};
// 获取审批统计数据
export const getApprovalStatistics = (year?: number) => {
return request({
url: '/cw/approval/statistics',
method: 'get',
params: { year }
});
};
// 按部门统计审批数据
export const getApprovalStatsByDept = () => {
return request({
url: '/cw/approval/statsByDept',
method: 'get'
});
};
// 按月份统计审批数据
export const getApprovalStatsByMonth = (year?: number) => {
return request({
url: '/cw/approval/statsByMonth',
method: 'get',
params: { query: { year } }
});
};
// 获取审批效率统计
export const getApprovalEfficiency = () => {
return request({
url: '/cw/approval/efficiency',
method: 'get'
});
};
+89
View File
@@ -0,0 +1,89 @@
/**
* 绩效指标管理 - API服务
*
* @author ruoyi
* @date 2025-08-01
*/
import request from '@/utils/request';
import type { AxiosPromise } from 'axios';
// 绩效指标类型定义
export interface PerformanceIndicatorForm {
id?: number;
applicationId: number;
applicationNo: string;
indicatorType: string;
indicatorTypeName: string;
goalName: string;
targetValue: string;
weightPercentage: number;
description?: string;
unit?: string;
measurementMethod?: string;
dataSource?: string;
}
// 查询绩效指标列表
export const listPerformanceIndicator = (query: any): AxiosPromise<PerformanceIndicatorForm[]> => {
return request({
url: '/cw/performance/list',
method: 'get',
params: query
});
};
// 根据申报ID查询绩效指标
export const getPerformanceByApplicationId = (applicationId: number): AxiosPromise<PerformanceIndicatorForm[]> => {
return request({
url: `/cw/performance/application/${applicationId}`,
method: 'get'
});
};
// 批量保存绩效指标
export const batchSavePerformanceIndicator = (applicationId: number, applicationNo: string, data: PerformanceIndicatorForm[]) => {
return request({
url: '/cw/performance/batchSave',
method: 'post',
data: {
applicationId,
applicationNo,
indicators: data
}
});
};
// 新增绩效指标
export const addPerformanceIndicator = (data: PerformanceIndicatorForm) => {
return request({
url: '/cw/performance',
method: 'post',
data: data
});
};
// 修改绩效指标
export const updatePerformanceIndicator = (data: PerformanceIndicatorForm) => {
return request({
url: '/cw/performance',
method: 'put',
data: data
});
};
// 删除绩效指标
export const delPerformanceIndicator = (id: string | number) => {
return request({
url: '/cw/performance/' + id,
method: 'delete'
});
};
// 根据申报ID删除绩效指标
export const delPerformanceByApplicationId = (applicationId: number) => {
return request({
url: `/cw/performance/application/${applicationId}`,
method: 'delete'
});
};
+217
View File
@@ -0,0 +1,217 @@
/**
* 财务项目预算审批 - API服务
*
* @author ruoyi
* @date 2025-01-26
*/
import request from '@/utils/request';
import type { AxiosPromise } from 'axios';
import type {
CwProjectApplicationQuery,
CwProjectApplicationVO,
CwProjectApplicationForm,
CwApproverConfigQuery,
CwApproverConfigVO,
CwApproverConfigForm,
CwApplicationStatistics
} from '@/api/cw/types';
// 查询项目预算申报列表
export const listProjectApplication = (query: CwProjectApplicationQuery): AxiosPromise<CwProjectApplicationVO[]> => {
return request({
url: '/cw/application/list',
method: 'get',
params: query
});
};
// 查询项目预算申报详情
export const getProjectApplication = (id: string | number): AxiosPromise<CwProjectApplicationVO> => {
return request({
url: '/cw/application/' + id,
method: 'get'
});
};
// 新增项目预算申报
export const addProjectApplication = (data: CwProjectApplicationForm) => {
return request({
url: '/cw/application',
method: 'post',
data: data
});
};
// 修改项目预算申报
export const updateProjectApplication = (data: CwProjectApplicationForm) => {
return request({
url: '/cw/application',
method: 'put',
data: data
});
};
// 删除项目预算申报
export const delProjectApplication = (id: string | number | Array<string | number>) => {
return request({
url: '/cw/application/' + id,
method: 'delete'
});
};
// 保存草稿
export const saveDraft = (data: CwProjectApplicationForm) => {
return request({
url: '/cw/application/saveDraft',
method: 'post',
data: data
});
};
// 提交申报
export const submitApplication = (id: string | number) => {
return request({
url: '/cw/application/submit/' + id,
method: 'post'
});
};
// 撤回申报
export const withdrawApplication = (id: string | number) => {
return request({
url: '/cw/application/withdraw/' + id,
method: 'put'
});
};
// 取消申报
export const cancelApplication = (id: string | number) => {
return request({
url: '/cw/application/cancel/' + id,
method: 'put'
});
};
// 查询我的申报
export const getMyApplications = (query: CwProjectApplicationQuery): AxiosPromise<CwProjectApplicationVO[]> => {
return request({
url: '/cw/application/my',
method: 'get',
params: query
});
};
// 查询部门申报
export const getDepartmentApplications = (query: CwProjectApplicationQuery): AxiosPromise<CwProjectApplicationVO[]> => {
return request({
url: '/cw/application/dept',
method: 'get',
params: query
});
};
// 查询待处理申报
export const getPendingApplications = (query: CwProjectApplicationQuery): AxiosPromise<CwProjectApplicationVO[]> => {
return request({
url: '/cw/application/pending',
method: 'get',
params: query
});
};
// 导出项目预算申报
export const exportProjectApplication = (query: CwProjectApplicationQuery) => {
return request({
url: '/cw/application/export',
method: 'post',
data: query
});
};
// 获取申报统计数据
export const getApplicationStatistics = (year?: number): AxiosPromise<CwApplicationStatistics> => {
return request({
url: '/cw/application/statistics',
method: 'get',
params: { year }
});
};
// 申报数量统计
export const getApplicationCount = (): AxiosPromise<number> => {
return request({
url: '/cw/application/count',
method: 'get'
});
};
// 申报金额汇总
export const getApplicationAmountSum = (): AxiosPromise<number> => {
return request({
url: '/cw/application/amountSum',
method: 'get'
});
};
// 按部门统计
export const getStatisticsByDept = () => {
return request({
url: '/cw/application/statisticsByDept',
method: 'get'
});
};
// 按月份统计
export const getStatisticsByMonth = (year?: number) => {
return request({
url: '/cw/application/statisticsByMonth',
method: 'get',
params: { year }
});
};
// ==================== 审批人配置相关 API ====================
// 查询审批人配置列表
export const listApproverConfig = (query: CwApproverConfigQuery): AxiosPromise<CwApproverConfigVO[]> => {
return request({
url: '/cw/approverConfig/list',
method: 'get',
params: query
});
};
// 查询审批人配置详情
export const getApproverConfig = (id: string | number): AxiosPromise<CwApproverConfigVO> => {
return request({
url: '/cw/approverConfig/' + id,
method: 'get'
});
};
// 新增审批人配置
export const addApproverConfig = (data: CwApproverConfigForm) => {
return request({
url: '/cw/approverConfig',
method: 'post',
data: data
});
};
// 修改审批人配置
export const updateApproverConfig = (data: CwApproverConfigForm) => {
return request({
url: '/cw/approverConfig',
method: 'put',
data: data
});
};
// 删除审批人配置
export const delApproverConfig = (id: string | number | Array<string | number>) => {
return request({
url: '/cw/approverConfig/' + id,
method: 'delete'
});
};
+202
View File
@@ -0,0 +1,202 @@
/**
* 财务项目预算审批 - TypeScript类型定义
*
* @author ruoyi
* @date 2025-01-26
*/
// 项目预算申报查询对象
export interface CwProjectApplicationQuery {
pageNum?: number;
pageSize?: number;
applicationNo?: string;
projectName?: string;
principal?: string;
approvalStatus?: string;
departmentId?: number;
startDate?: string;
endDate?: string;
year?: number;
}
// 项目预算申报视图对象
export interface CwProjectApplicationVO {
id?: number;
applicationNo?: string;
projectName?: string;
principal?: string;
contactPhone?: string;
startDate?: string;
endDate?: string;
targetSme?: boolean;
targetMicroSme?: boolean;
importEquipment?: boolean;
applicationReason?: string;
constructionContent?: string;
totalPerformanceGoal?: string;
currentYearGoal?: string;
totalInvestment?: number;
totalMunicipalFiscal?: number;
totalSelfFunding?: number;
totalOtherFunding?: number;
currentYearTotal?: number;
currentYearMunicipal?: number;
currentYearSelf?: number;
currentYearOther?: number;
nextYearTotal?: number;
nextYearMunicipal?: number;
nextYearSelf?: number;
nextYearOther?: number;
futureYearTotal?: number;
futureYearMunicipal?: number;
futureYearSelf?: number;
futureYearOther?: number;
applicantId?: number;
applicantName?: string;
departmentId?: number;
departmentName?: string;
approvalStatus?: string;
submitTime?: string;
approveTime?: string;
year?: number;
createBy?: number;
createTime?: string;
updateBy?: number;
updateTime?: string;
remark?: string;
}
// 项目预算申报表单对象
export interface CwProjectApplicationForm {
id?: number;
applicationNo?: string;
projectName?: string;
principal?: string;
contactPhone?: string;
startDate?: string;
endDate?: string;
targetSme?: boolean;
targetMicroSme?: boolean;
importEquipment?: boolean;
applicationReason?: string;
constructionContent?: string;
totalPerformanceGoal?: string;
currentYearGoal?: string;
totalInvestment?: number;
totalMunicipalFiscal?: number;
totalSelfFunding?: number;
totalOtherFunding?: number;
currentYearTotal?: number;
currentYearMunicipal?: number;
currentYearSelf?: number;
currentYearOther?: number;
nextYearTotal?: number;
nextYearMunicipal?: number;
nextYearSelf?: number;
nextYearOther?: number;
futureYearTotal?: number;
futureYearMunicipal?: number;
futureYearSelf?: number;
futureYearOther?: number;
applicantId?: number;
applicantName?: string;
departmentId?: number;
departmentName?: string;
approvalStatus?: string;
year?: number;
remark?: string;
performanceIndicators?: any[]; // 绩效指标数据
}
// 审批人配置查询对象
export interface CwApproverConfigQuery {
pageNum?: number;
pageSize?: number;
configType?: string;
deptId?: number;
status?: string;
}
// 审批人配置视图对象
export interface CwApproverConfigVO {
id?: number;
configType?: string;
deptId?: number;
minAmount?: number;
maxAmount?: number;
deptApproverId?: number;
deptApproverName?: string;
financeApproverId?: number;
financeApproverName?: string;
leaderApproverId?: number;
leaderApproverName?: string;
deptDelegateId?: number;
deptDelegateName?: string;
financeDelegateId?: number;
financeDelegateName?: string;
leaderDelegateId?: number;
leaderDelegateName?: string;
status?: string;
remark?: string;
createTime?: string;
updateTime?: string;
}
// 审批人配置表单对象
export interface CwApproverConfigForm {
id?: number;
configType?: string;
deptId?: number;
minAmount?: number;
maxAmount?: number;
deptApproverId?: number;
deptApproverName?: string;
financeApproverId?: number;
financeApproverName?: string;
leaderApproverId?: number;
leaderApproverName?: string;
deptDelegateId?: number;
deptDelegateName?: string;
financeDelegateId?: number;
financeDelegateName?: string;
leaderDelegateId?: number;
leaderDelegateName?: string;
status?: string;
remark?: string;
}
// 项目申报统计对象
export interface CwApplicationStatistics {
totalCount: number;
approvingCount: number;
approvedCount: number;
rejectedCount: number;
totalAmount: number;
monthlyTrend: MonthlyTrend[];
deptStatistics: DeptStatistics[];
statusDistribution: StatusDistribution[];
}
// 部门统计对象
export interface DeptStatistics {
deptName: string;
totalCount: number;
totalAmount: number;
approvedCount: number;
approvedAmount: number;
}
// 月度趋势对象
export interface MonthlyTrend {
month: string;
count: number;
amount: number;
}
// 状态分布对象
export interface StatusDistribution {
status: string;
statusName: string;
count: number;
percentage: number;
}
+16 -4
View File
@@ -8,7 +8,10 @@ export function listHallReservation(query: HallReservationQuery) {
return request<TableDataInfo<HallReservationVO>>({
url: '/hq/hallReservation/list',
method: 'get',
params: query
params: query,
headers: {
isEncrypt: 'true'
}
});
}
@@ -16,7 +19,10 @@ export function listHallReservation(query: HallReservationQuery) {
export function getHallReservation(id: string | number) {
return request<R<HallReservationVO>>({
url: '/hq/hallReservation/' + id,
method: 'get'
method: 'get',
headers: {
isEncrypt: 'true'
}
});
}
@@ -25,7 +31,10 @@ export function auditHallReservation(data: HallReservationAuditForm) {
return request<R<any>>({
url: '/hq/hallReservation/audit',
method: 'put',
data: data
data: data,
headers: {
isEncrypt: 'true'
}
});
}
@@ -34,6 +43,9 @@ export function checkTimeConflict(data: TimeCheckParams) {
return request<R<TimeCheckResult>>({
url: '/hq/hallReservation/checkTimeConflict',
method: 'post',
data: data
data: data,
headers: {
isEncrypt: 'true'
}
});
}
+20 -5
View File
@@ -12,7 +12,10 @@ export const listHallInfo = (query?: HallInfoQuery): AxiosPromise<HallInfoVO[]>
return request({
url: '/hq/hallInfo/list',
method: 'get',
params: query
params: query,
headers: {
isEncrypt: 'true'
}
});
};
@@ -23,7 +26,10 @@ export const listHallInfo = (query?: HallInfoQuery): AxiosPromise<HallInfoVO[]>
export const getHallInfo = (id: string | number): AxiosPromise<HallInfoVO> => {
return request({
url: '/hq/hallInfo/' + id,
method: 'get'
method: 'get',
headers: {
isEncrypt: 'true'
}
});
};
@@ -35,7 +41,10 @@ export const addHallInfo = (data: HallInfoForm) => {
return request({
url: '/hq/hallInfo',
method: 'post',
data: data
data: data,
headers: {
isEncrypt: 'true'
}
});
};
@@ -47,7 +56,10 @@ export const updateHallInfo = (data: HallInfoForm) => {
return request({
url: '/hq/hallInfo',
method: 'put',
data: data
data: data,
headers: {
isEncrypt: 'true'
}
});
};
@@ -58,6 +70,9 @@ export const updateHallInfo = (data: HallInfoForm) => {
export const delHallInfo = (id: string | number | Array<string | number>) => {
return request({
url: '/hq/hallInfo/' + id,
method: 'delete'
method: 'delete',
headers: {
isEncrypt: 'true'
}
});
};
+20 -5
View File
@@ -12,7 +12,10 @@ export const listHallReservation = (query?: HallReservationQuery): AxiosPromise<
return request({
url: '/hq/hallReservation/list',
method: 'get',
params: query
params: query,
headers: {
isEncrypt: 'true'
}
});
};
@@ -23,7 +26,10 @@ export const listHallReservation = (query?: HallReservationQuery): AxiosPromise<
export const getHallReservation = (id: string | number): AxiosPromise<HallReservationVO> => {
return request({
url: '/hq/hallReservation/' + id,
method: 'get'
method: 'get',
headers: {
isEncrypt: 'true'
}
});
};
@@ -35,7 +41,10 @@ export const addHallReservation = (data: HallReservationForm) => {
return request({
url: '/hq/hallReservation',
method: 'post',
data: data
data: data,
headers: {
isEncrypt: 'true'
}
});
};
@@ -47,7 +56,10 @@ export const updateHallReservation = (data: HallReservationForm) => {
return request({
url: '/hq/hallReservation',
method: 'put',
data: data
data: data,
headers: {
isEncrypt: 'true'
}
});
};
@@ -58,6 +70,9 @@ export const updateHallReservation = (data: HallReservationForm) => {
export const delHallReservation = (id: string | number | Array<string | number>) => {
return request({
url: '/hq/hallReservation/' + id,
method: 'delete'
method: 'delete',
headers: {
isEncrypt: 'true'
}
});
};
-80
View File
@@ -1,80 +0,0 @@
/**
* 部门树结构
*/
export interface XgDeptTreeVO extends BaseEntity {
id: number | string;
label: string;
parentId: number | string;
weight: number;
children: XgDeptTreeVO[];
disabled: boolean;
}
// 学生查询参数
export interface StudentQuery {
pageNum: number;
pageSize: number;
studentName?: string;
studentNo?: string;
phonenumber?: string;
status?: string;
deptId?: number;
}
// 学生视图对象
export interface StudentVO {
studentId: number | string;
studentNo: string;
studentName: string;
nickName?: string;
sex?: string;
age?: number;
phonenumber?: string;
email?: string;
idCard?: string;
deptId?: number;
deptName?: string;
majorId?: number;
majorName?: string;
classId?: number;
className?: string;
address?: string;
status?: string;
createBy?: number;
createTime?: string;
updateBy?: number;
updateTime?: string;
remark?: string;
}
// 学生表单对象
export interface StudentForm {
studentId?: number | string;
deptId?: number;
studentNo: string;
studentName: string;
nickName?: string;
password?: string;
phonenumber?: string;
email?: string;
idCard?: string;
sex?: string;
status?: string;
majorId?: number;
classId?: number;
address?: string;
remark?: string;
}
// 统一响应类型
export interface R<T> {
code: number;
msg: string;
data: T;
}
// 表格数据信息类型
export interface TableDataInfo<T> {
total: number;
rows: T;
}
+63
View File
@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { SysRegionVO, SysRegionForm, SysRegionQuery } from '@/api/system/SysRegion/types';
/**
* 查询行政区划列表
* @param query
* @returns {*}
*/
export const listSysRegion = (query?: SysRegionQuery): AxiosPromise<SysRegionVO[]> => {
return request({
url: '/system/SysRegion/list',
method: 'get',
params: query
});
};
/**
* 查询行政区划详细
* @param id
*/
export const getSysRegion = (id: string | number): AxiosPromise<SysRegionVO> => {
return request({
url: '/system/SysRegion/' + id,
method: 'get'
});
};
/**
* 新增行政区划
* @param data
*/
export const addSysRegion = (data: SysRegionForm) => {
return request({
url: '/system/SysRegion',
method: 'post',
data: data
});
};
/**
* 修改行政区划
* @param data
*/
export const updateSysRegion = (data: SysRegionForm) => {
return request({
url: '/system/SysRegion',
method: 'put',
data: data
});
};
/**
* 删除行政区划
* @param id
*/
export const delSysRegion = (id: string | number | Array<string | number>) => {
return request({
url: '/system/SysRegion/' + id,
method: 'delete'
});
};
+191
View File
@@ -0,0 +1,191 @@
export interface SysRegionVO {
/**
* 主键ID
*/
id: string | number;
/**
* 行政区划编码
*/
code: string;
/**
* 行政区划名称
*/
name: string;
/**
* 行政区划简称
*/
shortName: string;
/**
* 父级行政区划编码,顶级为0
*/
parentCode: number;
/**
* 层级(1:省,2:市,3:区/县,4:街道/镇)
*/
level: number;
/**
* 排序
*/
sort: number;
/**
* 状态(0正常 1停用)
*/
status: string;
/**
* 备注
*/
remark: string;
/**
* 经度
*/
longitude: number;
/**
* 纬度
*/
latitude: number;
/**
* 逻辑删除标识(0未删除 1已删除)
*/
deleted: number;
/**
* 子节点列表(用于树形结构)
*/
children?: SysRegionVO[];
}
export interface SysRegionForm extends BaseEntity {
/**
* 主键ID
*/
id?: string | number;
/**
* 行政区划编码
*/
code?: string;
/**
* 行政区划名称
*/
name?: string;
/**
* 行政区划简称
*/
shortName?: string;
/**
* 父级行政区划编码,顶级为0
*/
parentCode?: number;
/**
* 层级(1:省,2:市,3:区/县,4:街道/镇)
*/
level?: number;
/**
* 排序
*/
sort?: number;
/**
* 状态(0正常 1停用)
*/
status?: string;
/**
* 备注
*/
remark?: string;
/**
* 经度
*/
longitude?: number;
/**
* 纬度
*/
latitude?: number;
/**
* 逻辑删除标识(0未删除 1已删除)
*/
deleted?: number;
}
export interface SysRegionQuery extends PageQuery {
/**
* 行政区划编码
*/
code?: string;
/**
* 行政区划名称
*/
name?: string;
/**
* 行政区划简称
*/
shortName?: string;
/**
* 父级行政区划编码,顶级为0
*/
parentCode?: number;
/**
* 层级(1:省,2:市,3:区/县,4:街道/镇)
*/
level?: number;
/**
* 排序
*/
sort?: number;
/**
* 状态(0正常 1停用)
*/
status?: string;
/**
* 经度
*/
longitude?: number;
/**
* 纬度
*/
latitude?: number;
/**
* 逻辑删除标识(0未删除 1已删除)
*/
deleted?: number;
/**
* 日期范围参数
*/
params?: any;
}
+124
View File
@@ -0,0 +1,124 @@
import request from '@/utils/request'
/**
* 重置bucket访问密码
* @param bucketName bucket名称
* @param newPassword 新密码
* @returns 操作结果
*/
export function resetBucketPassword(bucketName: string, newPassword: string) {
return request({
url: '/system/bucket-password/reset',
method: 'post',
params: { bucketName, newPassword }
})
}
/**
* 获取bucket密码信息
* @param bucketName bucket名称
* @returns bucket密码信息
*/
export function getBucketPasswordInfo(bucketName: string) {
return request({
url: `/system/bucket-password/info/${bucketName}`,
method: 'get'
})
}
/**
* 获取bucket访问记录
* @param bucketName bucket名称
* @param limit 限制数量,默认10条
* @returns 访问记录列表
*/
export function getBucketAccessRecords(bucketName: string, limit: number = 10) {
return request({
url: `/system/bucket-password/records/${bucketName}`,
method: 'get',
params: { limit }
})
}
/**
* 验证bucket密码
* @param bucketName bucket名称
* @param password 密码
* @returns 验证结果
*/
export function validateBucketPassword(bucketName: string, password: string) {
return request({
url: '/system/bucket-password/validate',
method: 'post',
params: { bucketName, password }
})
}
/**
* 生成强密码
* @param length 密码长度,默认12位
* @returns 生成的密码
*/
export function generateStrongPassword(length: number = 12): string {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'
let password = ''
for (let i = 0; i < length; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length))
}
return password
}
/**
* 验证密码强度
* @param password 密码
* @returns 密码强度评估
*/
export function validatePasswordStrength(password: string): {
isValid: boolean
score: number
suggestions: string[]
} {
const suggestions: string[] = []
let score = 0
// 长度检查
if (password.length >= 8) {
score += 1
} else {
suggestions.push('密码长度至少8位')
}
// 包含大写字母
if (/[A-Z]/.test(password)) {
score += 1
} else {
suggestions.push('包含大写字母')
}
// 包含小写字母
if (/[a-z]/.test(password)) {
score += 1
} else {
suggestions.push('包含小写字母')
}
// 包含数字
if (/\d/.test(password)) {
score += 1
} else {
suggestions.push('包含数字')
}
// 包含特殊字符
if (/[!@#$%^&*]/.test(password)) {
score += 1
} else {
suggestions.push('包含特殊字符')
}
return {
isValid: score >= 4,
score,
suggestions
}
}
+161
View File
@@ -0,0 +1,161 @@
import request from '@/utils/request'
/**
* 获取文件临时访问URL
* @param ossId 文件ID
* @param bucketPassword bucket访问密码
* @param expireSeconds 过期时间(秒),默认300秒
* @returns 临时访问URL
*/
export function getFileAccessUrl(ossId: number | string | bigint, bucketPassword?: string, expireSeconds: number = 300) {
return request({
url: `/resource/file/access/${ossId}`,
method: 'get',
params: {
bucketPassword,
expireSeconds
}
})
}
/**
* 批量获取文件临时访问URL
* @param ossIds 文件ID列表
* @param bucketPassword bucket访问密码
* @param expireSeconds 过期时间(秒),默认300秒
* @returns 文件ID与临时URL的映射
*/
export function getBatchFileAccessUrl(ossIds: number[], bucketPassword?: string, expireSeconds: number = 300) {
return request({
url: '/resource/file/access/batch',
method: 'post',
data: ossIds,
params: {
bucketPassword,
expireSeconds
}
})
}
/**
* 获取文件下载URL(带权限检查)
* @param ossId 文件ID
* @param bucketPassword bucket访问密码
* @param expireSeconds 过期时间(秒),默认300秒
* @returns 下载URL
*/
export function getFileDownloadUrl(ossId: number | string | bigint, bucketPassword?: string, expireSeconds: number = 300) {
return request({
url: `/resource/oss/download/${ossId}`,
method: 'get',
params: {
bucketPassword,
expireSeconds
}
})
}
/**
* 获取文件预览URL(带权限检查)
* @param ossId 文件ID
* @param bucketPassword bucket访问密码
* @param expireSeconds 过期时间(秒),默认300秒
* @returns 预览URL
*/
export function getFilePreviewUrl(ossId: number | string | bigint, bucketPassword?: string, expireSeconds: number = 300) {
return request({
url: `/resource/file/preview/${ossId}`,
method: 'get',
params: {
bucketPassword,
expireSeconds
}
})
}
/**
* 安全下载文件(带bucket密码验证)
* @param ossId 文件ID
* @param fileName 文件名
* @param bucketPassword bucket访问密码
* @returns Promise<void>
*/
export async function secureDownloadFile(ossId: number | string | bigint, fileName: string, bucketPassword?: string) {
try {
// 获取临时下载URL
const { data: downloadUrl } = await getFileDownloadUrl(ossId, bucketPassword)
// 使用fetch下载文件
const response = await fetch(downloadUrl)
if (!response.ok) {
throw new Error('下载失败')
}
const blob = await response.blob()
// 创建下载链接
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
// 释放内存
URL.revokeObjectURL(url)
return Promise.resolve()
} catch (error) {
console.error('文件下载失败:', error)
return Promise.reject(error)
}
}
/**
* 安全预览文件(带bucket密码验证)
* @param ossId 文件ID
* @param contentType 文件类型
* @param bucketPassword bucket访问密码
* @returns Promise<string> 预览URL
*/
export async function securePreviewFile(ossId: number | string | bigint, contentType?: string, bucketPassword?: string) {
try {
// 获取临时预览URL
const { data: previewUrl } = await getFilePreviewUrl(ossId, bucketPassword)
// 根据文件类型选择预览方式
if (contentType?.startsWith('image/')) {
// 图片直接返回URL用于预览
return Promise.resolve(previewUrl)
} else {
// 其他文件在新窗口打开
window.open(previewUrl, '_blank')
return Promise.resolve(previewUrl)
}
} catch (error) {
console.error('文件预览失败:', error)
return Promise.reject(error)
}
}
/**
* 获取bucket访问密码提示
* @param bucketName bucket名称
* @returns 密码提示信息
*/
export function getBucketPasswordHint(bucketName: string) {
// 这里可以根据bucket名称返回相应的密码提示
// 例如:从配置中获取密码提示信息
return `请输入 ${bucketName} bucket 的访问密码`
}
/**
* 验证bucket密码格式
* @param password 密码
* @returns 验证结果
*/
export function validateBucketPassword(password: string): boolean {
// 简单的密码格式验证
return password && password.length >= 6
}
+1
View File
@@ -48,6 +48,7 @@ export interface UserVO extends BaseEntity {
postIds: any;
roleId: any;
admin: boolean;
accountType: string;
}
/**
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,6 +1,6 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { DormBedVO, DormBedForm, DormBedQuery } from '@/api/xgss/dormBed/types';
import { DormBedVO, DormBedForm, DormBedQuery } from '@/api/xg/xgss/dormBed/types';
/**
* 查询宿舍床位信息列表
File renamed without changes.
@@ -1,6 +1,6 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { DormRoomVO, DormRoomForm, DormRoomQuery } from '@/api/xgss/dormRoom/types';
import { DormRoomVO, DormRoomForm, DormRoomQuery } from '@/api/xg/xgss/dormRoom/types';
/**
* 查询宿舍房间信息列表
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { StuColBatchVO, StuColBatchForm, StuColBatchQuery } from '@/api/xg/xsjcxx/XgStuColBatch/stuColBatch/types';
/**
* 查询学生信息收集批次列表
* @param query
* @returns {*}
*/
export const listStuColBatch = (query?: StuColBatchQuery): AxiosPromise<StuColBatchVO[]> => {
return request({
url: '/XgStuColBatch/stuColBatch/list',
method: 'get',
params: query
});
};
/**
* 查询学生信息收集批次详细
* @param id
*/
export const getStuColBatch = (id: string | number): AxiosPromise<StuColBatchVO> => {
return request({
url: '/XgStuColBatch/stuColBatch/' + id,
method: 'get'
});
};
/**
* 新增学生信息收集批次
* @param data
*/
export const addStuColBatch = (data: StuColBatchForm) => {
return request({
url: '/XgStuColBatch/stuColBatch',
method: 'post',
data: data
});
};
/**
* 修改学生信息收集批次
* @param data
*/
export const updateStuColBatch = (data: StuColBatchForm) => {
return request({
url: '/XgStuColBatch/stuColBatch',
method: 'put',
data: data
});
};
/**
* 删除学生信息收集批次
* @param id
*/
export const delStuColBatch = (id: string | number | Array<string | number>) => {
return request({
url: '/XgStuColBatch/stuColBatch/' + id,
method: 'delete'
});
};
@@ -0,0 +1,101 @@
export interface StuColBatchVO {
/**
* 批次ID
*/
id: string | number;
/**
* 批次名称
*/
batchName: string;
/**
* 批次描述
*/
description: string;
/**
* 开始时间
*/
startTime: string;
/**
* 结束时间
*/
endTime: string;
/**
* 状态(0正常 1停用)
*/
status: string;
/**
* 收集类型 1 - 新学期收集 2 - 其他
*/
collectionType: string;
}
export interface StuColBatchForm extends BaseEntity {
/**
* 批次ID
*/
id?: string | number;
/**
* 批次名称
*/
batchName?: string;
/**
* 批次描述
*/
description?: string;
/**
* 开始时间
*/
startTime?: string;
/**
* 结束时间
*/
endTime?: string;
/**
* 状态(0正常 1停用)
*/
status?: string;
/**
* 收集类型 1 - 新学期收集 2 - 其他
*/
collectionType?: string;
}
export interface StuColBatchQuery extends PageQuery {
/**
* 批次名称
*/
batchName?: string;
/**
* 状态(0正常 1停用)
*/
status?: string;
/**
* 收集类型 1 - 新学期收集 2 - 其他
*/
collectionType?: string;
/**
* 日期范围参数
*/
params?: any;
}
File renamed without changes.
@@ -152,6 +152,8 @@ export interface ClassMsgVO {
lastCountUpdate?: string;
/** 备注 */
remark?: string;
/** 学制 */
schoolSystem?: string;
}
/** 班级表单数据 */
@@ -0,0 +1,101 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { StuColBatchClassVO, StuColBatchClassForm, StuColBatchClassQuery } from '@/api/xg/xsjcxx/stuColBatchClass/stuColBatchClass/types';
/**
* 查询学生信息收集批次班级权限列表
* @param query
* @returns {*}
*/
export const listStuColBatchClass = (query?: StuColBatchClassQuery): AxiosPromise<StuColBatchClassVO[]> => {
return request({
url: '/stuColBatchClass/stuColBatchClass/list',
method: 'get',
params: query
});
};
/**
* 查询学生信息收集批次班级权限详细
* @param id
*/
export const getStuColBatchClass = (id: string | number): AxiosPromise<StuColBatchClassVO> => {
return request({
url: '/stuColBatchClass/stuColBatchClass/' + id,
method: 'get'
});
};
/**
* 新增学生信息收集批次班级权限
* @param data
*/
export const addStuColBatchClass = (data: StuColBatchClassForm) => {
return request({
url: '/stuColBatchClass/stuColBatchClass',
method: 'post',
data: data
});
};
/**
* 修改学生信息收集批次班级权限
* @param data
*/
export const updateStuColBatchClass = (data: StuColBatchClassForm) => {
return request({
url: '/stuColBatchClass/stuColBatchClass',
method: 'put',
data: data
});
};
/**
* 删除学生信息收集批次班级权限
* @param id
*/
export const delStuColBatchClass = (id: string | number | Array<string | number>) => {
return request({
url: '/stuColBatchClass/stuColBatchClass/' + id,
method: 'delete'
});
};
/**
* 设置学生信息收集批次班级权限
* @param batchId 批次ID
* @param deptIds 授权的班级ID列表
*/
export const setScope = (batchId: string | number, deptIds: Array<string | number>) => {
return request({
url: '/stuColBatchClass/stuColBatchClass/setScope',
method: 'post',
params: {
batchId,
deptIds
}
});
};
/**
* 获取学生信息收集批次班级权限
* @param batchId 批次ID
*/
export const getScope = (batchId: string | number) => {
return request({
url: '/stuColBatchClass/stuColBatchClass/getScope/' + batchId,
method: 'get'
});
};
/**
* 获取批次配置的权限范围详细信息
* @param batchId 批次ID
*/
export const getScopeDetail = (batchId: string | number) => {
return request({
url: '/stuColBatchClass/stuColBatchClass/getScopeDetail/' + batchId,
method: 'get'
});
};
@@ -0,0 +1,156 @@
export interface StuColBatchClassVO {
/**
* 主键ID
*/
id: string | number;
/**
* 批次ID
*/
batchId: string | number;
/**
* 班级ID
*/
classId: string | number;
/**
* 班级名称
*/
className: string;
/**
* 专业ID
*/
majorId: string | number;
/**
* 专业名称
*/
majorName: string;
/**
* 院系ID
*/
collegeId: string | number;
/**
* 院系名称
*/
collegeName: string;
/**
* 备注
*/
remark: string;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted: number;
}
export interface StuColBatchClassForm extends BaseEntity {
/**
* 主键ID
*/
id?: string | number;
/**
* 批次ID
*/
batchId?: string | number;
/**
* 班级ID
*/
classId?: string | number;
/**
* 班级名称
*/
className?: string;
/**
* 专业ID
*/
majorId?: string | number;
/**
* 专业名称
*/
majorName?: string;
/**
* 院系ID
*/
collegeId?: string | number;
/**
* 院系名称
*/
collegeName?: string;
/**
* 备注
*/
remark?: string;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted?: number;
}
export interface StuColBatchClassQuery extends PageQuery {
/**
* 批次ID
*/
batchId?: string | number;
/**
* 班级ID
*/
classId?: string | number;
/**
* 班级名称
*/
className?: string;
/**
* 专业ID
*/
majorId?: string | number;
/**
* 专业名称
*/
majorName?: string;
/**
* 院系ID
*/
collegeId?: string | number;
/**
* 院系名称
*/
collegeName?: string;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted?: number;
/**
* 日期范围参数
*/
params?: any;
}
@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { StuColBatchVO, StuColBatchForm, StuColBatchQuery } from '@/api/xg/xsjcxx/stuMsgAudit/types';
/**
* 查询学生信息收集批次列表
* @param query
* @returns {*}
*/
export const listStuColBatch = (query?: StuColBatchQuery): AxiosPromise<StuColBatchVO[]> => {
return request({
url: '/XgStuColBatch/stuColBatch/list',
method: 'get',
params: query
});
};
/**
* 查询学生信息收集批次详细
* @param id
*/
export const getStuColBatch = (id: string | number): AxiosPromise<StuColBatchVO> => {
return request({
url: '/XgStuColBatch/stuColBatch/' + id,
method: 'get'
});
};
/**
* 新增学生信息收集批次
* @param data
*/
export const addStuColBatch = (data: StuColBatchForm) => {
return request({
url: '/XgStuColBatch/stuColBatch',
method: 'post',
data: data
});
};
/**
* 修改学生信息收集批次
* @param data
*/
export const updateStuColBatch = (data: StuColBatchForm) => {
return request({
url: '/XgStuColBatch/stuColBatch',
method: 'put',
data: data
});
};
/**
* 删除学生信息收集批次
* @param id
*/
export const delStuColBatch = (id: string | number | Array<string | number>) => {
return request({
url: '/XgStuColBatch/stuColBatch/' + id,
method: 'delete'
});
};
+101
View File
@@ -0,0 +1,101 @@
export interface StuColBatchVO {
/**
* 批次ID
*/
id: string | number;
/**
* 批次名称
*/
batchName: string;
/**
* 批次描述
*/
description: string;
/**
* 开始时间
*/
startTime: string;
/**
* 结束时间
*/
endTime: string;
/**
* 状态(0正常 1停用)
*/
status: string;
/**
* 收集类型 1 - 新学期收集 2 - 其他
*/
collectionType: string;
}
export interface StuColBatchForm extends BaseEntity {
/**
* 批次ID
*/
id?: string | number;
/**
* 批次名称
*/
batchName?: string;
/**
* 批次描述
*/
description?: string;
/**
* 开始时间
*/
startTime?: string;
/**
* 结束时间
*/
endTime?: string;
/**
* 状态(0正常 1停用)
*/
status?: string;
/**
* 收集类型 1 - 新学期收集 2 - 其他
*/
collectionType?: string;
}
export interface StuColBatchQuery extends PageQuery {
/**
* 批次名称
*/
batchName?: string;
/**
* 状态(0正常 1停用)
*/
status?: string;
/**
* 收集类型 1 - 新学期收集 2 - 其他
*/
collectionType?: string;
/**
* 日期范围参数
*/
params?: any;
}
@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { StuColBatchVO, StuColBatchForm, StuColBatchQuery } from '@/api/xg/xsjcxx/stuMsgSupply/types';
/**
* 查询学生信息收集批次列表
* @param query
* @returns {*}
*/
export const listStuColBatch = (query?: StuColBatchQuery): AxiosPromise<StuColBatchVO[]> => {
return request({
url: '/XgStuColBatch/stuColBatch/list',
method: 'get',
params: query
});
};
/**
* 查询学生信息收集批次详细
* @param id
*/
export const getStuColBatch = (id: string | number): AxiosPromise<StuColBatchVO> => {
return request({
url: '/XgStuColBatch/stuColBatch/' + id,
method: 'get'
});
};
/**
* 新增学生信息收集批次
* @param data
*/
export const addStuColBatch = (data: StuColBatchForm) => {
return request({
url: '/XgStuColBatch/stuColBatch',
method: 'post',
data: data
});
};
/**
* 修改学生信息收集批次
* @param data
*/
export const updateStuColBatch = (data: StuColBatchForm) => {
return request({
url: '/XgStuColBatch/stuColBatch',
method: 'put',
data: data
});
};
/**
* 删除学生信息收集批次
* @param id
*/
export const delStuColBatch = (id: string | number | Array<string | number>) => {
return request({
url: '/XgStuColBatch/stuColBatch/' + id,
method: 'delete'
});
};
+101
View File
@@ -0,0 +1,101 @@
export interface StuColBatchVO {
/**
* 批次ID
*/
id: string | number;
/**
* 批次名称
*/
batchName: string;
/**
* 批次描述
*/
description: string;
/**
* 开始时间
*/
startTime: string;
/**
* 结束时间
*/
endTime: string;
/**
* 状态(0正常 1停用)
*/
status: string;
/**
* 收集类型 1 - 新学期收集 2 - 其他
*/
collectionType: string;
}
export interface StuColBatchForm extends BaseEntity {
/**
* 批次ID
*/
id?: string | number;
/**
* 批次名称
*/
batchName?: string;
/**
* 批次描述
*/
description?: string;
/**
* 开始时间
*/
startTime?: string;
/**
* 结束时间
*/
endTime?: string;
/**
* 状态(0正常 1停用)
*/
status?: string;
/**
* 收集类型 1 - 新学期收集 2 - 其他
*/
collectionType?: string;
}
export interface StuColBatchQuery extends PageQuery {
/**
* 批次名称
*/
batchName?: string;
/**
* 状态(0正常 1停用)
*/
status?: string;
/**
* 收集类型 1 - 新学期收集 2 - 其他
*/
collectionType?: string;
/**
* 日期范围参数
*/
params?: any;
}
+61
View File
@@ -0,0 +1,61 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import type { XgDeptTreeVO, R, TableDataInfo, XgStudentMsgAdvancedSearchQuery } from '@/api/xg/xsjcxx/stuRoster/types';
/**
* 查询部门下拉树结构
* @param deptId 部门ID
*/
export const XgDeptTreeSelect = (belongDeptId?: number): AxiosPromise<XgDeptTreeVO[]> => {
return request({
url: '/xgStudentManagerSource/studentManagerSource/currentUserAuthTree',
method: 'get'
});
};
// 查询院系树结构
export function deptTreeSelect(): AxiosPromise<R<any>> {
return request({
url: '/system/dept/treeselect',
method: 'get'
});
}
// 查询学生花名册列表 - 使用高级搜索接口
export function listStudentRoster(query?: XgStudentMsgAdvancedSearchQuery): AxiosPromise<R<TableDataInfo<any[]>>> {
return request({
url: '/studentMsg/studentMsg/advancedSearch',
method: 'post',
data: query
});
}
// 导出学生花名册
export function exportStudentRoster(query?: any): AxiosPromise<any> {
return request({
url: '/studentMsg/studentMsg/export',
method: 'get',
params: query,
responseType: 'blob'
});
}
/**
* 获取学生个人信息
* @param userId 用户ID,可为空,为空时使用当前登录用户ID
*/
export function getStudentPersonalInfo(userId?: number): AxiosPromise<any> {
return request({
url: '/studentMsg/studentMsg/personalInfo',
method: 'get',
params: { userId }
});
}
export default {
XgDeptTreeSelect,
deptTreeSelect,
listStudentRoster,
exportStudentRoster,
getStudentPersonalInfo
};
+187
View File
@@ -0,0 +1,187 @@
// 部门树节点类型
export interface XgDeptTreeVO {
id: number;
label: string;
children?: XgDeptTreeVO[];
disabled?: boolean;
}
// 通用响应类型
export interface R<T = any> {
code: number;
msg: string;
data: T;
}
// 分页数据信息
export interface TableDataInfo<T = any> {
total: number;
rows: T;
}
// 学生花名册查询参数
export interface StudentRosterQuery {
pageNum?: number;
pageSize?: number;
studentNo?: string;
name?: string;
phone?: string;
studentStatus?: string;
deptIds?: number[];
}
// 学生花名册数据
export interface StudentRosterVO {
studentId: number;
studentNo: string;
studentName: string;
nickName?: string;
sex: string;
age?: number;
phonenumber: string;
email?: string;
idCard: string;
deptId: number;
deptName: string;
majorId?: number;
majorName?: string;
classId?: number;
className?: string;
address?: string;
studentStatus: string;
createTime: string;
remark?: string;
}
// 学生信息高级搜索查询参数
export interface XgStudentMsgAdvancedSearchQuery {
// 基础信息检索
studentNo?: string;
name?: string;
idCard?: string;
phone?: string;
email?: string;
gender?: string[];
nationality?: string[];
politicalStatus?: string[];
birthDateStart?: string;
birthDateEnd?: string;
nameEnglish?: string;
formerName?: string;
nativePlace?: string[];
hukouLocation?: string;
homeAddress?: string;
currentAddress?: string;
qq?: string;
wechat?: string;
maritalStatus?: string[];
religion?: string;
householdRegisterType?: string[];
educationLevel?: string[];
// 学籍信息检索
studentStatus?: string[];
enrollmentYearStart?: number;
enrollmentYearEnd?: number;
enrollmentDateStart?: string;
enrollmentDateEnd?: string;
admissionCategory?: string[];
// 学院专业班级检索
collegeName?: string;
majorName?: string;
className?: string;
grade?: string[];
gradeMin?: string;
gradeMax?: string;
counselorName?: string;
classTeacherName?: string;
// 家庭信息检索
fatherName?: string;
motherName?: string;
guardianName?: string;
familyIncomeMin?: number;
familyIncomeMax?: number;
perCapitaIncomeMin?: number;
perCapitaIncomeMax?: number;
familyEconomicStatus?: string[];
familyMembersMin?: number;
familyMembersMax?: number;
// 特殊身份检索
isPoorStudent?: string[];
isMinority?: string[];
isDisabled?: string[];
isOrphan?: string[];
isLeftBehindChild?: string[];
isSingleChild?: string[];
isLowIncome?: string[];
hasLoan?: string[];
loanAmountMin?: number;
loanAmountMax?: number;
// 身体状况检索
heightMin?: number;
heightMax?: number;
weightMin?: number;
weightMax?: number;
bloodType?: string[];
healthStatus?: string;
disabilityType?: string[];
disabilityLevel?: string;
// 住宿信息检索
isBoarder?: string[];
dormitoryBuilding?: string[];
dormitoryRoom?: string;
// 家庭详细信息检索
fatherPhone?: string;
fatherWorkUnit?: string;
motherPhone?: string;
motherWorkUnit?: string;
guardianPhone?: string;
guardianRelation?: string;
incomeSource?: string;
emergencyContactName?: string;
emergencyContactPhone?: string;
emergencyContactRelation?: string;
// 扩展信息检索
specialties?: string;
awards?: string;
violations?: string;
socialActivities?: string;
// 健康详细信息检索
specialCondition?: string;
medicalHistory?: string;
allergies?: string;
// 其他信息检索
sourceRegion?: string[];
graduateSchool?: string;
trainingType?: string[];
academicLevel?: string[];
schoolSystemMin?: number;
schoolSystemMax?: number;
infoStatus?: string;
dataSource?: string;
isComplete?: number;
completionRateMin?: number;
completionRateMax?: number;
// 时间范围检索
createTimeStart?: string;
createTimeEnd?: string;
updateTimeStart?: string;
updateTimeEnd?: string;
// 部门树节点
deptIds?: number[];
// 分页参数
pageNum?: number;
pageSize?: number;
}
@@ -9,12 +9,17 @@ import { StudentQuery, StudentVO, StudentForm, XgDeptTreeVO, R, TableDataInfo }
*/
export const XgDeptTreeSelect = (belongDeptId?: number): AxiosPromise<XgDeptTreeVO[]> => {
// 实际项目中连接后端接口
// return request({
// url: '/system/user/deptTree',
// method: 'get',
// params: {
// belongDeptId
// }
// });
return request({
url: '/system/user/deptTree',
method: 'get',
params: {
belongDeptId
}
url: '/xgStudentManagerSource/studentManagerSource/currentUserAuthTree',
method: 'get'
});
// // 模拟数据,直接返回与页面组件期望格式一致的数据
@@ -182,6 +187,15 @@ export function exportStudent(query?: StudentQuery): AxiosPromise<any> {
}) as any;
}
// 下载导入模板
export function downloadImportTemplate(): AxiosPromise<any> {
return request({
url: '/studentMsg/studentMsg/importTemplate',
method: 'post',
responseType: 'blob'
});
}
// 学生状态修改
export function changeStudentStatus(studentId: string | number, status: string): AxiosPromise<R<any>> {
// 实际使用时连接后端接口
@@ -222,7 +236,7 @@ const mockStudentData: StudentVO[] = [
classId: 101,
className: '计算机2001班',
address: '北京市海淀区',
status: '0',
studentStatus: '1',
createTime: '2023-01-01 10:00:00',
remark: ''
},
@@ -243,7 +257,7 @@ const mockStudentData: StudentVO[] = [
classId: 101,
className: '计算机2001班',
address: '上海市浦东新区',
status: '0',
studentStatus: '2',
createTime: '2023-01-01 10:10:00',
remark: ''
},
@@ -264,7 +278,7 @@ const mockStudentData: StudentVO[] = [
classId: 201,
className: '软工2001班',
address: '广州市天河区',
status: '0',
studentStatus: '5',
createTime: '2023-01-02 09:00:00',
remark: ''
},
@@ -285,7 +299,7 @@ const mockStudentData: StudentVO[] = [
classId: 201,
className: '软工2001班',
address: '深圳市南山区',
status: '0',
studentStatus: '3',
createTime: '2023-01-02 10:30:00',
remark: ''
},
@@ -306,7 +320,7 @@ const mockStudentData: StudentVO[] = [
classId: 301,
className: '人工智能2001班',
address: '杭州市西湖区',
status: '1', // 停用状态
studentStatus: '4',
createTime: '2023-01-03 11:20:00',
remark: '休学中'
}
@@ -1,6 +1,6 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { StudentMsgVO, StudentMsgForm, StudentMsgQuery } from '@/api/studentMsg/studentMsg/types';
import { StudentMsgVO, StudentMsgForm, StudentMsgQuery } from '@/api/xg/xsjcxx/studentMsg/studentMsg/types';
/**
* 查询学生信息列表
@@ -61,3 +61,26 @@ export const delStudentMsg = (id: string | number | Array<string | number>) => {
method: 'delete'
});
};
/**
* 获取学生个人信息
* @param userId 用户ID,可为空,为空时使用当前登录用户ID
*/
export const getPersonalInfo = (userId?: number): AxiosPromise<StudentMsgForm> => {
return request({
url: '/studentMsg/studentMsg/personalInfo',
method: 'get',
params: { userId }
});
};
/**
* 下载导入模板
*/
export const downloadImportTemplate = () => {
return request({
url: '/studentMsg/studentMsg/importTemplate',
method: 'post',
responseType: 'blob'
});
};
@@ -35,7 +35,7 @@ export interface StudentMsgVO {
formerName: string;
/**
* 性别:M-男,F-女
* 性别:0-男,1-女
*/
gender: string;
@@ -417,7 +417,7 @@ export interface StudentMsgVO {
/**
* 学历层次 - 预留,数据从班级表关联
*/
academicLevel: string;
academicLevel: string | number;
/**
* 培养方式:全日制、在职、函授、夜大等 - 预留,数据从班级表关联
@@ -558,7 +558,7 @@ export interface StudentMsgForm extends BaseEntity {
formerName?: string;
/**
* 性别:M-男,F-女
* 性别:0-男,1-女
*/
gender?: string;
@@ -940,7 +940,7 @@ export interface StudentMsgForm extends BaseEntity {
/**
* 学历层次 - 预留,数据从班级表关联
*/
academicLevel?: string;
academicLevel?: string | number;
/**
* 培养方式:全日制、在职、函授、夜大等 - 预留,数据从班级表关联
@@ -1077,7 +1077,7 @@ export interface StudentMsgQuery extends PageQuery {
formerName?: string;
/**
* 性别:M-男,F-女
* 性别:0-男,1-女
*/
gender?: string;
@@ -1459,7 +1459,7 @@ export interface StudentMsgQuery extends PageQuery {
/**
* 学历层次 - 预留,数据从班级表关联
*/
academicLevel?: string;
academicLevel?: string | number;
/**
* 培养方式:全日制、在职、函授、夜大等 - 预留,数据从班级表关联
+178
View File
@@ -0,0 +1,178 @@
/**
* 部门树结构
*/
export interface XgDeptTreeVO extends BaseEntity {
id: number | string;
label: string;
parentId: number | string;
weight: number;
children: XgDeptTreeVO[];
disabled: boolean;
}
// 学生查询参数
export interface StudentQuery {
pageNum: number;
pageSize: number;
name?: string;
studentNo?: string;
phone?: string;
studentStatus?: string;
deptId?: number;
}
// 学生视图对象
export interface StudentVO {
id: number | string;
studentNo: string;
name: string;
nickName?: string;
gender?: string;
age?: number;
phone?: string;
email?: string;
idCard?: string;
collegeId?: number;
colegeName?: string;
majorId?: number;
majorName?: string;
classId?: number;
className?: string;
address?: string;
studentStatus?: string;
createBy?: number;
createTime?: string;
updateBy?: number;
updateTime?: string;
remark?: string;
}
// 学生表单对象
export interface StudentForm {
id?: number | string;
// 原有字段
studentId?: number | string;
deptId?: number;
studentNo: string;
studentName: string;
nickName?: string;
password?: string;
phonenumber?: string;
email?: string;
idCard?: string;
sex?: string;
status?: string;
majorId?: number;
classId?: number;
address?: string;
remark?: string;
// 基础信息字段
student_no?: string;
id_card?: string;
name?: string;
name_english?: string;
former_name?: string;
gender?: string;
birth_date?: string;
nationality?: string;
native_place?: string;
political_status?: string;
hukou_location?: string;
home_address?: string;
current_address?: string;
postal_code?: string;
photo_url?: string;
photo_path?: string;
phone?: string;
is_boarder?: number;
// 扩展信息字段
education_level?: string;
household_register_type?: string;
religion?: string;
source_region?: string;
graduate_school?: string;
qq?: string;
wechat?: string;
marital_status?: string;
emergency_contact_name?: string;
emergency_contact_phone?: string;
emergency_contact_relation?: string;
is_poor_student?: number;
is_minority?: number;
is_disabled?: number;
is_orphan?: number;
is_left_behind_child?: number;
is_single_child?: number;
is_low_income?: number;
has_loan?: number;
loan_amount?: string;
specialties?: string;
awards?: string;
violations?: string;
social_activities?: string;
// 家庭信息字段
father_name?: string;
father_phone?: string;
father_work_unit?: string;
mother_name?: string;
mother_phone?: string;
mother_work_unit?: string;
guardian_name?: string;
guardian_phone?: string;
guardian_relation?: string;
family_income?: string;
per_capita_income?: string;
income_source?: string;
family_economic_status?: string;
family_members?: string;
// 健康信息字段
height?: number | string;
weight?: number | string;
blood_type?: string;
health_status?: string;
special_condition?: string;
medical_history?: string;
allergies?: string;
disability_type?: string;
disability_level?: string;
// 学籍信息字段
admission_category?: string;
student_status?: string;
enrollment_year?: string;
enrollment_date?: string;
school_system?: string;
academic_level?: string | number;
training_type?: string;
major_name?: string;
class_name?: string;
grade?: string;
college_name?: string;
counselor_name?: string;
class_teacher_name?: string;
dormitory_building?: string;
dormitory_room?: string;
// 银行卡信息字段
bank_card_no?: string;
bank_name?: string;
bank_branch?: string;
card_holder_name?: string;
}
// 统一响应类型
export interface R<T> {
code: number;
msg: string;
data: T;
}
// 表格数据信息类型
export interface TableDataInfo<T> {
total: number;
rows: T;
}
@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { StudentMsgBatchVO, StudentMsgBatchForm, StudentMsgBatchQuery } from '@/api/xg/xsjcxx/studentMsgBatch/studentMsgBatch/types';
/**
* 查询学生信息批次收集列表
* @param query
* @returns {*}
*/
export const listStudentMsgBatch = (query?: StudentMsgBatchQuery): AxiosPromise<StudentMsgBatchVO[]> => {
return request({
url: '/studentMsgBatch/studentMsgBatch/list',
method: 'get',
params: query
});
};
/**
* 查询学生信息批次收集详细
* @param id
*/
export const getStudentMsgBatch = (id: string | number): AxiosPromise<StudentMsgBatchVO> => {
return request({
url: '/studentMsgBatch/studentMsgBatch/' + id,
method: 'get'
});
};
/**
* 新增学生信息批次收集
* @param data
*/
export const addStudentMsgBatch = (data: StudentMsgBatchForm) => {
return request({
url: '/studentMsgBatch/studentMsgBatch',
method: 'post',
data: data
});
};
/**
* 修改学生信息批次收集
* @param data
*/
export const updateStudentMsgBatch = (data: StudentMsgBatchForm) => {
return request({
url: '/studentMsgBatch/studentMsgBatch',
method: 'put',
data: data
});
};
/**
* 删除学生信息批次收集
* @param id
*/
export const delStudentMsgBatch = (id: string | number | Array<string | number>) => {
return request({
url: '/studentMsgBatch/studentMsgBatch/' + id,
method: 'delete'
});
};
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { XgClassPosDictVO, XgClassPosDictForm, XgClassPosDictQuery } from '@/api/xg/xsjcxx/xgClassPosDict/xgClassPosDict/types';
/**
* 查询班级干部职务配置列表
* @param query
* @returns {*}
*/
export const listXgClassPosDict = (query?: XgClassPosDictQuery): AxiosPromise<XgClassPosDictVO[]> => {
return request({
url: '/xgClassPosDict/xgClassPosDict/list',
method: 'get',
params: query
});
};
/**
* 查询班级干部职务配置详细
* @param id
*/
export const getXgClassPosDict = (id: string | number): AxiosPromise<XgClassPosDictVO> => {
return request({
url: '/xgClassPosDict/xgClassPosDict/' + id,
method: 'get'
});
};
/**
* 新增班级干部职务配置
* @param data
*/
export const addXgClassPosDict = (data: XgClassPosDictForm) => {
return request({
url: '/xgClassPosDict/xgClassPosDict',
method: 'post',
data: data
});
};
/**
* 修改班级干部职务配置
* @param data
*/
export const updateXgClassPosDict = (data: XgClassPosDictForm) => {
return request({
url: '/xgClassPosDict/xgClassPosDict',
method: 'put',
data: data
});
};
/**
* 删除班级干部职务配置
* @param id
*/
export const delXgClassPosDict = (id: string | number | Array<string | number>) => {
return request({
url: '/xgClassPosDict/xgClassPosDict/' + id,
method: 'delete'
});
};
@@ -0,0 +1,171 @@
export interface XgClassPosDictVO {
/**
* 主键ID(雪花ID)
*/
id: string | number;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted: number;
/**
* 职务编码
*/
positionCode: string;
/**
* 职务名称
*/
positionName: string;
/**
* 学生职务类别:class-班级干部,party-党团干部,other-其他
*/
positionCategory: string;
/**
* 学生职务级别:monitor-班长,vice-monitor-副班长,committee-委员,ordinary-普通成员
*/
positionLevel: number;
/**
* 学生职务类型:regular-常规职务,special-特殊职务,temporary-临时职务
*/
positionType: string;
/**
* 职责描述
*/
responsibilities: string;
/**
* 状态:0-启用,1-停用
*/
status: string;
/**
* 排序顺序
*/
sortOrder: number;
/**
* 备注信息
*/
remark: string;
}
export interface XgClassPosDictForm extends BaseEntity {
/**
* 主键ID(雪花ID)
*/
id?: string | number;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted?: number;
/**
* 职务编码
*/
positionCode?: string;
/**
* 职务名称
*/
positionName?: string;
/**
* 学生职务类别:class-班级干部,party-党团干部,other-其他
*/
positionCategory?: string;
/**
* 学生职务级别:monitor-班长,vice-monitor-副班长,committee-委员,ordinary-普通成员
*/
positionLevel?: number;
/**
* 学生职务类型:regular-常规职务,special-特殊职务,temporary-临时职务
*/
positionType?: string;
/**
* 职责描述
*/
responsibilities?: string;
/**
* 状态:0-启用,1-停用
*/
status?: string;
/**
* 排序顺序
*/
sortOrder?: number;
/**
* 备注信息
*/
remark?: string;
}
export interface XgClassPosDictQuery extends PageQuery {
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted?: number;
/**
* 职务编码
*/
positionCode?: string;
/**
* 职务名称
*/
positionName?: string;
/**
* 学生职务类别:class-班级干部,party-党团干部,other-其他
*/
positionCategory?: string;
/**
* 学生职务级别:monitor-班长,vice-monitor-副班长,committee-委员,ordinary-普通成员
*/
positionLevel?: number;
/**
* 学生职务类型:regular-常规职务,special-特殊职务,temporary-临时职务
*/
positionType?: string;
/**
* 职责描述
*/
responsibilities?: string;
/**
* 状态:0-启用,1-停用
*/
status?: string;
/**
* 排序顺序
*/
sortOrder?: number;
/**
* 日期范围参数
*/
params?: any;
}
@@ -0,0 +1,82 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { XgClassPosMsgVO, XgClassPosMsgForm, XgClassPosMsgQuery } from '@/api/xg/xsjcxx/xgClassPosMsg/xgClassPosMsg/types';
/**
* 查询班级干部信息列表
* @param query
* @returns {*}
*/
export const listXgClassPosMsg = (query?: XgClassPosMsgQuery): AxiosPromise<XgClassPosMsgVO[]> => {
return request({
url: '/xgClassPosMsg/xgClassPosMsg/list',
method: 'get',
params: query
});
};
/**
* 查询班级干部信息详细
* @param id
*/
export const getXgClassPosMsg = (id: string | number): AxiosPromise<XgClassPosMsgVO> => {
return request({
url: '/xgClassPosMsg/xgClassPosMsg/' + id,
method: 'get'
});
};
/**
* 新增班级干部信息
* @param data
*/
export const addXgClassPosMsg = (data: XgClassPosMsgForm) => {
return request({
url: '/xgClassPosMsg/xgClassPosMsg',
method: 'post',
data: data
});
};
/**
* 修改班级干部信息
* @param data
*/
export const updateXgClassPosMsg = (data: XgClassPosMsgForm) => {
return request({
url: '/xgClassPosMsg/xgClassPosMsg',
method: 'put',
data: data
});
};
/**
* 删除班级干部信息
* @param id
*/
export const delXgClassPosMsg = (id: string | number | Array<string | number>) => {
return request({
url: '/xgClassPosMsg/xgClassPosMsg/' + id,
method: 'delete'
});
};
/**
* 学生干部离任
* @param id 班级干部信息ID
* @param termEndDate 任期结束日期
* @param cadreStatus 学生职务任职状态
* @param dismissalReason 离任原因
*/
export const dismissCadre = (id: string | number, termEndDate: string, cadreStatus: string, dismissalReason: string) => {
return request({
url: '/xgClassPosMsg/xgClassPosMsg/dismiss/' + id,
method: 'post',
params: {
termEndDate,
cadreStatus,
dismissalReason
}
});
};
@@ -0,0 +1,346 @@
export interface XgClassPosMsgVO {
/**
* 主键ID(雪花ID)
*/
id: string | number;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted: number;
/**
* 班级ID,关联xg_class_msg.id
*/
classId: string | number;
/**
* 班级名称(联表查询获取)
*/
className: string;
/**
* 专业ID,关联sys_dept.dept_id
*/
majorId: string | number;
/**
* 专业名称(联表查询获取)
*/
majorName: string;
/**
* 学院ID,关联sys_dept.dept_id
*/
collegeId: string | number;
/**
* 学院名称(联表查询获取)
*/
collegeName: string;
/**
* 学生ID,关联xg_student_msg.id
*/
studentId: string | number;
/**
* 学号
*/
studentNo: string;
/**
* 学生姓名(冗余字段)
*/
studentName: string;
/**
* 职务编码,关联xg_class_pos_dict.position_code
*/
positionCode: string;
/**
* 职务名称(联表查询获取)
*/
positionName: string;
/**
* 任职日期
*/
appointmentDate: string;
/**
* 任期开始日期
*/
termStartDate: string;
/**
* 任期结束日期
*/
termEndDate: string;
/**
* 任期时长(月)
*/
termDuration: number;
/**
* 是否现任:0-否,1-是
*/
isCurrent: number;
/**
* 学生职务任职状态:1-在任,2-到期,3-辞职,4-免职,5-调任,6-休学
*/
cadreStatus: string;
/**
* 任命原因
*/
appointmentReason: string;
/**
* 学生职务任命方式:appointment-任命,election-选举,recommendation-推荐
*/
appointmentMethod: string;
/**
* 离任日期
*/
dismissalDate: string;
/**
* 离任原因
*/
dismissalReason: string;
/**
* 备注信息
*/
remark: string;
}
export interface XgClassPosMsgForm extends BaseEntity {
/**
* 主键ID(雪花ID)
*/
id?: string | number;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted?: number;
/**
* 班级ID,关联xg_class_msg.id
*/
classId?: string | number;
/**
* 班级名称(显示用,非数据库字段)
*/
className?: string;
/**
* 专业ID,关联sys_dept.dept_id
*/
majorId?: string | number;
/**
* 专业名称(显示用,非数据库字段)
*/
majorName?: string;
/**
* 学院ID,关联sys_dept.dept_id
*/
collegeId?: string | number;
/**
* 学院名称(显示用,非数据库字段)
*/
collegeName?: string;
/**
* 学生ID,关联xg_student_msg.id
*/
studentId?: string | number;
/**
* 学号
*/
studentNo?: string;
/**
* 学生姓名(冗余字段)
*/
studentName?: string;
/**
* 职务编码id,关联xg_class_pos_dict.id
*/
positionId?: string | number;
/**
* 职务编码,关联xg_class_pos_dict.position_code
*/
positionCode?: string;
/**
* 职务名称(显示用,非数据库字段)
*/
positionName?: string;
/**
* 任职日期
*/
appointmentDate?: string;
/**
* 任期开始日期
*/
termStartDate?: string;
/**
* 任期结束日期
*/
termEndDate?: string;
/**
* 任期时长(月)
*/
termDuration?: number;
/**
* 是否现任:0-否,1-是
*/
isCurrent?: number;
/**
* 学生职务任职状态:1-在任,2-到期,3-辞职,4-免职,5-调任,6-休学
*/
cadreStatus?: string;
/**
* 任命原因
*/
appointmentReason?: string;
/**
* 学生职务任命方式:appointment-任命,election-选举,recommendation-推荐
*/
appointmentMethod?: string;
/**
* 离任日期
*/
dismissalDate?: string;
/**
* 离任原因
*/
dismissalReason?: string;
/**
* 备注信息
*/
remark?: string;
}
export interface XgClassPosMsgQuery extends PageQuery {
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted?: number;
/**
* 班级ID,关联xg_class_msg.id
*/
classId?: string | number;
/**
* 学生ID,关联xg_student_msg.id
*/
studentId?: string | number;
/**
* 学号
*/
studentNo?: string;
/**
* 学生姓名(冗余字段)
*/
studentName?: string;
/**
* 职务编码,关联xg_class_pos_dict.position_code
*/
positionCode?: string;
/**
* 任职日期
*/
appointmentDate?: string;
/**
* 任期开始日期
*/
termStartDate?: string;
/**
* 任期结束日期
*/
termEndDate?: string;
/**
* 任期时长(月)
*/
termDuration?: number;
/**
* 是否现任:0-否,1-是
*/
isCurrent?: number;
/**
* 学生职务任职状态:1-在任,2-到期,3-辞职,4-免职,5-调任,6-休学
*/
cadreStatus?: string;
/**
* 任命原因
*/
appointmentReason?: string;
/**
* 学生职务任命方式:appointment-任命,election-选举,recommendation-推荐
*/
appointmentMethod?: string;
/**
* 离任日期
*/
dismissalDate?: string;
/**
* 离任原因
*/
dismissalReason?: string;
/**
* 部门ID,用于按部门查询班级干部信息
*/
deptId?: string | number;
/**
* 日期范围参数
*/
params?: any;
}
@@ -0,0 +1,95 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { StudentManagerVO, StudentManagerForm, StudentManagerQuery } from '@/api/xg/xsjcxx/xgStudentManager/studentManager/types';
// 临时定义一个接口来表示返回的教师数据
export interface TeacherData {
id: string | number;
managerName: string;
phone: string;
gender: string;
}
/**
* 获取可用的教职工信息列表
* @param managerType 管理员类型(1 全校管理员 2 自定义管理员 3 班主任)
*/
export const listAvailableTeachers = (managerType: string): AxiosPromise<TeacherData[]> => {
return request({
url: '/xgStudentManager/studentManager/listAvailableTeachers',
method: 'get',
params: { managerType }
});
};
/**
* 查询宿舍管理员信息列表
* @param query
* @returns {*}
*/
export const listStudentManager = (query?: StudentManagerQuery): AxiosPromise<StudentManagerVO[]> => {
return request({
url: '/xgStudentManager/studentManager/list',
method: 'get',
params: query
});
};
/**
* 查询宿舍管理员信息详细
* @param id
*/
export const getStudentManager = (id: string | number): AxiosPromise<StudentManagerVO> => {
return request({
url: '/xgStudentManager/studentManager/' + id,
method: 'get'
});
};
/**
* 新增宿舍管理员信息
* @param data
*/
export const addStudentManager = (data: StudentManagerForm) => {
return request({
url: '/xgStudentManager/studentManager',
method: 'post',
data: data
});
};
/**
* 修改宿舍管理员信息
* @param data
*/
export const updateStudentManager = (data: StudentManagerForm) => {
return request({
url: '/xgStudentManager/studentManager',
method: 'put',
data: data
});
};
/**
* 删除宿舍管理员信息
* @param id
*/
export const delStudentManager = (id: string | number | Array<string | number>) => {
return request({
url: '/xgStudentManager/studentManager/' + id,
method: 'delete'
});
};
/**
* 获取当前正在教学的班主任信息
* @param params 包含 phone 和 name 字段
*/
export const listActiveClassTeachers = (params: { phone?: string; name?: string }) => {
return request({
url: '/xgStudentManager/studentManager/listActiveClassTeachers',
method: 'get',
params
});
};
@@ -0,0 +1,156 @@
export interface StudentManagerVO {
/**
* 主键ID
*/
id: string | number;
/**
* 管理员姓名
*/
managerName: string;
/**
* 管理员类型(1 全校管理员 2 自定义管理员 3 班主任)
*/
managerType: string;
/**
* 管理级别(1-3,1为最高)
*/
managerLevel: number;
/**
* 关联用户ID
*/
userId: string | number;
/**
* 联系电话
*/
phone: string;
/**
* 性别(0男 1女 2未知)
*/
gender: string;
/**
* 状态(0正常 1停用)
*/
status: string;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted: string;
/**
* 备注
*/
remark: string;
}
export interface StudentManagerForm extends BaseEntity {
/**
* 主键ID
*/
id?: string | number;
/**
* 管理员姓名
*/
managerName?: string;
/**
* 管理员类型(1 全校管理员 2 自定义管理员 3 班主任)
*/
managerType?: string;
/**
* 管理级别(1-3,1为最高)
*/
managerLevel?: number;
/**
* 关联用户ID
*/
userId?: string | number;
/**
* 联系电话
*/
phone?: string;
/**
* 性别(0男 1女 2未知)
*/
gender?: string;
/**
* 状态(0正常 1停用)
*/
status?: string;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted?: string;
/**
* 备注
*/
remark?: string;
}
export interface StudentManagerQuery extends PageQuery {
/**
* 管理员姓名
*/
managerName?: string;
/**
* 管理员类型(1 全校管理员 2 自定义管理员 3 班主任)
*/
managerType?: string;
/**
* 管理级别(1-3,1为最高)
*/
managerLevel?: number;
/**
* 关联用户ID
*/
userId?: string | number;
/**
* 联系电话
*/
phone?: string;
/**
* 性别(0男 1女 2未知)
*/
gender?: string;
/**
* 状态(0正常 1停用)
*/
status?: string;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted?: string;
/**
* 日期范围参数
*/
params?: any;
}
@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { StudentManagerSourceVO, StudentManagerSourceForm, StudentManagerSourceQuery } from '@/api/xg/xsjcxx/xgStudentManagerSource/studentManagerSource/types';
/**
* 查询学生管理员授权关系列表
* @param query
* @returns {*}
*/
export const listStudentManagerSource = (query?: StudentManagerSourceQuery): AxiosPromise<StudentManagerSourceVO[]> => {
return request({
url: '/xgStudentManagerSource/studentManagerSource/list',
method: 'get',
params: query
});
};
/**
* 查询学生管理员授权关系详细
* @param id
*/
export const getStudentManagerSource = (id: string | number): AxiosPromise<StudentManagerSourceVO> => {
return request({
url: '/xgStudentManagerSource/studentManagerSource/' + id,
method: 'get'
});
};
/**
* 新增学生管理员授权关系
* @param data
*/
export const addStudentManagerSource = (data: StudentManagerSourceForm) => {
return request({
url: '/xgStudentManagerSource/studentManagerSource',
method: 'post',
data: data
});
};
/**
* 修改学生管理员授权关系
* @param data
*/
export const updateStudentManagerSource = (data: StudentManagerSourceForm) => {
return request({
url: '/xgStudentManagerSource/studentManagerSource',
method: 'put',
data: data
});
};
/**
* 删除学生管理员授权关系
* @param id
*/
export const delStudentManagerSource = (id: string | number | Array<string | number>) => {
return request({
url: '/xgStudentManagerSource/studentManagerSource/' + id,
method: 'delete'
});
};
@@ -0,0 +1,126 @@
export interface StudentManagerSourceVO {
/**
* 主键ID
*/
id: string | number;
/**
* 管理员ID
*/
managerId: string | number;
/**
* 权限类型(1查看 2编辑 3完全管理)
*/
permissionType: string;
/**
* 授权类型(1全校级 2自定义 3班主任)
*/
authType: string;
/**
* 授权来源ID(对应班级id)
*/
authSource: number;
/**
* 状态(0有效 1已撤销)
*/
status: string;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted: string;
/**
* 备注
*/
remark: string;
}
export interface StudentManagerSourceForm extends BaseEntity {
/**
* 主键ID
*/
id?: string | number;
/**
* 管理员ID
*/
managerId?: string | number;
/**
* 权限类型(1查看 2编辑 3完全管理)
*/
permissionType?: string;
/**
* 授权类型(1全校级 2自定义 3班主任)
*/
authType?: string;
/**
* 授权来源ID(对应班级id)
*/
authSource?: number;
/**
* 状态(0有效 1已撤销)
*/
status?: string;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted?: string;
/**
* 备注
*/
remark?: string;
}
export interface StudentManagerSourceQuery extends PageQuery {
/**
* 管理员ID
*/
managerId?: string | number;
/**
* 权限类型(1查看 2编辑 3完全管理)
*/
permissionType?: string;
/**
* 授权类型(1全校级 2自定义 3班主任)
*/
authType?: string;
/**
* 授权来源ID(对应班级id)
*/
authSource?: number;
/**
* 状态(0有效 1已撤销)
*/
status?: string;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted?: string;
/**
* 日期范围参数
*/
params?: any;
}
+3 -3
View File
@@ -236,7 +236,7 @@ export interface XljkEvaluationDetailVO {
studentName: string;
/**
* 性别(M-男,F-女)
* 性别(0-男,1-女)
*/
gender: string;
@@ -328,7 +328,7 @@ export interface XljkEvaluationDetailForm extends BaseEntity {
studentName?: string;
/**
* 性别(M-男,F-女)
* 性别(0-男,1-女)
*/
gender?: string;
@@ -407,7 +407,7 @@ export interface StudentInfo {
studentName: string;
/**
* 性别(M-男,F-女)
* 性别(0-男,1-女)
*/
gender: string;
+3 -3
View File
@@ -15,7 +15,7 @@ export interface EnrollmentInfoVO {
idCard: string;
/**
* 性别(M-男 F-女)
* 性别(0-男 1-女)
*/
gender: string;
@@ -197,7 +197,7 @@ export interface EnrollmentInfoForm extends BaseEntity {
idCard?: string;
/**
* 性别(M-男 F-女)
* 性别(0-男 1-女)
*/
gender?: string;
@@ -354,7 +354,7 @@ export interface EnrollmentInfoQuery extends PageQuery {
idCard?: string;
/**
* 性别(M-男 F-女)
* 性别(0-男 1-女)
*/
gender?: string;
+1 -1
View File
@@ -153,7 +153,7 @@ export interface EnrollmentProcessVO {
updateTime: string
/**
* 性别(M-男 F-女)
* 性别(0-男 1-女)
*/
gender?: string
+94
View File
@@ -0,0 +1,94 @@
/**
* 企业信息管理 API
* @author Cursor AI
* @date 2025-01-25
*/
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { EnterpriseVO, EnterpriseForm, EnterpriseQuery } from './types';
// TableDataInfo 接口定义
export interface TableDataInfo<T> {
total: number;
rows: T[];
code?: number;
msg?: string;
}
/**
* 查询企业信息列表
* @param query 查询参数
* @returns Promise<TableDataInfo<EnterpriseVO>>
*/
export function listEnterprise(query?: EnterpriseQuery): AxiosPromise<TableDataInfo<EnterpriseVO>> {
return request({
url: '/zsjy/enterprise/list',
method: 'get',
params: query
});
}
/**
* 查询企业信息详细
* @param id 企业主键
* @returns Promise<EnterpriseVO>
*/
export function getEnterprise(id: string | number): AxiosPromise<EnterpriseVO> {
return request({
url: `/zsjy/enterprise/${id}`,
method: 'get'
});
}
/**
* 新增企业信息
* @param data 企业信息
* @returns Promise<any>
*/
export function addEnterprise(data: EnterpriseForm): AxiosPromise<any> {
return request({
url: '/zsjy/enterprise',
method: 'post',
data: data
});
}
/**
* 修改企业信息
* @param data 企业信息
* @returns Promise<any>
*/
export function updateEnterprise(data: EnterpriseForm): AxiosPromise<any> {
return request({
url: '/zsjy/enterprise',
method: 'put',
data: data
});
}
/**
* 删除企业信息
* @param id 企业主键
* @returns Promise<any>
*/
export function delEnterprise(id: string | number | Array<string | number>): AxiosPromise<any> {
return request({
url: `/zsjy/enterprise/${id}`,
method: 'delete'
});
}
/**
* 导出企业信息
* @param query 查询参数
* @returns Promise<any>
*/
export function exportEnterprise(query: EnterpriseQuery): AxiosPromise<any> {
return request({
url: '/zsjy/enterprise/export',
method: 'post',
data: query,
responseType: 'blob'
});
}
+297
View File
@@ -0,0 +1,297 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import {
GraduateInfoVO,
GraduateInfoForm,
GraduateInfoQuery,
GraduateStatisticsVO
} from '@/api/zsjy/types';
/**
* 查询毕业生信息列表
* @param query 查询参数
*/
export const listGraduateInfo = (query?: GraduateInfoQuery): AxiosPromise<GraduateInfoVO[]> => {
return request({
url: '/zsjy/graduate/list',
method: 'get',
params: query
});
};
/**
* 查询毕业生信息详细信息
* @param id 毕业生信息ID
*/
export const getGraduateInfo = (id: string | number): AxiosPromise<GraduateInfoVO> => {
return request({
url: `/zsjy/graduate/${id}`,
method: 'get'
});
};
/**
* 新增毕业生信息
* @param data 毕业生信息数据
*/
export const addGraduateInfo = (data: GraduateInfoForm) => {
return request({
url: '/zsjy/graduate',
method: 'post',
data: data
});
};
/**
* 修改毕业生信息
* @param data 毕业生信息数据
*/
export const updateGraduateInfo = (data: GraduateInfoForm) => {
return request({
url: '/zsjy/graduate',
method: 'put',
data: data
});
};
/**
* 删除毕业生信息
* @param id 毕业生信息ID
*/
export const delGraduateInfo = (id: string | number | Array<string | number>) => {
return request({
url: `/zsjy/graduate/${id}`,
method: 'delete'
});
};
/**
* 导出毕业生信息列表
* @param query 查询参数
*/
export const exportGraduateInfo = (query: GraduateInfoQuery) => {
return request({
url: '/zsjy/graduate/export',
method: 'post',
data: query,
responseType: 'blob'
});
};
/**
* 根据学号查询毕业生信息
* @param studentNo 学号
*/
export const getGraduateInfoByStudentNo = (studentNo: string): AxiosPromise<GraduateInfoVO> => {
return request({
url: `/zsjy/graduate/student/${studentNo}`,
method: 'get'
});
};
/**
* 根据身份证号查询毕业生信息
* @param idCard 身份证号
*/
export const getGraduateInfoByIdCard = (idCard: string): AxiosPromise<GraduateInfoVO> => {
return request({
url: `/zsjy/graduate/idcard/${idCard}`,
method: 'get'
});
};
/**
* 校验学号是否唯一
* @param studentNo 学号
* @param id 主键ID(修改时传入)
*/
export const checkStudentNoUnique = (studentNo: string, id?: string | number): AxiosPromise<boolean> => {
return request({
url: '/zsjy/graduate/checkStudentNo',
method: 'get',
params: { studentNo, id }
});
};
/**
* 校验身份证号是否唯一
* @param idCard 身份证号
* @param id 主键ID(修改时传入)
*/
export const checkIdCardUnique = (idCard: string, id?: string | number): AxiosPromise<boolean> => {
return request({
url: '/zsjy/graduate/checkIdCard',
method: 'get',
params: { idCard, id }
});
};
/**
* 审核毕业生信息
* @param id 主键
* @param auditStatus 审核状态
* @param auditRemark 审核意见
*/
export const auditGraduateInfo = (id: string | number, auditStatus: string, auditRemark?: string) => {
return request({
url: `/zsjy/graduate/audit/${id}`,
method: 'put',
params: { auditStatus, auditRemark }
});
};
/**
* 批量审核毕业生信息
* @param ids 主键数组
* @param auditStatus 审核状态
* @param auditRemark 审核意见
*/
export const batchAuditGraduateInfo = (ids: (string | number)[], auditStatus: string, auditRemark?: string) => {
return request({
url: '/zsjy/graduate/batchAudit',
method: 'put',
data: ids,
params: { auditStatus, auditRemark }
});
};
/**
* 统计总体就业情况
*/
export const getOverallStatistics = (): AxiosPromise<GraduateStatisticsVO> => {
return request({
url: '/zsjy/graduate/statistics/overall',
method: 'get'
});
};
/**
* 按专业统计就业情况
* @param graduationYear 毕业年份
*/
export const getStatisticsByMajor = (graduationYear?: number): AxiosPromise<GraduateStatisticsVO[]> => {
return request({
url: '/zsjy/graduate/statistics/major',
method: 'get',
params: { graduationYear }
});
};
/**
* 按学院统计就业情况
* @param graduationYear 毕业年份
*/
export const getStatisticsByCollege = (graduationYear?: number): AxiosPromise<GraduateStatisticsVO[]> => {
return request({
url: '/zsjy/graduate/statistics/college',
method: 'get',
params: { graduationYear }
});
};
/**
* 按就业状态统计
* @param graduationYear 毕业年份
*/
export const getStatisticsByEmploymentStatus = (graduationYear?: number): AxiosPromise<GraduateStatisticsVO[]> => {
return request({
url: '/zsjy/graduate/statistics/employment',
method: 'get',
params: { graduationYear }
});
};
/**
* 按单位性质统计
* @param graduationYear 毕业年份
*/
export const getStatisticsByCompanyType = (graduationYear?: number): AxiosPromise<GraduateStatisticsVO[]> => {
return request({
url: '/zsjy/graduate/statistics/company',
method: 'get',
params: { graduationYear }
});
};
/**
* 按地域分布统计
* @param graduationYear 毕业年份
*/
export const getStatisticsByRegion = (graduationYear?: number): AxiosPromise<GraduateStatisticsVO[]> => {
return request({
url: '/zsjy/graduate/statistics/region',
method: 'get',
params: { graduationYear }
});
};
/**
* 按薪资范围统计
* @param graduationYear 毕业年份
*/
export const getStatisticsBySalaryRange = (graduationYear?: number): AxiosPromise<GraduateStatisticsVO[]> => {
return request({
url: '/zsjy/graduate/statistics/salary',
method: 'get',
params: { graduationYear }
});
};
/**
* 按年份统计历年就业情况
*/
export const getStatisticsByYear = (): AxiosPromise<GraduateStatisticsVO[]> => {
return request({
url: '/zsjy/graduate/statistics/year',
method: 'get'
});
};
/**
* 查询待审核的毕业生信息数量
*/
export const getPendingAuditCount = (): AxiosPromise<number> => {
return request({
url: '/zsjy/graduate/pendingCount',
method: 'get'
});
};
/**
* 查询指定学院的毕业生信息列表
* @param collegeId 学院ID
* @param query 查询条件
*/
export const getListByCollegeId = (collegeId: string | number, query?: GraduateInfoQuery): AxiosPromise<GraduateInfoVO[]> => {
return request({
url: `/zsjy/graduate/college/${collegeId}`,
method: 'get',
params: query
});
};
/**
* 查询指定专业的毕业生信息列表
* @param majorId 专业ID
* @param query 查询条件
*/
export const getListByMajorId = (majorId: string | number, query?: GraduateInfoQuery): AxiosPromise<GraduateInfoVO[]> => {
return request({
url: `/zsjy/graduate/major/${majorId}`,
method: 'get',
params: query
});
};
/**
* 查询指定班级的毕业生信息列表
* @param classId 班级ID
* @param query 查询条件
*/
export const getListByClassId = (classId: string | number, query?: GraduateInfoQuery): AxiosPromise<GraduateInfoVO[]> => {
return request({
url: `/zsjy/graduate/class/${classId}`,
method: 'get',
params: query
});
};
+11 -9
View File
@@ -6,13 +6,15 @@
import request from '@/utils/request';
// 此处导出招生就业管理模块相关的所有API接口
// 示例:
// export * from './enrollment' // 招生管理
// export * from './employment' // 就业管理
// export * from './enterprise' // 企业管理
// export * from './career' // 职业规划
// export * from './statistics' // 统计分析
// 导出招生就业管理模块相关的所有API接口
export * from './enterprise'; // 企业信息管理
// export * from './jobPosition'; // 招聘岗位管理 (待开发)
// export * from './recruitmentPoster'; // 招聘海报管理 (待开发)
// 临时导出一个空对象,防止编译错误
export default {};
// 现有导出
export * from './statistics'; // 统计分析
export * from './enrollmentProcess'; // 录取流程管理
export * from './enrollmentStatistics'; // 报名统计
// 类型导出
export * from './types'; // 类型定义
+91
View File
@@ -0,0 +1,91 @@
import request from '@/utils/request'
import { JobPositionVO, JobPositionForm, JobPositionQuery, TableDataInfo } from './types'
import { AxiosPromise } from 'axios'
/**
* 查询招聘岗位列表
* @param query 查询参数
* @returns 招聘岗位列表
*/
export function listJobPosition(query?: JobPositionQuery): AxiosPromise<TableDataInfo<JobPositionVO>> {
return request({
url: '/zsjy/position/list',
method: 'get',
params: query
})
}
/**
* 查询招聘岗位详细
* @param id 招聘岗位ID
* @returns 招聘岗位详情
*/
export function getJobPosition(id: string | number): AxiosPromise<JobPositionVO> {
return request({
url: '/zsjy/position/' + id,
method: 'get'
})
}
/**
* 新增招聘岗位
* @param data 招聘岗位表单
* @returns 操作结果
*/
export function addJobPosition(data: JobPositionForm) {
return request({
url: '/zsjy/position',
method: 'post',
data: data
})
}
/**
* 修改招聘岗位
* @param data 招聘岗位表单
* @returns 操作结果
*/
export function updateJobPosition(data: JobPositionForm) {
return request({
url: '/zsjy/position',
method: 'put',
data: data
})
}
/**
* 删除招聘岗位
* @param id 招聘岗位ID
* @returns 操作结果
*/
export function delJobPosition(id: string | number | Array<string | number>) {
return request({
url: '/zsjy/position/' + id,
method: 'delete'
})
}
/**
* 导出招聘岗位
* @param query 查询参数
* @returns 导出结果
*/
export function exportJobPosition(query: JobPositionQuery) {
return request({
url: '/zsjy/position/export',
method: 'post',
data: query,
responseType: 'blob'
})
}
/**
* 查询企业下拉选项
* @returns 企业列表
*/
export function listEnterpriseOptions() {
return request({
url: '/zsjy/enterprise/options',
method: 'get'
})
}
+93
View File
@@ -0,0 +1,93 @@
import request from '@/utils/request'
import { RecruitmentPosterVO, RecruitmentPosterForm, RecruitmentPosterQuery, TableDataInfo } from './types'
import { AxiosPromise } from 'axios'
/**
* 查询招聘海报列表
* @param query 查询参数
* @returns 招聘海报列表
*/
export function listRecruitmentPoster(query?: RecruitmentPosterQuery): AxiosPromise<TableDataInfo<RecruitmentPosterVO>> {
return request({
url: '/zsjy/poster/list',
method: 'get',
params: query
})
}
/**
* 查询招聘海报详细
* @param id 招聘海报ID
* @returns 招聘海报详情
*/
export function getRecruitmentPoster(id: string | number): AxiosPromise<RecruitmentPosterVO> {
return request({
url: '/zsjy/poster/' + id,
method: 'get'
})
}
/**
* 新增招聘海报
* @param data 招聘海报表单
* @returns 操作结果
*/
export function addRecruitmentPoster(data: RecruitmentPosterForm) {
return request({
url: '/zsjy/poster',
method: 'post',
data: data
})
}
/**
* 修改招聘海报
* @param data 招聘海报表单
* @returns 操作结果
*/
export function updateRecruitmentPoster(data: RecruitmentPosterForm) {
return request({
url: '/zsjy/poster',
method: 'put',
data: data
})
}
/**
* 删除招聘海报
* @param id 招聘海报ID
* @returns 操作结果
*/
export function delRecruitmentPoster(id: string | number | Array<string | number>) {
return request({
url: '/zsjy/poster/' + id,
method: 'delete'
})
}
/**
* 导出招聘海报
* @param query 查询参数
* @returns 导出结果
*/
export function exportRecruitmentPoster(query: RecruitmentPosterQuery) {
return request({
url: '/zsjy/poster/export',
method: 'post',
data: query,
responseType: 'blob'
})
}
/**
* 查询岗位下拉选项
* @param enterpriseId 企业ID
* @returns 岗位列表
*/
export function listPositionOptions(enterpriseId?: number | string) {
return request({
url: '/zsjy/position/options',
method: 'get',
params: { enterpriseId }
})
}
+743
View File
@@ -0,0 +1,743 @@
/**
* 招生就业管理模块 - 类型定义
*/
/**
* 企业信息 VO
*/
export interface EnterpriseVO {
/**
* 企业ID
*/
id?: string | number;
/**
* 企业名称
*/
enterpriseName?: string;
/**
* 所属行业
*/
industry?: string;
/**
* 行业名称
*/
industryName?: string;
/**
* 主营业务
*/
mainBusiness?: string;
/**
* 企业规模
*/
enterpriseScale?: string;
/**
* 企业规模名称
*/
enterpriseScaleName?: string;
/**
* 企业联系人姓名
*/
contactName?: string;
/**
* 联系电话
*/
contactPhone?: string;
/**
* 企业邮箱
*/
email?: string;
/**
* 企业地址
*/
address?: string;
/**
* 状态(0正常 1停用)
*/
status?: string;
/**
* 备注
*/
remark?: string;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted?: number;
/**
* 租户编号
*/
tenantId?: string;
/**
* 创建部门
*/
createDept?: number;
/**
* 创建时间
*/
createTime: string;
/**
* 创建者
*/
createBy?: number;
/**
* 更新时间
*/
updateTime?: string;
/**
* 更新者
*/
updateBy?: number;
}
/**
* 企业信息表单对象
*/
export interface EnterpriseForm extends BaseEntity {
/**
* 企业ID
*/
id?: string | number;
/**
* 企业名称
*/
enterpriseName: string;
/**
* 所属行业
*/
industry: string;
/**
* 主营业务
*/
mainBusiness: string;
/**
* 企业规模
*/
enterpriseScale: string;
/**
* 企业联系人姓名
*/
contactName: string;
/**
* 联系电话
*/
contactPhone: string;
/**
* 企业邮箱
*/
email: string;
/**
* 企业地址
*/
address: string;
/**
* 状态(0正常 1停用)
*/
status: string;
/**
* 备注
*/
remark?: string;
}
/**
* 企业信息查询条件
*/
export interface EnterpriseQuery extends PageQuery {
/**
* 企业名称
*/
enterpriseName?: string;
/**
* 所属行业
*/
industry?: string;
/**
* 主营业务
*/
mainBusiness?: string;
/**
* 企业规模
*/
enterpriseScale?: string;
/**
* 企业联系人姓名
*/
contactName?: string;
/**
* 联系电话
*/
contactPhone?: string;
/**
* 企业邮箱
*/
email?: string;
/**
* 企业地址
*/
address?: string;
/**
* 状态(0正常 1停用)
*/
status?: string;
}
/**
* 招聘岗位VO对象
*/
export interface JobPositionVO {
/**
* 岗位ID
*/
id: number | string;
/**
* 所属企业ID
*/
enterpriseId: number | string;
/**
* 企业名称(关联字段)
*/
enterpriseName?: string;
/**
* 岗位名称
*/
positionName: string;
/**
* 工作地址
*/
workAddress: string;
/**
* 工作时间
*/
workTime: string;
/**
* 薪资
*/
salary: string;
/**
* 所需技能
*/
requiredSkills?: string;
/**
* 有效期开始日期
*/
startDate: string;
/**
* 有效期结束日期
*/
endDate: string;
/**
* 是否永久有效(0否 1是)
*/
isPermanent: number;
/**
* 状态(0正常 1停用)
*/
status: string;
/**
* 备注
*/
remark?: string;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted?: number;
/**
* 租户编号
*/
tenantId?: string;
/**
* 创建部门
*/
createDept?: number;
/**
* 创建时间
*/
createTime: string;
/**
* 创建者
*/
createBy?: number;
/**
* 更新时间
*/
updateTime?: string;
/**
* 更新者
*/
updateBy?: number;
}
/**
* 招聘岗位Form对象
*/
export interface JobPositionForm extends BaseEntity {
/**
* 岗位ID
*/
id?: number | string;
/**
* 所属企业ID
*/
enterpriseId: number | string;
/**
* 岗位名称
*/
positionName: string;
/**
* 工作地址
*/
workAddress: string;
/**
* 工作时间
*/
workTime: string;
/**
* 薪资
*/
salary: string;
/**
* 所需技能
*/
requiredSkills?: string;
/**
* 有效期开始日期
*/
startDate: string;
/**
* 有效期结束日期
*/
endDate: string;
/**
* 是否永久有效(0否 1是)
*/
isPermanent: number;
/**
* 状态(0正常 1停用)
*/
status: string;
/**
* 备注
*/
remark?: string;
}
/**
* 招聘岗位Query对象
*/
export interface JobPositionQuery extends PageQuery {
/**
* 所属企业ID
*/
enterpriseId?: number | string;
/**
* 岗位名称
*/
positionName?: string;
/**
* 工作地址
*/
workAddress?: string;
/**
* 状态(0正常 1停用)
*/
status?: string;
/**
* 是否永久有效
*/
isPermanent?: number;
/**
* 创建时间范围
*/
params?: {
beginCreateTime?: string;
endCreateTime?: string;
};
}
/**
* 招聘海报VO对象
*/
export interface RecruitmentPosterVO {
/**
* 海报ID
*/
id: number | string;
/**
* 企业ID
*/
enterpriseId: number | string;
/**
* 企业名称(关联字段)
*/
enterpriseName?: string;
/**
* 岗位ID(为空表示企业整体海报)
*/
positionId?: number | string;
/**
* 岗位名称(关联字段)
*/
positionName?: string;
/**
* 海报文件路径
*/
posterPath: string;
/**
* 海报类型(1企业海报 2岗位海报)
*/
posterType: string;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted?: number;
/**
* 租户编号
*/
tenantId?: string;
/**
* 创建部门
*/
createDept?: number;
/**
* 创建时间
*/
createTime: string;
/**
* 创建者
*/
createBy?: number;
/**
* 更新时间
*/
updateTime?: string;
/**
* 更新者
*/
updateBy?: number;
}
/**
* 招聘海报Form对象
*/
export interface RecruitmentPosterForm extends BaseEntity {
/**
* 海报ID
*/
id?: number | string;
/**
* 企业ID
*/
enterpriseId: number | string;
/**
* 岗位ID(为空表示企业整体海报)
*/
positionId?: number | string;
/**
* 海报文件路径
*/
posterPath: string;
/**
* 海报类型(1企业海报 2岗位海报)
*/
posterType: string;
}
/**
* 招聘海报Query对象
*/
export interface RecruitmentPosterQuery extends PageQuery {
/**
* 企业ID
*/
enterpriseId?: number | string;
/**
* 岗位ID
*/
positionId?: number | string;
/**
* 海报类型(1企业海报 2岗位海报)
*/
posterType?: string;
}
/**
* 分页响应数据
*/
export interface TableDataInfo<T> {
total: number;
rows: T[];
code?: number;
msg?: string;
}
/**
* 毕业生信息VO
*/
export interface GraduateInfoVO {
id?: string | number;
studentNo?: string;
name?: string;
gender?: string;
genderName?: string;
idCard?: string;
nation?: string;
politicalStatus?: string;
birthDate?: string;
enrollmentYear?: number;
graduationYear?: number;
collegeId?: string | number;
collegeName?: string;
majorId?: string | number;
majorName?: string;
classId?: string | number;
className?: string;
educationLevel?: string;
educationLevelName?: string;
trainingMode?: string;
phoneNumber?: string;
email?: string;
qqNumber?: string;
wechatNumber?: string;
province?: string;
city?: string;
district?: string;
detailAddress?: string;
fullAddress?: string;
postalCode?: string;
employmentStatus?: string;
employmentStatusName?: string;
employmentCompany?: string;
companyType?: string;
companyTypeName?: string;
jobPosition?: string;
workProvince?: string;
workCity?: string;
workLocation?: string;
salaryRange?: string;
salaryRangeName?: string;
furtherSchool?: string;
furtherMajor?: string;
skills?: string;
awards?: string;
jobIntention?: string;
auditStatus?: string;
auditStatusName?: string;
auditTime?: string;
auditBy?: string | number;
auditByName?: string;
auditRemark?: string;
status?: string;
remark?: string;
createBy?: string | number;
createTime?: string;
updateBy?: string | number;
updateTime?: string;
}
/**
* 毕业生信息表单
*/
export interface GraduateInfoForm {
id?: string | number;
studentNo?: string;
name?: string;
gender?: string;
idCard?: string;
nation?: string;
politicalStatus?: string;
birthDate?: string;
enrollmentYear?: number;
graduationYear?: number;
collegeId?: string | number;
collegeName?: string;
majorId?: string | number;
majorName?: string;
classId?: string | number;
className?: string;
educationLevel?: string;
trainingMode?: string;
phoneNumber?: string;
email?: string;
qqNumber?: string;
wechatNumber?: string;
province?: string;
city?: string;
district?: string;
detailAddress?: string;
postalCode?: string;
employmentStatus?: string;
employmentCompany?: string;
companyType?: string;
jobPosition?: string;
workProvince?: string;
workCity?: string;
salaryRange?: string;
furtherSchool?: string;
furtherMajor?: string;
skills?: string;
awards?: string;
jobIntention?: string;
auditStatus?: string;
auditTime?: string;
auditBy?: string | number;
auditRemark?: string;
status?: string;
remark?: string;
}
/**
* 毕业生信息查询
*/
export interface GraduateInfoQuery extends PageQuery {
studentNo?: string;
name?: string;
gender?: string;
idCard?: string;
nation?: string;
politicalStatus?: string;
birthDate?: string;
enrollmentYear?: number;
graduationYear?: number;
collegeId?: string | number;
collegeName?: string;
majorId?: string | number;
majorName?: string;
classId?: string | number;
className?: string;
educationLevel?: string;
trainingMode?: string;
phoneNumber?: string;
email?: string;
employmentStatus?: string;
employmentCompany?: string;
companyType?: string;
jobPosition?: string;
workProvince?: string;
workCity?: string;
salaryRange?: string;
furtherSchool?: string;
furtherMajor?: string;
auditStatus?: string;
auditBy?: string | number;
status?: string;
params?: any;
}
/**
* 毕业生信息统计VO
*/
export interface GraduateStatisticsVO {
name?: string;
value?: number;
total?: number;
percentage?: number;
employmentCount?: number;
employmentRate?: number;
furtherStudyCount?: number;
furtherStudyRate?: number;
unemploymentCount?: number;
type?: string;
year?: number;
collegeId?: string | number;
collegeName?: string;
majorId?: string | number;
majorName?: string;
employmentStatus?: string;
employmentStatusName?: string;
companyType?: string;
companyTypeName?: string;
province?: string;
city?: string;
salaryRange?: string;
salaryRangeName?: string;
}
+4
View File
@@ -229,3 +229,7 @@ aside {
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__header {
padding: 10px 10px !important;
}
.el-message {
z-index: 4000 !important;
}
+64 -3
View File
@@ -33,11 +33,12 @@
<!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li v-for="(file, index) in fileList" :key="file.uid" class="el-upload-list__item ele-upload-list__item-content">
<el-link :href="`${file.url}`" :underline="false" target="_blank">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
<!-- <el-link :href="`${file.url}`" :underline="false" target="_blank"> -->
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
<!-- </el-link> -->
<div class="ele-upload-list__item-content-action">
<el-button type="danger" v-if="!disabled" link @click="handleDelete(index)">删除</el-button>
<el-button type="danger" v-if="!disabled" link @click="handleDownloadAttachment(file)">下载</el-button>
</div>
</li>
</transition-group>
@@ -48,6 +49,8 @@
import { propTypes } from '@/utils/propTypes';
import { delOss, listByIds } from '@/api/system/oss';
import { globalHeaders } from '@/utils/request';
import request from '@/utils/request';
import { vLoading, ElLoading } from 'element-plus';
const props = defineProps({
modelValue: {
@@ -116,6 +119,64 @@ watch(
{ deep: true, immediate: true }
);
/** 下载附件 */
const handleDownloadAttachment = async (file: any) => {
let downloadUrl = file.ossId;
let fileName = file.name || 'attachment';
if (!downloadUrl) {
proxy?.$modal.msgError('无法获取文件下载地址');
return;
}
// 对于服务器文件,使用fetch获取内容强制下载
try {
const loading = ElLoading.service({
text: '正在下载文件...',
target: document.body
});
// const response = await fetch(downloadUrl);
// if (!response.ok) {
// throw new Error(`HTTP error! status: ${response.status}`);
// }
// 通过fetch获取文件内容,强制下载
// const response = await req('/resource/oss/download/' + attachment.filePath);
const response = await request({
url: '/resource/oss/download/' + downloadUrl,
method: 'get',
responseType: 'blob',
timeout: 300000 // 300秒超时
});
// 由于axios响应拦截器对blob类型响应直接返回blob数据
// 这里response直接就是blob对象,而不是包含data属性的响应对象
const blob = response;
// 验证响应数据
if (!blob) {
throw new Error('响应数据为空');
}
// const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
loading?.close();
console.log('下载服务器文件成功:', fileName);
} catch (error) {
console.error('下载文件失败:', error);
proxy?.$modal.msgError('下载文件失败,请稍后重试');
}
};
// 上传前校检格式和大小
const handleBeforeUpload = (file: any) => {
// 校检文件类型
+276 -20
View File
@@ -1,18 +1,34 @@
<template>
<el-image :src="`${realSrc}`" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="realSrcList" preview-teleported>
<el-image
:src="realSrc"
fit="cover"
:style="`width:${realWidth};height:${realHeight};`"
:preview-src-list="realSrcList"
preview-teleported
v-loading="loading"
@error="handleImageError"
@load="handleImageLoad"
>
<template #error>
<div class="image-slot">
<el-icon><picture-filled /></el-icon>
<div class="error-text">图片加载失败</div>
</div>
</template>
</el-image>
</template>
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes';
import { computed, ref, watch, onBeforeUnmount } from 'vue';
import { listByIds } from '@/api/system/oss';
import type { OssVO } from '@/api/system/oss/types';
import request from '@/utils/request';
const props = defineProps({
src: propTypes.string.def(''),
src: {
type: String,
default: ''
},
width: {
type: [Number, String],
default: ''
@@ -20,31 +36,264 @@ const props = defineProps({
height: {
type: [Number, String],
default: ''
},
// 是否为ossid模式
isOssId: {
type: Boolean,
default: true
}
});
const realSrc = computed(() => {
if (!props.src) {
return;
}
const real_src = props.src.split(',')[0];
return real_src;
const loading = ref(false);
const urlCache = ref<Map<string, string>>(new Map());
const blobUrls = ref<string[]>([]);
// 图片加载成功事件
const handleImageLoad = () => {
// 图片加载成功
};
// 图片加载失败事件
const handleImageError = (error: any) => {
// 图片加载失败处理
};
// 清理blob URL,防止内存泄漏
const cleanupBlobUrls = () => {
blobUrls.value.forEach((url) => {
URL.revokeObjectURL(url);
});
blobUrls.value = [];
urlCache.value.clear();
};
// 组件卸载时清理blob URLs
onBeforeUnmount(() => {
cleanupBlobUrls();
});
const realSrcList = computed(() => {
if (!props.src) {
return [];
// 验证blob数据是否为有效图片
const validateImageBlob = (blob: Blob): boolean => {
// 允许的图片类型,包括通用的octet-stream类型(OSS服务器常返回此类型)
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'application/octet-stream'];
// 基本验证:类型检查和大小检查
const hasValidType = validTypes.includes(blob.type) || blob.type === '' || blob.type === 'application/octet-stream';
const hasValidSize = blob.size > 0;
return hasValidType && hasValidSize;
};
// 通过认证请求获取图片数据并转换为blob URL
const getImageBlobUrl = async (imageUrl: string): Promise<string> => {
try {
// 检查是否已有缓存的blob URL
const cachedUrl = urlCache.value.get(imageUrl);
if (cachedUrl) {
return cachedUrl;
}
// 通过axios请求图片数据,这样会自动带上token和clientId
const response = await request({
url: imageUrl,
method: 'get',
responseType: 'blob',
timeout: 300000 // 300秒超时
});
// 由于axios响应拦截器对blob类型响应直接返回blob数据
// 这里response直接就是blob对象,而不是包含data属性的响应对象
const blob = response;
// 验证响应数据
if (!blob) {
throw new Error('响应数据为空');
}
if (!(blob instanceof Blob)) {
// 如果不是Blob,尝试获取实际内容用于调试
let actualContent = 'Unknown';
try {
if (typeof blob === 'string') {
actualContent = (blob as string).substring(0, 200);
} else if (blob && typeof blob.toString === 'function') {
actualContent = blob.toString().substring(0, 200);
}
} catch (e) {
actualContent = 'Cannot convert to string';
}
throw new Error(`响应数据不是有效的Blob对象。实际类型: ${typeof blob}, 内容: ${actualContent}`);
}
// 检查blob内容类型
// const contentType = blob.type;
// 如果Content-Type明确表明这不是图片数据且不是通用二进制格式,进行检查
// if (
// contentType &&
// !contentType.toLowerCase().startsWith('image/') &&
// !contentType.toLowerCase().includes('octet-stream') &&
// (contentType.toLowerCase().includes('text/') || contentType.toLowerCase().includes('html'))
// ) {
// const text = await blob.text();
// const preview = text.substring(0, 300);
// throw new Error(`响应Content-Type不是图片格式: ${contentType}。响应内容预览: ${preview}`);
// }
// 验证是否为有效图片
// if (!validateImageBlob(blob)) {
// throw new Error(`无效的图片格式: ${blob.type}, 大小: ${blob.size}`);
// }
// 创建blob URL
const blobUrl = URL.createObjectURL(blob);
// 缓存blob URL
// urlCache.value.set(imageUrl, blobUrl);
// blobUrls.value.push(blobUrl);
return blobUrl;
} catch (error) {
return '';
}
const real_src_list = props.src.split(',');
const srcList: string[] = [];
real_src_list.forEach((item: string) => {
if (item.trim() === '') {
};
// 通过ossid获取真实URL
const getUrlByOssId = async (ossId: string): Promise<string> => {
try {
loading.value = true;
// const response = await listByIds(ossId);
// // RuoYi框架的响应格式:{code, msg, data},实际数据在data字段中
// // 正确提取数据,考虑可能的响应格式
// const ossList: OssVO[] = (response as any)?.data || response;
// if (!ossList || ossList.length === 0) {
// throw new Error(`OSS ID ${ossId} 在数据库中不存在或已被删除。可能原因:1) OSS ID不正确 2) 文件已被删除 3) 权限不足`);
// }
// const ossFile = ossList[0];
// // 额外验证ossFile对象的完整性
// if (!ossFile || typeof ossFile !== 'object') {
// throw new Error(`OSS数据格式异常:期望对象,实际获得 ${typeof ossFile}`);
// }
// if (!ossFile.url) {
// throw new Error(`OSS文件缺少url属性。完整数据:${JSON.stringify(ossFile)}`);
// }
// const imageUrl = ossFile.url;
// // 检查URL是否有效
// if (!imageUrl || typeof imageUrl !== 'string') {
// throw new Error(`OSS接口返回的URL无效。URL: ${imageUrl}, 类型: ${typeof imageUrl}`);
// }
// // 如果URL不是以/开头的下载路径,可能是直接的OSS URL
// if (!imageUrl.startsWith('/resource/oss/download/')) {
// // 对于直接的OSS URL,我们仍然通过认证请求获取
// }
// 获取认证的blob URL
return await getImageBlobUrl('/resource/oss/download/' + ossId);
} catch (error) {
return '';
} finally {
loading.value = false;
}
};
// 处理多个ossid的情况
const getUrlsByOssIds = async (ossIds: string[]): Promise<string[]> => {
const urls: string[] = [];
for (const ossId of ossIds) {
if (ossId.trim()) {
const url = await getUrlByOssId(ossId.trim());
if (url) {
urls.push(url);
}
}
}
return urls;
};
// 处理普通URL模式(非ossId)
const getUrlsByDirectUrls = async (urls: string[]): Promise<string[]> => {
const blobUrls: string[] = [];
for (const url of urls) {
if (url.trim()) {
const blobUrl = await getImageBlobUrl(url.trim());
if (blobUrl) {
blobUrls.push(blobUrl);
}
}
}
return blobUrls;
};
const realSrc = ref('');
const realSrcList = ref<string[]>([]);
// 处理src变化
watch(
() => props.src,
async (newSrc) => {
// 清理之前的blob URLs
cleanupBlobUrls();
if (!newSrc) {
realSrc.value = '';
realSrcList.value = [];
return;
}
return srcList.push(item);
});
return srcList;
});
loading.value = true;
try {
if (props.isOssId) {
// console.log('ossid模式');
// console.log(newSrc);
// ossid模式:通过接口获取真实URL,然后转换为认证的blob URL
const ossIds = newSrc.split(',').filter((id) => id.trim());
const urls = await getUrlsByOssIds(ossIds);
if (urls.length > 0) {
realSrc.value = urls[0];
realSrcList.value = urls;
} else {
realSrc.value = '';
realSrcList.value = [];
}
} else {
// 普通模式:直接使用src作为URL,但仍需要通过认证请求转换为blob URL
const urls = newSrc.split(',').filter((url) => url.trim());
const blobUrls = await getUrlsByDirectUrls(urls);
if (blobUrls.length > 0) {
realSrc.value = blobUrls[0];
realSrcList.value = blobUrls;
} else {
realSrc.value = '';
realSrcList.value = [];
}
}
} catch (error) {
realSrc.value = '';
realSrcList.value = [];
} finally {
loading.value = false;
}
},
{ immediate: true }
);
const realWidth = computed(() => (typeof props.width == 'string' ? props.width : `${props.width}px`));
@@ -68,12 +317,19 @@ const realHeight = computed(() => (typeof props.height == 'string' ? props.heigh
:deep(.image-slot) {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
color: #909399;
font-size: 30px;
.error-text {
font-size: 12px;
margin-top: 8px;
text-align: center;
}
}
}
</style>
@@ -0,0 +1,409 @@
<template>
<div class="component-upload-image">
<el-upload
ref="imageUploadRef"
multiple
:action="uploadImgUrl"
list-type="picture-card"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
:limit="limit"
:accept="fileAccept"
:on-error="handleUploadError"
:on-exceed="handleExceed"
:before-remove="handleDelete"
:show-file-list="true"
:headers="headers"
:file-list="fileList"
:on-preview="handlePictureCardPreview"
:class="{ hide: fileList.length >= limit }"
:http-request="customUpload"
>
<el-icon class="avatar-uploader-icon">
<plus />
</el-icon>
</el-upload>
<!-- 上传提示 -->
<div v-if="showTip" class="el-upload__tip">
请上传
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</template>
<template v-if="fileType">
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
</template>
的文件
</div>
<el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
<img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
</el-dialog>
<!-- Bucket密码输入对话框 -->
<el-dialog v-model="passwordDialogVisible" title="Bucket访问密码" width="400px" append-to-body>
<el-form :model="passwordForm" label-width="100px">
<el-form-item label="Bucket密码" required>
<el-input
v-model="passwordForm.bucketPassword"
type="password"
placeholder="请输入bucket访问密码"
show-password
@keyup.enter="confirmPassword"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="passwordDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmPassword">确认</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, getCurrentInstance } from 'vue';
import { compressAccurately } from 'image-conversion';
import request from '@/utils/request';
// 定义props类型
interface Props {
modelValue?: string | any[] | any;
limit?: number;
fileSize?: number;
fileType?: string[];
isShowTip?: boolean;
compressSupport?: boolean;
compressTargetSize?: number;
bucketPassword?: string;
enablePasswordValidation?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => [],
limit: 15,
fileSize: 10,
fileType: () => ['png', 'jpg', 'jpeg'],
isShowTip: true,
compressSupport: false,
compressTargetSize: 300,
bucketPassword: '',
enablePasswordValidation: false
});
const { proxy } = getCurrentInstance() as any;
const emit = defineEmits(['update:modelValue']);
const number = ref(0);
const uploadList = ref<any[]>([]);
const dialogImageUrl = ref('');
const dialogVisible = ref(false);
// 密码相关
const passwordDialogVisible = ref(false);
const passwordForm = ref({
bucketPassword: ''
});
const pendingUploadFile = ref<any>(null);
// 上传的图片服务器地址(用于el-upload组件的action属性)
const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload');
// 设置正确的headers(用于el-upload组件)
const headers = ref({
'Authorization': 'Bearer ' + (localStorage.getItem('token') || ''),
'clientid': import.meta.env.VITE_APP_CLIENT_ID || 'e5cd7e4891bf95d1d19206ce24a7b32e',
'Content-Language': localStorage.getItem('language') || 'zh-CN',
'referer': window.location.origin + '/login?redirect=' + encodeURIComponent(window.location.pathname)
});
const fileList = ref<any[]>([]);
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
const imageUploadRef = ref();
// 监听 fileType 变化,更新 fileAccept
const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(','));
watch(
() => props.modelValue,
async (val: any) => {
if (val) {
// 首先将值转为数组
let list: any[] = [];
if (Array.isArray(val)) {
list = val;
} else if (typeof val === 'string') {
// 检查是否包含逗号分隔的URL
if (val.includes(',')) {
// 多个URL用逗号分隔
const urls = val.split(',').filter((url) => url.trim());
list = urls.map((url, index) => ({
ossId: url,
url: url.trim(),
originalName: `image_${index + 1}`
}));
} else if (val.startsWith('http://') || val.startsWith('https://')) {
// 单个URL
list = [{ ossId: val, url: val, originalName: 'image' }];
} else {
// 如果是OSS ID,暂时跳过接口调用
list = [{ ossId: val, url: val, originalName: 'image' }];
}
}
// 然后将数组转为对象数组
fileList.value = list.map((item, index) => {
// 字符串回显处理 如果此处存的是url可直接回显 如果存的是id需要调用接口查出来
let itemData;
if (typeof item === 'string') {
itemData = { name: item, url: item };
} else {
// 此处name使用url 防止删除出现重名,对于URL格式使用url_index的格式
const name = item.url && (item.url.startsWith('http://') || item.url.startsWith('https://')) ? `${item.url}_${index}` : item.ossId;
itemData = { name: name, url: item.url, ossId: item.ossId };
}
return itemData;
});
} else {
fileList.value = [];
return [];
}
},
{ deep: true, immediate: true }
);
/** 上传前loading加载 */
const handleBeforeUpload = (file: any) => {
let isImg = false;
if (props.fileType.length) {
let fileExtension = '';
if (file.name.lastIndexOf('.') > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1);
}
isImg = props.fileType.some((type: any) => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
});
} else {
isImg = file.type.indexOf('image') > -1;
}
if (!isImg) {
proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}图片格式文件!`);
return false;
}
if (file.name.includes(',')) {
proxy?.$modal.msgError('文件名不正确,不能包含英文逗号!');
return false;
}
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
proxy?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
return false;
}
}
//压缩图片,开启压缩并且大于指定的压缩大小时才压缩
if (props.compressSupport && file.size / 1024 > props.compressTargetSize) {
proxy?.$modal.loading('正在上传图片,请稍候...');
number.value++;
return compressAccurately(file, props.compressTargetSize);
} else {
proxy?.$modal.loading('正在上传图片,请稍候...');
number.value++;
}
};
// 文件个数超出
const handleExceed = () => {
proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
};
// 自定义上传方法
const customUpload = async (options: any) => {
try {
// 检查是否需要bucket密码验证
if (props.enablePasswordValidation && !props.bucketPassword) {
// 显示密码输入对话框
pendingUploadFile.value = options;
passwordDialogVisible.value = true;
return;
}
// 添加小延迟,避免并发请求冲突
await new Promise((resolve) => setTimeout(resolve, 100));
// 直接上传
await performUpload(options);
} catch (error) {
console.error('上传失败6:', error);
proxy?.$modal.msgError('上传失败7');
proxy?.$modal.closeLoading();
}
};
// 确认密码
const confirmPassword = async () => {
if (!passwordForm.value.bucketPassword) {
proxy?.$modal.msgError('请输入bucket访问密码');
return;
}
try {
// 密码验证成功,执行上传
if (pendingUploadFile.value) {
await performUpload(pendingUploadFile.value);
}
// 关闭对话框
passwordDialogVisible.value = false;
passwordForm.value.bucketPassword = '';
pendingUploadFile.value = null;
} catch (error) {
console.error('密码验证失败:', error);
proxy?.$modal.msgError('密码验证失败');
}
};
// 执行上传
const performUpload = async (options: any) => {
try {
const formData = new FormData();
formData.append('file', options.file);
// 添加bucket密码到FormData中
if (props.bucketPassword || passwordForm.value.bucketPassword) {
formData.append('bucket-password', props.bucketPassword || passwordForm.value.bucketPassword || '');
}
// 使用request方式调用接口,禁用防重复提交检查
const result = await request.post('/resource/oss/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
'repeatSubmit': false // 禁用防重复提交检查
}
});
if (result.code === 200) {
// 上传成功
const uploadResult = {
name: result.data.fileName,
url: result.data.url,
ossId: result.data.ossId
};
uploadList.value.push(uploadResult);
uploadedSuccessfully();
} else {
throw new Error(result.msg || '上传失败1');
}
} catch (error: any) {
console.error('上传失败2:', error);
// 处理防重复提交错误
if (error.message && error.message.includes('数据正在处理,请勿重复提交')) {
proxy?.$modal.msgError('上传过于频繁,请稍后再试');
} else {
proxy?.$modal.msgError(error.message || '上传失败3');
}
proxy?.$modal.closeLoading();
}
};
// 上传成功回调
const handleUploadSuccess = (res: any, file: any) => {
// 确保res存在且有正确的结构
if (res && res.code === 200) {
// 避免重复添加,检查是否已经存在
const existingIndex = uploadList.value.findIndex((item) => item.name === res.data.fileName || item.ossId === res.data.ossId);
if (existingIndex === -1) {
uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
uploadedSuccessfully();
}
} else {
number.value--;
proxy?.$modal.closeLoading();
proxy?.$modal.msgError(res?.msg || '上传失败4');
if (file && imageUploadRef.value) {
imageUploadRef.value?.handleRemove(file);
}
uploadedSuccessfully();
}
};
// 删除图片
const handleDelete = (file: any): boolean => {
const findex = fileList.value.map((f) => f.name).indexOf(file.name);
if (findex > -1 && uploadList.value.length === number.value) {
const ossId = fileList.value[findex].ossId;
// 这里可以调用删除接口
fileList.value.splice(findex, 1);
emit('update:modelValue', listToString(fileList.value));
return false;
}
return true;
};
// 上传结束处理
const uploadedSuccessfully = () => {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter((f) => f.ossId !== undefined).concat(uploadList.value);
uploadList.value = [];
number.value = 0;
emit('update:modelValue', listToString(fileList.value));
proxy?.$modal.closeLoading();
}
};
// 上传失败5
const handleUploadError = () => {
proxy?.$modal.msgError('上传图片失败');
proxy?.$modal.closeLoading();
};
// 预览 - 使用安全预览
const handlePictureCardPreview = async (file: any) => {
try {
if (file.ossId) {
// 使用安全预览,这里简化处理
dialogImageUrl.value = file.url;
} else {
// 直接使用URL预览
dialogImageUrl.value = file.url;
}
dialogVisible.value = true;
} catch (error) {
console.error('预览失败:', error);
proxy?.$modal.msgError('预览失败');
}
};
// 对象转成指定字符串分隔
const listToString = (list: any[], separator?: string) => {
let strs = '';
separator = separator || ',';
for (const i in list) {
if (undefined !== list[i].ossId && list[i].ossId.indexOf('blob:') !== 0) {
// 返回ossId
strs += list[i].ossId + separator;
}
}
return strs != '' ? strs.substring(0, strs.length - 1) : '';
};
</script>
<style lang="scss" scoped>
// .el-upload--picture-card 控制加号部分
:deep(.hide .el-upload--picture-card) {
display: none;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>
+584 -46
View File
@@ -35,17 +35,32 @@
</div>
<el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
<img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
<div v-loading="previewLoading" style="text-align: center; min-height: 200px">
<img
v-if="dialogImageUrl"
:src="dialogImageUrl"
style="display: block; max-width: 100%; margin: 0 auto"
@error="handlePreviewError"
@load="handlePreviewLoad"
/>
<div v-if="previewError" class="preview-error">
<el-icon size="48"><picture-filled /></el-icon>
<div class="error-text">图片预览失败</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick, getCurrentInstance, onBeforeUnmount, type ComponentInternalInstance } from 'vue';
import { listByIds, delOss } from '@/api/system/oss';
import { OssVO } from '@/api/system/oss/types';
import { propTypes } from '@/utils/propTypes';
import { globalHeaders } from '@/utils/request';
import { compressAccurately } from 'image-conversion';
import { PictureFilled, ZoomIn, Delete } from '@element-plus/icons-vue';
import request from '@/utils/request';
const props = defineProps({
modelValue: {
@@ -79,6 +94,17 @@ const uploadList = ref<any[]>([]);
const dialogImageUrl = ref('');
const dialogVisible = ref(false);
// 预览相关状态
const previewLoading = ref(false);
const previewError = ref(false);
const previewUrlCache = ref<Map<string, string>>(new Map());
const previewBlobUrls = ref<string[]>([]);
// 异步加载相关状态
const loadingStates = ref<Map<string, boolean>>(new Map());
const loadErrorStates = ref<Map<string, boolean>>(new Map());
// 使用环境变量
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadImgUrl = ref(baseUrl + '/resource/oss/upload'); // 上传的图片服务器地址
const headers = ref(globalHeaders());
@@ -86,34 +112,280 @@ const headers = ref(globalHeaders());
const fileList = ref<any[]>([]);
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
const imageUploadRef = ref<ElUploadInstance>();
const imageUploadRef = ref<any>();
// 添加标志位,避免上传成功后重复处理
const isInternalUpdate = ref(false);
// 监听 fileType 变化,更新 fileAccept
const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(','));
// 清理预览blob URL,防止内存泄漏
const cleanupPreviewBlobUrls = () => {
previewBlobUrls.value.forEach((url) => {
URL.revokeObjectURL(url);
});
previewBlobUrls.value = [];
previewUrlCache.value.clear();
};
// 组件卸载时清理blob URLs
onBeforeUnmount(() => {
cleanupPreviewBlobUrls();
});
// 验证blob数据是否为有效图片
const validateImageBlob = (blob: Blob): boolean => {
// 允许的图片类型,包括通用的octet-stream类型(OSS服务器常返回此类型)
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'application/octet-stream'];
// 基本验证:类型检查和大小检查
const hasValidType = validTypes.includes(blob.type) || blob.type === '' || blob.type === 'application/octet-stream';
const hasValidSize = blob.size > 0;
return hasValidType && hasValidSize;
};
// 通过认证请求获取图片数据并转换为blob URL
const getPreviewImageBlobUrl = async (imageUrl: string): Promise<string> => {
try {
// 检查是否已有缓存的blob URL
const cachedUrl = previewUrlCache.value.get(imageUrl);
if (cachedUrl) {
return cachedUrl;
}
// 通过request请求图片数据,这样会自动带上token和clientId
const response = await request({
url: imageUrl,
method: 'get',
responseType: 'blob',
timeout: 300000 // 300秒超时
});
// 由于request响应拦截器对blob类型响应直接返回blob数据
// 这里response直接就是blob对象,而不是包含data属性的响应对象
const blob = response;
// 验证响应数据
if (!blob) {
throw new Error('响应数据为空');
}
if (!(blob instanceof Blob)) {
// 如果不是Blob,尝试获取实际内容用于调试
let actualContent = 'Unknown';
try {
if (typeof blob === 'string') {
actualContent = (blob as string).substring(0, 200);
} else if (blob && typeof blob.toString === 'function') {
actualContent = blob.toString().substring(0, 200);
}
} catch (e) {
actualContent = 'Cannot convert to string';
}
throw new Error(`响应数据不是有效的Blob对象。实际类型: ${typeof blob}, 内容: ${actualContent}`);
}
// 检查blob内容类型
// const contentType = blob.type;
// 如果Content-Type明确表明这不是图片数据且不是通用二进制格式,进行检查
// if (
// contentType &&
// !contentType.toLowerCase().startsWith('image/') &&
// !contentType.toLowerCase().includes('octet-stream') &&
// (contentType.toLowerCase().includes('text/') || contentType.toLowerCase().includes('html'))
// ) {
// const text = await blob.text();
// const preview = text.substring(0, 300);
// throw new Error(`响应Content-Type不是图片格式: ${contentType}。响应内容预览: ${preview}`);
// }
// 验证是否为有效图片
// if (!validateImageBlob(blob)) {
// throw new Error(`无效的图片格式: ${blob.type}, 大小: ${blob.size}`);
// }
// 创建blob URL
const blobUrl = URL.createObjectURL(blob);
// 缓存blob URL
previewUrlCache.value.set(imageUrl, blobUrl);
previewBlobUrls.value.push(blobUrl);
return blobUrl;
} catch (error) {
console.error('获取预览图片失败:', error);
return '';
}
};
// 通过ossid获取预览URL
const getPreviewUrlByOssId = async (ossId: string): Promise<string> => {
try {
// const response = await listByIds(ossId);
// // RuoYi框架的响应格式:{code, msg, data},实际数据在data字段中
// const ossList: OssVO[] = (response as any)?.data || response;
// if (!ossList || ossList.length === 0) {
// throw new Error(`OSS ID ${ossId} 在数据库中不存在或已被删除`);
// }
// const ossFile = ossList[0];
// if (!ossFile || !ossFile.url) {
// throw new Error(`OSS文件缺少url属性`);
// }
// const imageUrl = ossFile.url;
// // 检查URL是否有效
// if (!imageUrl || typeof imageUrl !== 'string') {
// throw new Error(`OSS接口返回的URL无效`);
// }
// 获取认证的blob URL
return await getPreviewImageBlobUrl('/resource/oss/download/' + ossId);
} catch (error) {
console.error('通过ossId获取预览图片失败:', error);
throw error;
}
};
// 异步加载单个文件的图片
const loadFileImageAsync = async (item: any, index: number): Promise<void> => {
const fileKey = item.ossId || item.url || `file_${index}`;
// 设置加载状态
loadingStates.value.set(fileKey, true);
loadErrorStates.value.set(fileKey, false);
try {
let blobUrl = '';
if (item.ossId) {
// 优先通过OSS ID获取预览URL
console.log(`loadFileImageAsync: 通过OSS ID获取预览URL for ${item.ossId}`);
blobUrl = await getPreviewUrlByOssId(item.ossId);
} else if (typeof item === 'string') {
// 直接URL情况
console.log(`loadFileImageAsync: 直接URL情况 for ${item}`);
blobUrl = await getPreviewImageBlobUrl(item);
} else if (item.url) {
// OSS对象情况(降级处理)
console.log(`loadFileImageAsync: OSS对象情况 for ${item.url}`);
blobUrl = await getPreviewImageBlobUrl(item.url);
}
if (blobUrl) {
// 更新文件列表中的对应项
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId || f.url === item.url || f.originalUrl === item.originalUrl);
if (fileIndex !== -1) {
fileList.value[fileIndex].url = blobUrl;
fileList.value[fileIndex].isLoaded = true;
fileList.value[fileIndex].isPlaceholder = false;
console.log(`loadFileImageAsync: 成功更新文件 ${item.ossId || item.url}`);
}
} else {
throw new Error('获取图片blob URL失败');
}
} catch (error) {
console.error('异步加载图片失败:', error);
loadErrorStates.value.set(fileKey, true);
// 降级处理:尝试通过OSS ID重新获取
if (item.ossId) {
try {
console.log(`loadFileImageAsync: 降级处理,重新通过OSS ID获取 ${item.ossId}`);
const fallbackBlobUrl = await getPreviewUrlByOssId(item.ossId);
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId);
if (fileIndex !== -1 && fallbackBlobUrl) {
fileList.value[fileIndex].url = fallbackBlobUrl;
fileList.value[fileIndex].isLoaded = true;
fileList.value[fileIndex].isPlaceholder = false;
console.log(`loadFileImageAsync: 降级处理成功 for ${item.ossId}`);
}
} catch (fallbackError) {
console.error('loadFileImageAsync: 降级处理也失败:', fallbackError);
}
}
} finally {
loadingStates.value.set(fileKey, false);
}
};
// 批量异步加载图片
const loadImagesAsync = async (items: any[]): Promise<void> => {
// 先创建占位框,使用一个占位图片URL
const placeholderItems = items.map((item, index) => {
const name =
item.url && (item.url.startsWith('http://') || item.url.startsWith('https://')) ? `${item.url}_${index}` : item.ossId || `file_${index}`;
return {
name: name,
url: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjZjVmN2ZhIi8+CjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIHI9IjE1IiBzdHJva2U9IiM5MDkzOTkiIHN0cm9rZS13aWR0aD0iMiIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtZGFzaGFycmF5PSI2LCAzIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KCTxhbmltYXRlVHJhbnNmb3JtIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgdHlwZT0icm90YXRlIiB2YWx1ZXM9IjAgNTAgNTA7MzYwIDUwIDUwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIvPgo8L2NpcmNsZT4KPC9zdmc+', // 小圆圈浅灰色加载中占位图片
originalUrl: item.url,
ossId: item.ossId,
isLoaded: false, // 标记为未加载
isPlaceholder: true // 标记为占位符
};
});
// 立即更新文件列表显示占位框
fileList.value = placeholderItems;
// 异步加载所有图片
const loadPromises = items.map((item, index) => loadFileImageAsync(item, index));
// 等待所有图片加载完成(使用Promise.all替代allSettled以兼容旧版本)
await Promise.all(loadPromises);
};
// 图片预览加载成功
const handlePreviewLoad = () => {
previewError.value = false;
};
// 图片预览加载失败
const handlePreviewError = () => {
previewError.value = true;
};
watch(
() => props.modelValue,
async (val: string) => {
// 如果是内部更新触发的,跳过处理
if (isInternalUpdate.value) {
return;
}
if (val) {
// 首先将值转为数组
let list: OssVO[] = [];
if (Array.isArray(val)) {
list = val as OssVO[];
} else if (typeof val === 'string') {
// 检查是否包含逗号分隔的URL
console.log('val', val);
// 检查是否包含逗号分隔的OSS ID
if (val.includes(',')) {
// 多个URL用逗号分隔
const urls = val.split(',').filter((url) => url.trim());
list = urls.map((url, index) => ({
ossId: url,
url: url.trim(),
originalName: `image_${index + 1}`
})) as any;
// 多个OSS ID用逗号分隔,需要调用接口获取详细信息
const ossIds = val.split(',').filter((id) => id.trim());
try {
const res = await listByIds(ossIds.join(','));
list = res.data;
} catch (error) {
console.error('调用listByIds接口失败:', error);
list = [];
}
} else if (val.startsWith('http://') || val.startsWith('https://')) {
// 单个URL
list = [{ ossId: val, url: val, originalName: 'image' }] as any;
} else {
// 如果是OSS ID,调用接口获取详细信息
// 单个OSS ID,调用接口获取详细信息
try {
const res = await listByIds(val);
list = res.data;
@@ -123,19 +395,26 @@ watch(
}
}
}
// 然后将数组转为对象数组
fileList.value = list.map((item, index) => {
// 字符串回显处理 如果此处存的是url可直接回显 如果存的是id需要调用接口查出来
let itemData;
if (typeof item === 'string') {
itemData = { name: item, url: item };
} else {
// 此处name使用url 防止删除出现重名,对于URL格式使用url_index的格式
const name = item.url && (item.url.startsWith('http://') || item.url.startsWith('https://')) ? `${item.url}_${index}` : item.ossId;
itemData = { name: name, url: item.url, ossId: item.ossId };
}
return itemData;
});
// 使用异步加载方式处理文件列表
try {
await loadImagesAsync(list);
console.log('fileList with async loaded images', fileList.value);
} catch (error) {
console.error('异步加载文件列表失败:', error);
// 降级处理:使用原始URL
fileList.value = list.map((item, index) => {
let itemData;
if (typeof item === 'string') {
itemData = { name: item, url: item, originalUrl: item, isLoaded: true };
} else {
const name = item.url && (item.url.startsWith('http://') || item.url.startsWith('https://')) ? `${item.url}_${index}` : item.ossId;
itemData = { name: name, url: item.url, originalUrl: item.url, ossId: item.ossId, isLoaded: true };
}
return itemData;
});
console.log('fileList (fallback)', fileList.value);
}
} else {
fileList.value = [];
return [];
@@ -161,85 +440,261 @@ const handleBeforeUpload = (file: any) => {
isImg = file.type.indexOf('image') > -1;
}
if (!isImg) {
proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}图片格式文件!`);
(proxy as any)?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}图片格式文件!`);
return false;
}
if (file.name.includes(',')) {
proxy?.$modal.msgError('文件名不正确,不能包含英文逗号!');
(proxy as any)?.$modal.msgError('文件名不正确,不能包含英文逗号!');
return false;
}
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
proxy?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
(proxy as any)?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
return false;
}
}
//压缩图片,开启压缩并且大于指定的压缩大小时才压缩
if (props.compressSupport && file.size / 1024 > props.compressTargetSize) {
proxy?.$modal.loading('正在上传图片,请稍候...');
(proxy as any)?.$modal.loading('正在上传图片,请稍候...');
number.value++;
return compressAccurately(file, props.compressTargetSize);
} else {
proxy?.$modal.loading('正在上传图片,请稍候...');
(proxy as any)?.$modal.loading('正在上传图片,请稍候...');
number.value++;
}
};
// 文件个数超出
const handleExceed = () => {
proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
(proxy as any)?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
};
// 上传成功回调
const handleUploadSuccess = (res: any, file: UploadFile) => {
const handleUploadSuccess = (res: any, file: any) => {
console.log('handleUploadSuccess:res', res);
console.log('handleUploadSuccess:file', file);
if (res.code === 200) {
uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
const uploadItem = {
name: res.data.fileName,
url: res.data.url,
ossId: res.data.ossId
};
console.log('添加上传项到uploadList:', uploadItem);
uploadList.value.push(uploadItem);
uploadedSuccessfully();
} else {
number.value--;
proxy?.$modal.closeLoading();
proxy?.$modal.msgError(res.msg);
(proxy as any)?.$modal.closeLoading();
(proxy as any)?.$modal.msgError(res.msg);
imageUploadRef.value?.handleRemove(file);
uploadedSuccessfully();
}
};
// 删除图片
const handleDelete = (file: UploadFile): boolean => {
const handleDelete = (file: any): boolean => {
const findex = fileList.value.map((f) => f.name).indexOf(file.name);
if (findex > -1 && uploadList.value.length === number.value) {
const ossId = fileList.value[findex].ossId;
delOss(ossId);
if (ossId) {
// delOss(ossId);
}
// 清理对应的blob URL
const fileToDelete = fileList.value[findex];
if (fileToDelete.url && fileToDelete.url.startsWith('blob:')) {
URL.revokeObjectURL(fileToDelete.url);
}
fileList.value.splice(findex, 1);
// 设置内部更新标志
isInternalUpdate.value = true;
emit('update:modelValue', listToString(fileList.value));
// 重置标志
nextTick(() => {
isInternalUpdate.value = false;
});
return false;
}
return true;
};
// 上传结束处理
const uploadedSuccessfully = () => {
const uploadedSuccessfully = async () => {
console.log('uploadedSuccessfully called, number:', number.value, 'uploadList length:', uploadList.value.length);
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
console.log('开始处理新上传的文件:', uploadList.value);
// 对新上传的文件进行异步加载处理
const newUploadedFiles = uploadList.value.map((item) => ({
...item,
isLoaded: false,
isPlaceholder: false
}));
console.log('处理后的新上传文件:', newUploadedFiles);
// 先显示新上传的文件(使用占位符)
const placeholderItems = newUploadedFiles.map((item, index) => ({
name: item.name,
url: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjZjVmN2ZhIi8+CjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIHI9IjE1IiBzdHJva2U9IiM5MDkzOTkiIHN0cm9rZS13aWR0aD0iMiIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtZGFzaGFycmF5PSI2LCAzIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KCTxhbmltYXRlVHJhbnNmb3JtIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgdHlwZT0icm90YXRlIiB2YWx1ZXM9IjAgNTAgNTA7MzYwIDUwIDUwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIvPgo8L2NpcmNsZT4KPC9zdmc+',
originalUrl: item.url,
ossId: item.ossId,
isLoaded: false,
isPlaceholder: true
}));
console.log('占位符项目:', placeholderItems);
// 合并现有文件和新上传的文件
const existingFiles = fileList.value.filter((f) => f.url !== undefined);
console.log('现有文件:', existingFiles);
fileList.value = existingFiles.concat(placeholderItems);
console.log('合并后的文件列表:', fileList.value);
// 异步加载新上传的图片
const loadPromises = newUploadedFiles.map((item, index) => {
const fileKey = item.ossId || item.url || `file_${index}`;
console.log(`开始加载文件 ${index}:`, item);
// 设置加载状态
loadingStates.value.set(fileKey, true);
loadErrorStates.value.set(fileKey, false);
return (async () => {
try {
let blobUrl = '';
if (item.ossId) {
console.log(`通过OSS ID获取预览URL for ${item.ossId}`);
// 通过OSS ID获取预览URL
blobUrl = await getPreviewUrlByOssId(item.ossId);
console.log(`获取到的blob URL for ${item.ossId}:`, blobUrl);
} else if (item.url) {
console.log(`通过URL获取blob URL for ${item.ossId}:`, item.url);
// 直接URL情况(降级处理)
blobUrl = await getPreviewImageBlobUrl(item.url);
console.log(`获取到的blob URL for ${item.ossId}:`, blobUrl);
}
if (blobUrl) {
// 更新文件列表中的对应项
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId);
console.log(`更新文件列表索引 ${fileIndex} for ${item.ossId}`);
if (fileIndex !== -1) {
fileList.value[fileIndex].url = blobUrl;
fileList.value[fileIndex].isLoaded = true;
fileList.value[fileIndex].isPlaceholder = false;
console.log(`成功更新文件 ${item.ossId}`);
}
} else {
throw new Error('获取图片blob URL失败');
}
} catch (error) {
console.error('异步加载新上传图片失败:', error);
loadErrorStates.value.set(fileKey, true);
// 降级处理:通过OSS ID重新获取
try {
console.log(`降级处理:重新通过OSS ID获取 ${item.ossId}`);
const fallbackBlobUrl = await getPreviewUrlByOssId(item.ossId);
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId);
if (fileIndex !== -1 && fallbackBlobUrl) {
fileList.value[fileIndex].url = fallbackBlobUrl;
fileList.value[fileIndex].isLoaded = true;
fileList.value[fileIndex].isPlaceholder = false;
console.log(`降级处理成功,通过OSS ID重新获取 for ${item.ossId}`);
} else {
// 最后的降级:使用原始URL
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId);
if (fileIndex !== -1) {
fileList.value[fileIndex].url = item.url;
fileList.value[fileIndex].isLoaded = true;
fileList.value[fileIndex].isPlaceholder = false;
console.log(`最终降级处理,使用原始URL for ${item.ossId}`);
}
}
} catch (fallbackError) {
console.error('降级处理也失败:', fallbackError);
// 最后的降级:使用原始URL
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId);
if (fileIndex !== -1) {
fileList.value[fileIndex].url = item.url;
fileList.value[fileIndex].isLoaded = true;
fileList.value[fileIndex].isPlaceholder = false;
console.log(`最终降级处理,使用原始URL for ${item.ossId}`);
}
}
} finally {
loadingStates.value.set(fileKey, false);
}
})();
});
// 等待所有图片加载完成
await Promise.all(loadPromises);
console.log('所有图片加载完成,最终文件列表:', fileList.value);
uploadList.value = [];
number.value = 0;
// 设置内部更新标志
isInternalUpdate.value = true;
emit('update:modelValue', listToString(fileList.value));
proxy?.$modal.closeLoading();
// 重置标志
nextTick(() => {
isInternalUpdate.value = false;
});
(proxy as any)?.$modal.closeLoading();
}
};
// 上传失败
const handleUploadError = () => {
proxy?.$modal.msgError('上传图片失败');
proxy?.$modal.closeLoading();
(proxy as any)?.$modal.msgError('上传图片失败');
(proxy as any)?.$modal.closeLoading();
};
// 预览
const handlePictureCardPreview = (file: any) => {
dialogImageUrl.value = file.url;
const handlePictureCardPreview = async (file: any) => {
console.log('预览文件:', file);
previewLoading.value = true;
previewError.value = false;
dialogVisible.value = true;
try {
let previewUrl = '';
// 判断是否为ossId
if (file.ossId) {
// 通过ossId获取预览URL
previewUrl = await getPreviewUrlByOssId(file.ossId);
} else if (file.originalUrl) {
// 使用原始URL进行预览(避免重复转换)
previewUrl = await getPreviewImageBlobUrl(file.originalUrl);
} else if (file.url) {
// 直接URL预览
previewUrl = await getPreviewImageBlobUrl(file.url);
} else {
throw new Error('文件缺少必要的URL或ossId信息');
}
dialogImageUrl.value = previewUrl;
} catch (error) {
console.error('图片预览失败:', error);
previewError.value = true;
dialogImageUrl.value = '';
(proxy as any)?.$modal.msgError('图片预览失败');
} finally {
previewLoading.value = false;
}
};
// 对象转成指定字符串分隔
@@ -247,9 +702,9 @@ const listToString = (list: any[], separator?: string) => {
let strs = '';
separator = separator || ',';
for (const i in list) {
if (undefined !== list[i].url && list[i].url.indexOf('blob:') !== 0) {
// 始终返回URL而不是ossId
strs += list[i].url + separator;
if (undefined !== list[i].ossId && list[i].ossId.indexOf('blob:') !== 0) {
// 返回ossId用于数据存储
strs += list[i].ossId + separator;
}
}
return strs != '' ? strs.substring(0, strs.length - 1) : '';
@@ -261,4 +716,87 @@ const listToString = (list: any[], separator?: string) => {
:deep(.hide .el-upload--picture-card) {
display: none;
}
// 预览错误状态样式
.preview-error {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #909399;
min-height: 200px;
.error-text {
font-size: 14px;
margin-top: 8px;
text-align: center;
}
}
// 异步加载优化样式
:deep(.el-upload-list--picture-card .el-upload-list__item) {
// 占位图片加载时的样式
.el-upload-list__item-thumbnail {
transition: opacity 0.3s ease;
&.loading {
opacity: 0.6;
}
}
// 占位符图片样式优化
&.placeholder-item {
.el-upload-list__item-thumbnail {
background-color: #f5f7fa;
border: 1px dashed #d9d9d9;
display: flex;
align-items: center;
justify-content: center;
img {
opacity: 0.7;
transition: opacity 0.3s ease;
}
}
&:hover .el-upload-list__item-thumbnail img {
opacity: 1;
}
}
// 加载完成后的样式
&.loaded-item {
.el-upload-list__item-thumbnail {
img {
opacity: 1;
transition: opacity 0.3s ease;
}
}
}
}
// 加载动画
@keyframes loading-rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
// 占位符加载动画
@keyframes placeholder-pulse {
0%,
100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}
.placeholder-loading {
animation: placeholder-pulse 1.5s ease-in-out infinite;
}
</style>
+461
View File
@@ -0,0 +1,461 @@
<template>
<div class="poster-preview">
<!-- 海报画布容器 -->
<div
ref="posterRef"
class="poster-canvas"
:style="canvasStyle"
>
<!-- 企业信息叠加 -->
<div class="info-overlay">
<!-- 顶部企业标题 -->
<div class="company-header">
<h1 class="company-name">{{ posterData.enterpriseName }}</h1>
<div class="company-tagline">诚聘英才 · 共创未来</div>
</div>
<!-- 中间内容区域 - 根据海报类型显示不同内容 -->
<!-- 企业海报:展示企业文化和优势 -->
<div v-if="posterData.posterType === '1'" class="position-info">
<div class="position-title">加入我们 · 共创辉煌</div>
<div v-if="enterpriseDetail" class="position-details">
<br />
<br />
<br />
<br />
<br />
<br />
<div class="detail-item" v-if="enterpriseDetail.mainBusiness">
<span class="icon">🏢</span>
<span>{{ enterpriseDetail.mainBusiness }}</span>
</div>
<div class="detail-item" v-if="enterpriseDetail.enterpriseScale">
<span class="icon">👥</span>
<span>{{ getScaleLabel(enterpriseDetail.enterpriseScale) }}</span>
</div>
<div class="detail-item" v-if="enterpriseDetail.address">
<span class="icon">📍</span>
<span>{{ enterpriseDetail.address }}</span>
</div>
</div>
</div>
<!-- 岗位海报:展示具体岗位信息 -->
<div v-else-if="posterData.posterType === '2'" class="position-info">
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<div class="position-title">
{{ posterData.positionName || positionDetail?.positionName || '招聘岗位' }}
</div>
<div v-if="positionDetail && Object.keys(positionDetail).length > 0" class="position-details">
<div class="position-details-grid">
<div class="detail-item" v-if="positionDetail.workAddress">
<span class="icon">📍</span>
<span>{{ positionDetail.workAddress }}</span>
</div>
<div class="detail-item" v-if="positionDetail.salary">
<span class="icon">💰</span>
<span>{{ positionDetail.salary }}</span>
</div>
<div class="detail-item" v-if="positionDetail.workTime">
<span class="icon">⏰</span>
<span>{{ positionDetail.workTime }}</span>
</div>
<div class="detail-item" v-if="positionDetail.requiredSkills">
<span class="icon">🎯</span>
<span>{{ positionDetail.requiredSkills }}</span>
</div>
</div>
</div>
<!-- 如果没有岗位详情,显示提示 -->
<div v-else class="position-details">
<div class="detail-item">
<span class="icon">💼</span>
<span>岗位详情加载中或暂无数据</span>
</div>
</div>
</div>
<!-- 底部联系信息 - 根据海报类型显示不同内容 -->
<div class="company-footer">
<div v-if="enterpriseDetail" class="company-info">
<!-- 企业海报:重点显示联系方式 -->
<template v-if="posterData.posterType === '1'">
<div class="info-row" v-if="enterpriseDetail.contactPhone">
<span class="label">📞 联系电话:</span>
<span class="value">{{ enterpriseDetail.contactPhone }}</span>
</div>
<div class="info-row" v-if="enterpriseDetail.email">
<span class="label">📧 电子邮箱:</span>
<span class="value">{{ enterpriseDetail.email }}</span>
</div>
<div class="info-row" v-if="enterpriseDetail.address">
<span class="label">🏢 公司地址:</span>
<span class="value">{{ enterpriseDetail.address }}</span>
</div>
</template>
<!-- 岗位海报:显示企业背景信息 -->
<template v-else-if="posterData.posterType === '2'">
<div class="info-row" v-if="enterpriseDetail.mainBusiness">
<span class="label">主营业务:</span>
<span class="value">{{ enterpriseDetail.mainBusiness }}</span>
</div>
<div class="info-row" v-if="enterpriseDetail.enterpriseScale">
<span class="label">企业规模:</span>
<span class="value">{{ getScaleLabel(enterpriseDetail.enterpriseScale) }}</span>
</div>
<div class="info-row" v-if="enterpriseDetail.contactPhone">
<span class="label">联系电话:</span>
<span class="value">{{ enterpriseDetail.contactPhone }}</span>
</div>
<div class="info-row" v-if="enterpriseDetail.email">
<span class="label">电子邮箱:</span>
<span class="value">{{ enterpriseDetail.email }}</span>
</div>
</template>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<el-button type="primary" icon="Download" @click="exportToPDF" :loading="exporting">
导出PDF
</el-button>
<el-button icon="Picture" @click="exportToImage" :loading="exporting">
导出图片
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import html2canvas from 'html2canvas'
import jsPDF from 'jspdf'
import { ElMessage } from 'element-plus'
// 定义接口
interface PosterData {
posterPath: string
enterpriseName: string
positionName?: string
posterType: string
}
interface EnterpriseDetail {
mainBusiness?: string
enterpriseScale?: string
contactPhone?: string
email?: string
address?: string
}
interface PositionDetail {
positionName?: string
workAddress?: string
salary?: string
workTime?: string
requiredSkills?: string
}
// Props定义
const props = defineProps<{
posterData: PosterData
enterpriseDetail?: EnterpriseDetail
positionDetail?: PositionDetail
}>()
// 响应式数据
const posterRef = ref<HTMLElement>()
const exporting = ref(false)
// 计算样式
const canvasStyle = computed(() => ({
backgroundImage: `url(${props.posterData.posterPath})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat'
}))
// 企业规模字典
const scaleDict = {
'1': '50人以下',
'2': '50-200人',
'3': '200-500人',
'4': '500-1000人',
'5': '1000人以上'
}
// 获取规模标签
const getScaleLabel = (value: string) => {
return scaleDict[value as keyof typeof scaleDict] || value
}
// 导出为PDF
const exportToPDF = async () => {
if (!posterRef.value) return
try {
exporting.value = true
const canvas = await html2canvas(posterRef.value, {
backgroundColor: null,
scale: 2,
useCORS: true,
allowTaint: true,
foreignObjectRendering: false
})
const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4'
})
const imgWidth = 190
const imgHeight = (canvas.height * imgWidth) / canvas.width
pdf.addImage(imgData, 'PNG', 10, 10, imgWidth, imgHeight)
const fileName = `${props.posterData.enterpriseName}_招聘海报_${new Date().getTime()}.pdf`
pdf.save(fileName)
ElMessage.success('PDF导出成功')
} catch (error) {
console.error('PDF导出失败:', error)
ElMessage.error('PDF导出失败,请重试')
} finally {
exporting.value = false
}
}
// 导出为图片
const exportToImage = async () => {
if (!posterRef.value) return
try {
exporting.value = true
const canvas = await html2canvas(posterRef.value, {
backgroundColor: null,
scale: 2,
useCORS: true,
allowTaint: true
})
const link = document.createElement('a')
link.download = `${props.posterData.enterpriseName}_招聘海报_${new Date().getTime()}.png`
link.href = canvas.toDataURL('image/png')
link.click()
ElMessage.success('图片导出成功')
} catch (error) {
console.error('图片导出失败:', error)
ElMessage.error('图片导出失败,请重试')
} finally {
exporting.value = false
}
}
</script>
<style scoped lang="scss">
.poster-preview {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.poster-canvas {
position: relative;
width: 600px;
height: 800px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.info-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 40px;
color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.3) 0%,
rgba(0, 0, 0, 0.1) 50%,
rgba(0, 0, 0, 0.4) 100%
);
}
.company-header {
text-align: center;
margin-bottom: 20px;
}
.company-name {
font-size: 48px;
font-weight: 700;
margin: 0 0 10px 0;
letter-spacing: 2px;
background: linear-gradient(135deg, #ffd700, #ffed4e);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: none;
filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8));
}
.company-tagline {
font-size: 18px;
font-weight: 300;
letter-spacing: 4px;
opacity: 0.9;
}
.position-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
.position-title {
font-size: 36px;
font-weight: 600;
margin-bottom: 30px;
background: linear-gradient(135deg, #ff6b6b, #ffd93d);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: none;
filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8));
}
.position-details {
display: flex;
flex-direction: column;
gap: 12px;
font-size: 14px;
max-width: 500px;
margin: 0 auto;
}
.position-details-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
width: 100%;
}
.detail-item {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: rgba(255, 255, 255, 0.1);
padding: 8px 15px;
border-radius: 20px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
font-size: 14px;
min-height: 36px;
}
.icon {
font-size: 16px;
margin-right: 3px;
flex-shrink: 0;
}
.company-footer {
margin-top: 20px;
}
.company-info {
background: rgba(0, 0, 0, 0.6);
padding: 25px;
border-radius: 15px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.info-row {
display: flex;
margin-bottom: 12px;
font-size: 16px;
}
.info-row:last-child {
margin-bottom: 0;
}
.label {
font-weight: 600;
min-width: 80px;
color: #ffd700;
}
.value {
flex: 1;
opacity: 0.9;
}
.action-buttons {
display: flex;
gap: 15px;
margin-top: 20px;
}
@media (max-width: 768px) {
.poster-canvas {
width: 90vw;
height: calc(90vw * 4 / 3);
max-width: 450px;
max-height: 600px;
}
.info-overlay {
padding: 20px;
}
.company-name {
font-size: 32px;
}
.position-title {
font-size: 24px;
}
.detail-item {
font-size: 14px;
padding: 8px 15px;
}
}
</style>
@@ -44,7 +44,7 @@
<el-table-column prop="originalName" width="202" :show-overflow-tooltip="true" label="附件名称"></el-table-column>
<el-table-column prop="name" width="80" align="center" :show-overflow-tooltip="true" label="操作">
<template #default="tool">
<el-button type="text" @click="handleDownload(tool.row.ossId)">下载</el-button>
<el-button type="text" @click="handleDownload(tool.row.ossId, tool.row.originalName)">下载</el-button>
</template>
</el-table-column>
</el-table>
@@ -106,8 +106,9 @@ const getIds = async (ids: string | number) => {
};
/** 下载按钮操作 */
const handleDownload = (ossId: string) => {
proxy?.$download.oss(ossId);
const handleDownload = (ossId: string, originalName: string) => {
console.log(ossId);
proxy?.$download.secureOss(ossId, originalName);
};
/**
+2 -2
View File
@@ -6,7 +6,7 @@
<div class="right-menu flex align-center">
<template v-if="appStore.device !== 'mobile'">
<el-select
<!-- <el-select
v-if="userId === 1 && tenantEnabled"
v-model="companyName"
class="min-w-244px"
@@ -19,7 +19,7 @@
>
<el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
<template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
</el-select>
</el-select> -->
<search-menu ref="searchMenuRef" />
<el-tooltip content="搜索" effect="dark" placement="bottom">
+55 -11
View File
@@ -4,27 +4,35 @@ import errorCode from '@/utils/errorCode';
import { blobValidate } from '@/utils/ruoyi';
import { LoadingInstance } from 'element-plus/es/components/loading/src/loading.mjs';
import { globalHeaders } from '@/utils/request';
import { getFileDownloadUrl } from '@/api/system/file';
import request from '@/utils/request';
const baseURL = import.meta.env.VITE_APP_BASE_API;
let downloadLoadingInstance: LoadingInstance;
export default {
async oss(ossId: string | number) {
const url = baseURL + '/resource/oss/download/' + ossId;
downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
downloadLoadingInstance = ElLoading.service({ text: '正在获取文件访问链接,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
try {
const res = await axios({
// 使用新的安全下载方法,避免Number()转换导致的大整数精度丢失
// const { data: downloadUrl } = await getFileDownloadUrl(String(ossId));
const response = await request({
url: "/resource/oss/download/" + ossId,
method: 'get',
url: url,
responseType: 'blob',
headers: globalHeaders()
timeout: 300000 // 300秒超时
});
const isBlob = blobValidate(res.data);
if (isBlob) {
const blob = new Blob([res.data], { type: 'application/octet-stream' });
FileSaver.saveAs(blob, decodeURIComponent(res.headers['download-filename'] as string));
} else {
this.printErrMsg(res.data);
// 由于axios响应拦截器对blob类型响应直接返回blob数据
// 这里response直接就是blob对象,而不是包含data属性的响应对象
const blob = response;
// 验证响应数据
if (!blob) {
throw new Error('响应数据为空');
}
const filename = response.headers.get('content-disposition')?.split('filename=')[1]?.replace(/"/g, '') || `file_${ossId}`;
FileSaver.saveAs(blob, decodeURIComponent(filename));
downloadLoadingInstance.close();
} catch (r) {
console.error(r);
@@ -32,6 +40,41 @@ export default {
downloadLoadingInstance.close();
}
},
// 新增:安全下载文件方法
async secureOss(ossId: string | number, fileName?: string) {
console.log(ossId, fileName);
downloadLoadingInstance = ElLoading.service({ text: '正在获取文件访问链接,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
try {
// 获取临时下载URL,避免Number()转换导致的大整数精度丢失
// const { data: downloadUrl } = await getFileDownloadUrl(String(ossId));
const response = await request({
url: "/resource/oss/download/" + ossId,
method: 'get',
responseType: 'blob',
timeout: 300000 // 300秒超时
});
// 由于axios响应拦截器对blob类型响应直接返回blob数据
// 这里response直接就是blob对象,而不是包含data属性的响应对象
const blob = response;
// 验证响应数据
if (!blob) {
throw new Error('响应数据为空');
}
const finalFileName = fileName || response.headers.get('content-disposition')?.split('filename=')[1]?.replace(/"/g, '') || `file_${ossId}`;
FileSaver.saveAs(blob, decodeURIComponent(finalFileName));
downloadLoadingInstance.close();
} catch (r) {
console.error(r);
ElMessage.error('下载文件出现错误,请联系管理员!');
downloadLoadingInstance.close();
}
},
async zip(url: string, name: string) {
url = baseURL + url;
downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
@@ -40,6 +83,7 @@ export default {
method: 'get',
url: url,
responseType: 'blob',
timeout: 300000 ,// 300秒超时
headers: globalHeaders()
});
const isBlob = blobValidate(res.data);
+2 -1
View File
@@ -6,7 +6,7 @@ import auth from './auth';
// 预设动画
import animate from '@/animate';
import { download as dl } from '@/utils/request';
import { download as dl, downloadJson as dlJson } from '@/utils/request';
import { useDict } from '@/utils/dict';
import { getConfigKey, updateConfigByKey } from '@/api/system/config';
import { parseTime, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi';
@@ -34,6 +34,7 @@ export default function installPlugin(app: App) {
app.config.globalProperties.getConfigKey = getConfigKey;
app.config.globalProperties.updateConfigByKey = updateConfigByKey;
app.config.globalProperties.download = dl;
app.config.globalProperties.downloadJson = dlJson;
app.config.globalProperties.parseTime = parseTime;
app.config.globalProperties.handleTree = handleTree;
app.config.globalProperties.addDateRange = addDateRange;
+7
View File
@@ -89,6 +89,7 @@ export const constantRoutes: RouteRecordRaw[] = [
}
]
},
<<<<<<< HEAD
{
path: '/xljk',
component: Layout,
@@ -139,6 +140,12 @@ export const dynamicRoutes: RouteRecordRaw[] = [
]
}
];
=======
];
// 动态路由,基于用户权限动态去加载
export const dynamicRoutes: RouteRecordRaw[] = [];
>>>>>>> e450a8ffe73adab129a8f940bff3316c0361aa7a
/**
* 创建路由
+31 -2
View File
@@ -28,7 +28,7 @@ axios.defaults.headers['clientid'] = import.meta.env.VITE_APP_CLIENT_ID;
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000
timeout: 300000 // 增加到5分钟,适合大数据量导出
});
// 请求拦截器
@@ -205,7 +205,36 @@ export function download(url: string, params: any, fileName: string) {
}
],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: 'blob'
responseType: 'blob',
timeout: 1200000 // 导出接口单独设置20分钟超时
}).then(async (resp: any) => {
const isLogin = blobValidate(resp);
if (isLogin) {
const blob = new Blob([resp]);
FileSaver.saveAs(blob, fileName);
} else {
const blob = new Blob([resp]);
const resText = await blob.text();
const rspObj = JSON.parse(resText);
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'];
ElMessage.error(errMsg);
}
downloadLoadingInstance.close();
}).catch((r: any) => {
console.error(r);
ElMessage.error('下载文件出现错误,请联系管理员!');
downloadLoadingInstance.close();
});
}
// 通用下载方法(JSON格式入参)
export function downloadJson(url: string, params: any, fileName: string) {
downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
// prettier-ignore
return service.post(url, params, {
headers: { 'Content-Type': 'application/json;charset=utf-8' },
responseType: 'blob',
timeout: 1200000 // 导出接口单独设置20分钟超时
}).then(async (resp: any) => {
const isLogin = blobValidate(resp);
if (isLogin) {
@@ -0,0 +1,748 @@
<template>
<div class="performance-indicators">
<el-alert
title="绩效指标设置"
type="info"
show-icon
description="请按需要填写各类绩效指标,未使用的指标可以不填写。分值占比总和必须等于100%。"
:closable="false"
style="margin-bottom: 20px"
/>
<!-- 产出指标 -->
<el-card class="indicator-category" style="margin-bottom: 20px">
<template #header>
<div class="category-header">
<el-icon><TrendCharts /></el-icon>
<span>产出指标</span>
</div>
</template>
<!-- 数量指标 -->
<div class="indicator-item">
<h4 class="indicator-title">数量指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.quantity.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.quantity.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.quantity.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.quantity.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.quantity.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 质量指标 -->
<div class="indicator-item">
<h4 class="indicator-title">质量指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.quality.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.quality.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.quality.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.quality.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.quality.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 时效指标 -->
<div class="indicator-item">
<h4 class="indicator-title">时效指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.timeliness.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.timeliness.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.timeliness.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.timeliness.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.timeliness.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 成本指标 -->
<div class="indicator-item">
<h4 class="indicator-title">成本指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.cost.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.cost.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.cost.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.cost.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.cost.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</el-card>
<!-- 效益指标 -->
<el-card class="indicator-category" style="margin-bottom: 20px">
<template #header>
<div class="category-header">
<el-icon><Money /></el-icon>
<span>效益指标</span>
</div>
</template>
<!-- 经济效益指标 -->
<div class="indicator-item">
<h4 class="indicator-title">经济效益指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.economic.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.economic.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.economic.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.economic.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.economic.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 社会效益指标 -->
<div class="indicator-item">
<h4 class="indicator-title">社会效益指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.social.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.social.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.social.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.social.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.social.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 生态效益指标 -->
<div class="indicator-item">
<h4 class="indicator-title">生态效益指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.ecological.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.ecological.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.ecological.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.ecological.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.ecological.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</el-card>
<!-- 满意度指标 -->
<el-card class="indicator-category" style="margin-bottom: 20px">
<template #header>
<div class="category-header">
<el-icon><Star /></el-icon>
<span>满意度指标</span>
</div>
</template>
<!-- 服务对象满意度指标 -->
<div class="indicator-item">
<h4 class="indicator-title">服务对象满意度指标</h4>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="绩效目标名称">
<el-input
v-model="indicators.service_satisfaction.goalName"
placeholder="请输入绩效目标名称"
maxlength="200"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="目标值">
<el-input
v-model="indicators.service_satisfaction.targetValue"
placeholder="目标值"
maxlength="100"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="单位">
<el-input
v-model="indicators.service_satisfaction.unit"
placeholder="单位"
maxlength="20"
@input="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="分值占比(%)">
<el-input-number
v-model="indicators.service_satisfaction.weightPercentage"
:min="0"
:max="100"
:precision="1"
placeholder="分值占比"
style="width: 100%"
@change="emitChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细说明">
<el-input
v-model="indicators.service_satisfaction.description"
placeholder="详细说明"
maxlength="500"
@input="emitChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</el-card>
<!-- 分值占比统计 -->
<el-alert
:title="`当前分值占比总和:${totalWeight}%`"
:type="totalWeight === 100 ? 'success' : totalWeight === 0 ? 'info' : 'warning'"
show-icon
:description="totalWeight === 100 ? '分值占比设置正确' : totalWeight === 0 ? '请填写绩效指标的分值占比' : '分值占比总和必须等于100%'"
:closable="false"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed, watch } from 'vue'
import { TrendCharts, Money, Star } from '@element-plus/icons-vue'
interface PerformanceIndicator {
indicatorType: string
indicatorTypeName: string
goalName: string
targetValue: string
unit: string
weightPercentage: number
description: string
measurementMethod: string
dataSource: string
isQuantitative: boolean
sortOrder: number
status: string
}
export default defineComponent({
name: 'PerformanceIndicators',
components: {
TrendCharts,
Money,
Star
},
props: {
modelValue: {
type: Array,
default: () => []
},
readonly: {
type: Boolean,
default: false
}
},
emits: ['update:modelValue', 'change'],
setup(props, { emit }) {
// 创建默认指标对象
const createIndicator = (type: string, typeName: string): PerformanceIndicator => ({
indicatorType: type,
indicatorTypeName: typeName,
goalName: '',
targetValue: '',
unit: '',
weightPercentage: 0,
description: '',
measurementMethod: '',
dataSource: '',
isQuantitative: true,
sortOrder: 0,
status: '1'
})
// 8种固定的指标类型
const indicators = ref({
quantity: createIndicator('quantity', '数量指标'),
quality: createIndicator('quality', '质量指标'),
timeliness: createIndicator('timeliness', '时效指标'),
cost: createIndicator('cost', '成本指标'),
economic: createIndicator('economic', '经济效益指标'),
social: createIndicator('social', '社会效益指标'),
ecological: createIndicator('ecological', '生态效益指标'),
service_satisfaction: createIndicator('service_satisfaction', '服务对象满意度指标')
})
// 计算总分值占比
const totalWeight = computed(() => {
let total = 0
Object.values(indicators.value).forEach(indicator => {
total += indicator.weightPercentage || 0
})
return Math.round(total * 10) / 10
})
// 收集有效的指标数据
const collectValidIndicators = () => {
const validIndicators: PerformanceIndicator[] = []
let sortOrder = 0
Object.values(indicators.value).forEach(indicator => {
// 只有填写了目标名称的指标才被认为是有效的
if (indicator.goalName?.trim()) {
validIndicators.push({
...indicator,
sortOrder: sortOrder++
})
}
})
return validIndicators
}
// 发出变化事件
const emitChange = () => {
const validIndicators = collectValidIndicators()
emit('update:modelValue', validIndicators)
emit('change', validIndicators)
}
// 从传入的数据初始化指标
const initializeFromData = (data: any[]) => {
if (!data || data.length === 0) return
// 重置所有指标
indicators.value = {
quantity: createIndicator('quantity', '数量指标'),
quality: createIndicator('quality', '质量指标'),
timeliness: createIndicator('timeliness', '时效指标'),
cost: createIndicator('cost', '成本指标'),
economic: createIndicator('economic', '经济效益指标'),
social: createIndicator('social', '社会效益指标'),
ecological: createIndicator('ecological', '生态效益指标'),
service_satisfaction: createIndicator('service_satisfaction', '服务对象满意度指标')
}
// 将数据填入对应的指标类型
data.forEach(item => {
if (indicators.value[item.indicatorType]) {
indicators.value[item.indicatorType] = { ...item }
}
})
}
// 监听外部数据变化
watch(() => props.modelValue, (newValue) => {
initializeFromData(newValue)
}, { deep: true, immediate: true })
// 校验方法
const validate = () => {
const validIndicators = collectValidIndicators()
const errors = []
if (validIndicators.length === 0) {
errors.push('请至少填写一个绩效指标')
}
validIndicators.forEach((indicator, index) => {
if (!indicator.goalName?.trim()) {
errors.push(`第${index + 1}个指标未填写绩效目标名称`)
}
if (!indicator.targetValue?.trim()) {
errors.push(`第${index + 1}个指标未填写目标值`)
}
if (!indicator.weightPercentage || indicator.weightPercentage <= 0) {
errors.push(`第${index + 1}个指标未设置分值占比`)
}
})
if (Math.abs(totalWeight.value - 100) > 0.1) {
errors.push(`分值占比总和必须等于100%,当前为${totalWeight.value}%`)
}
if (errors.length > 0) {
return { valid: false, message: errors.join(';') }
}
return { valid: true }
}
// 获取所有有效指标数据
const getAllIndicators = () => {
return collectValidIndicators()
}
return {
indicators,
totalWeight,
emitChange,
validate,
getAllIndicators
}
}
})
</script>
<style scoped lang="scss">
.performance-indicators {
.category-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: bold;
font-size: 16px;
.el-icon {
font-size: 18px;
}
}
.indicator-item {
margin-bottom: 20px;
padding: 15px;
background-color: #fafafa;
border-radius: 6px;
border-left: 4px solid #409eff;
&:last-child {
margin-bottom: 0;
}
}
.indicator-title {
margin: 0 0 15px 0;
font-size: 14px;
font-weight: bold;
color: #606266;
}
.el-form-item {
margin-bottom: 10px;
}
}
</style>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+280
View File
@@ -0,0 +1,280 @@
<template>
<div class="app-container">
<el-card>
<template #header>
<div class="card-header">
<span>项目预算申报详情</span>
<el-tag :type="getStatusType(detail.approvalStatus)" class="ml10">
{{ getStatusText(detail.approvalStatus) }}
</el-tag>
</div>
</template>
<!-- 申报摘要 -->
<application-summary :application="detail" />
<!-- 审批流程 -->
<el-divider content-position="left">审批流程</el-divider>
<el-timeline>
<el-timeline-item
v-for="(item, index) in approvalTimeline"
:key="index"
:type="item.type"
:timestamp="item.timestamp"
>
<div class="timeline-content">
<h4>{{ item.title }}</h4>
<p>{{ item.description }}</p>
<span class="timeline-user" v-if="item.user">{{ item.user }}</span>
</div>
</el-timeline-item>
</el-timeline>
<!-- 操作日志 -->
<el-divider content-position="left">操作日志</el-divider>
<el-table :data="operationLogs" style="width: 100%">
<el-table-column prop="operation" label="操作" width="120"/>
<el-table-column prop="operator" label="操作人" width="100"/>
<el-table-column prop="operateTime" label="操作时间" width="180">
<template #default="scope">
{{ parseTime(scope.row.operateTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip/>
</el-table>
<!-- 操作按钮 -->
<div class="operation-buttons">
<el-button
v-if="detail.approvalStatus === 'draft'"
type="primary"
@click="handleEdit"
v-hasPermi="['cw:application:edit']"
>编辑</el-button>
<el-button
v-if="detail.approvalStatus === 'draft'"
type="success"
@click="handleSubmit"
v-hasPermi="['cw:application:submit']"
>提交</el-button>
<el-button
v-if="detail.approvalStatus === 'pending'"
type="warning"
@click="handleWithdraw"
v-hasPermi="['cw:application:withdraw']"
>撤回</el-button>
<el-button @click="goBack">返回</el-button>
</div>
</el-card>
</div>
</template>
<script setup name="CwApplicationDetail">
import { ref, getCurrentInstance, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { getProjectApplication, submitApplication, withdrawApplication } from "@/api/cw/projectApplication";
import ApplicationSummary from "./summary.vue";
import { parseTime } from "@/utils/ruoyi";
console.log('=== detail.vue 组件开始加载 ===');
const { proxy } = getCurrentInstance();
const route = useRoute();
const router = useRouter();
const detail = ref({});
const approvalTimeline = ref([]);
const operationLogs = ref([]);
console.log('=== detail.vue 组件初始化完成 ===');
console.log('route对象:', route);
console.log('初始查询参数:', route.query);
/** 获取状态类型 */
function getStatusType(status) {
const statusMap = {
'draft': 'info',
'pending': 'warning',
'approving': 'primary',
'approved': 'success',
'rejected': 'danger',
'withdrawn': 'info',
'cancelled': 'info'
};
return statusMap[status] || 'info';
}
/** 获取状态文本 */
function getStatusText(status) {
const statusMap = {
'draft': '草稿',
'pending': '待审批',
'approving': '审批中',
'approved': '已通过',
'rejected': '已拒绝',
'withdrawn': '已撤回',
'cancelled': '已取消'
};
return statusMap[status] || '未知';
}
/** 编辑按钮操作 */
function handleEdit() {
router.push("/cw/application/form?id=" + detail.value.id);
}
/** 提交按钮操作 */
function handleSubmit() {
proxy.$modal.confirm('是否确认提交申报编号为"' + detail.value.applicationNo + '"的数据项?').then(function() {
return submitApplication(detail.value.id);
}).then(() => {
getDetail();
proxy.$modal.msgSuccess("提交成功");
}).catch(() => {});
}
/** 撤回按钮操作 */
function handleWithdraw() {
proxy.$modal.confirm('是否确认撤回申报编号为"' + detail.value.applicationNo + '"的数据项?').then(function() {
return withdrawApplication(detail.value.id);
}).then(() => {
getDetail();
proxy.$modal.msgSuccess("撤回成功");
}).catch(() => {});
}
/** 返回按钮操作 */
function goBack() {
router.push("/cw/application");
}
/** 获取详情数据 */
function getDetail() {
const id = route.query.id;
console.log('=== 开始获取详情数据 ===');
console.log('URL参数中的ID:', id);
console.log('完整query参数:', route.query);
if (id) {
console.log('开始调用API:getProjectApplication, ID:', id);
getProjectApplication(id).then(response => {
console.log('=== API调用成功 ===');
console.log('API响应:', response);
console.log('响应代码:', response.code);
console.log('响应数据:', response.data);
detail.value = response.data;
// 调试日志:查看返回的数据
console.log('=== 项目申报详情数据 ===');
console.log('完整数据:', response.data);
console.log('绩效指标数据:', response.data.performanceIndicators);
console.log('绩效指标数量:', response.data.performanceIndicators ? response.data.performanceIndicators.length : 0);
// 模拟审批流程数据
approvalTimeline.value = [
{
type: 'success',
timestamp: detail.value.createTime,
title: '申报提交',
description: '申报人提交预算申报',
user: detail.value.applicantName
}
];
if (detail.value.submitTime) {
approvalTimeline.value.push({
type: 'primary',
timestamp: detail.value.submitTime,
title: '提交审批',
description: '申报已提交,等待部门审批',
user: detail.value.applicantName
});
}
if (detail.value.approveTime && detail.value.approvalStatus === 'approved') {
approvalTimeline.value.push({
type: 'success',
timestamp: detail.value.approveTime,
title: '审批通过',
description: '所有审批节点已通过',
user: '系统管理员'
});
}
// 模拟操作日志数据
operationLogs.value = [
{
operation: '创建申报',
operator: detail.value.applicantName,
operateTime: detail.value.createTime,
remark: '创建项目预算申报'
}
];
if (detail.value.submitTime) {
operationLogs.value.push({
operation: '提交审批',
operator: detail.value.applicantName,
operateTime: detail.value.submitTime,
remark: '提交预算申报进入审批流程'
});
}
}).catch(error => {
console.error('=== API调用失败 ===');
console.error('错误信息:', error);
console.error('错误响应:', error.response);
proxy.$modal.msgError('获取申报详情失败:' + (error.message || '未知错误'));
});
} else {
console.error('=== URL参数错误 ===');
console.error('缺少ID参数,无法获取详情数据');
proxy.$modal.msgError('缺少申报ID参数');
}
}
onMounted(() => {
console.log('=== detail.vue 页面已挂载 ===');
console.log('当前路由:', route);
console.log('当前路由路径:', route.path);
console.log('当前路由查询参数:', route.query);
getDetail();
});
</script>
<style scoped>
.card-header {
display: flex;
align-items: center;
}
.ml10 {
margin-left: 10px;
}
.timeline-content h4 {
margin: 0 0 8px 0;
font-size: 16px;
color: #303133;
}
.timeline-content p {
margin: 0 0 8px 0;
color: #606266;
font-size: 14px;
}
.timeline-user {
color: #909399;
font-size: 12px;
}
.operation-buttons {
margin-top: 30px;
text-align: center;
}
.operation-buttons .el-button {
margin: 0 10px;
}
</style>
File diff suppressed because it is too large. Load diff
+784
View File
@@ -0,0 +1,784 @@
<template>
<div class="page-container">
<!-- 统计卡片区域 -->
<el-row :gutter="20" class="stats-row">
<el-col :span="6">
<div class="stat-card stat-card-primary">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Document /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ totalCount }}</div>
<div class="stat-label">总申报数</div>
</div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card stat-card-success">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Check /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ approvedCount }}</div>
<div class="stat-label">已通过</div>
</div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card stat-card-warning">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ pendingCount }}</div>
<div class="stat-label">审批中</div>
</div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card stat-card-info">
<div class="stat-content">
<div class="stat-icon">
<el-icon><EditPen /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ draftCount }}</div>
<div class="stat-label">草稿</div>
</div>
</div>
</div>
</el-col>
</el-row>
<!-- 搜索区域 -->
<el-card class="button-card" v-show="showSearch">
<template #header>
<div class="card-header">
<span class="toolbar-title">项目申报查询</span>
</div>
</template>
<el-form :model="queryParams" ref="queryRef" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="申报编号" prop="applicationNo">
<el-input
v-model="queryParams.applicationNo"
placeholder="请输入申报编号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="项目名称" prop="projectName">
<el-input
v-model="queryParams.projectName"
placeholder="请输入项目名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="项目负责人" prop="principal">
<el-input
v-model="queryParams.principal"
placeholder="请输入项目负责人"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="审批状态" prop="approvalStatus">
<el-select v-model="queryParams.approvalStatus" placeholder="请选择审批状态" clearable style="width: 100%">
<el-option label="草稿" value="draft"/>
<el-option label="已提交" value="submitted"/>
<el-option label="审批中" value="approving"/>
<el-option label="已通过" value="approved"/>
<el-option label="已拒绝" value="rejected"/>
<el-option label="已退回" value="returned"/>
<el-option label="已取消" value="cancelled"/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="预算年度" prop="year">
<el-date-picker
v-model="queryParams.year"
type="year"
placeholder="选择年份"
value-format="YYYY"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="18">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 操作按钮区域 -->
<el-card class="button-card">
<template #header>
<div class="card-header">
<span class="toolbar-title">项目申报管理</span>
<div class="toolbar-actions">
<el-button
type="primary"
icon="Plus"
@click="handleAdd"
v-hasPermi="['cw:application:add']"
>新增申报</el-button>
<el-button
type="success"
icon="Edit"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['cw:application:edit']"
>修改</el-button>
<el-button
type="danger"
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['cw:application:remove']"
>删除</el-button>
<el-button
type="info"
icon="Upload"
@click="handleSubmit"
:disabled="multiple"
v-hasPermi="['cw:application:submit']"
>提交审批</el-button>
<el-button
type="warning"
icon="Download"
@click="handleExport"
v-hasPermi="['cw:application:export']"
>导出</el-button>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</div>
</div>
</template>
</el-card>
<!-- 数据表格区域 -->
<el-card class="table-card">
<div class="table-container">
<el-table
v-loading="loading"
:data="applicationList"
@selection-change="handleSelectionChange"
class="scroll-table"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="申报编号" align="center" prop="applicationNo" min-width="140" />
<el-table-column label="项目名称" align="center" prop="projectName" min-width="180" show-overflow-tooltip />
<el-table-column label="项目负责人" align="center" prop="principal" min-width="100" />
<el-table-column label="所属部门" align="center" prop="departmentName" min-width="120" show-overflow-tooltip />
<el-table-column label="预算年度" align="center" prop="year" width="100" />
<el-table-column label="项目总投资" align="center" prop="totalInvestment" min-width="120">
<template #default="scope">
<span class="amount-text">{{ formatAmount(scope.row.totalInvestment) }}</span>
</template>
</el-table-column>
<el-table-column label="审批状态" align="center" prop="approvalStatus" width="100">
<template #default="scope">
<el-tag
:type="getStatusTagType(scope.row.approvalStatus)"
effect="light"
>
{{ getStatusText(scope.row.approvalStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" min-width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="280" class-name="small-padding fixed-width">
<template #default="scope">
<div class="action-buttons">
<el-tooltip content="查看详情" placement="top">
<el-button
type="primary"
icon="View"
circle
size="small"
@click="handleView(scope.row)"
v-hasPermi="['cw:application:query']"
/>
</el-tooltip>
<el-tooltip content="编辑" placement="top" v-if="scope.row.approvalStatus === 'draft'">
<el-button
type="success"
icon="Edit"
circle
size="small"
@click="handleUpdate(scope.row)"
v-hasPermi="['cw:application:edit']"
/>
</el-tooltip>
<el-tooltip content="提交审批" placement="top" v-if="scope.row.approvalStatus === 'draft'">
<el-button
type="warning"
icon="Upload"
circle
size="small"
@click="handleSubmit(scope.row)"
v-hasPermi="['cw:application:submit']"
/>
</el-tooltip>
<el-tooltip content="撤回" placement="top" v-if="scope.row.approvalStatus === 'submitted'">
<el-button
type="info"
icon="RefreshLeft"
circle
size="small"
@click="handleWithdraw(scope.row)"
v-hasPermi="['cw:application:withdraw']"
/>
</el-tooltip>
<el-tooltip content="删除" placement="top" v-if="['draft', 'cancelled'].includes(scope.row.approvalStatus)">
<el-button
type="danger"
icon="Delete"
circle
size="small"
@click="handleDelete(scope.row)"
v-hasPermi="['cw:application:remove']"
/>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</el-card>
<!-- 添加或修改项目申报对话框 -->
<el-dialog :title="title" v-model="open" width="1200px" append-to-body>
<ApplicationForm ref="applicationFormRef" :form-data="form" @submit="submitForm" />
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取 消</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
</div>
</template>
</el-dialog>
<!-- 查看详情对话框 -->
<el-dialog title="项目申报详情" v-model="viewOpen" width="1200px" append-to-body>
<ApplicationSummary :application-id="currentId" />
<template #footer>
<div class="dialog-footer">
<el-button @click="viewOpen = false">关 闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import { listProjectApplication, getProjectApplication, delProjectApplication, addProjectApplication, updateProjectApplication, submitApplication, getApplicationStatistics } from "@/api/cw/projectApplication"
import ApplicationForm from "./form.vue"
import ApplicationSummary from "./summary.vue"
export default {
name: "ProjectApplication",
components: {
ApplicationForm,
ApplicationSummary
},
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 项目申报表格数据
applicationList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 是否显示查看对话框
viewOpen: false,
// 当前查看的ID
currentId: null,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
applicationNo: null,
projectName: null,
principal: null,
approvalStatus: null,
year: null
},
// 表单参数
form: {},
// 表单校验
rules: {},
// 统计数据
totalCount: 0,
approvedCount: 0,
pendingCount: 0,
draftCount: 0
}
},
created() {
this.getList()
this.getStatistics()
},
methods: {
/** 查询项目申报列表 */
getList() {
this.loading = true
listProjectApplication(this.queryParams).then(response => {
this.applicationList = response.rows
this.total = response.total
this.loading = false
})
},
/** 获取统计数据 */
getStatistics() {
// 调用专门的统计API接口
const year = this.queryParams.year ? parseInt(this.queryParams.year) : null
getApplicationStatistics(year).then(response => {
const data = response.data
this.totalCount = data.totalCount || 0
this.approvedCount = data.approvedCount || 0
this.pendingCount = data.approvingCount || 0 // 注意:后端返回的是approvingCount
this.draftCount = data.draftCount || 0
}).catch(error => {
console.error('获取统计数据失败:', error)
// 如果API失败,使用本地计算作为后备方案
this.totalCount = this.applicationList.length
this.approvedCount = this.applicationList.filter(item => item.approvalStatus === 'approved').length
this.pendingCount = this.applicationList.filter(item => item.approvalStatus === 'approving').length
this.draftCount = this.applicationList.filter(item => item.approvalStatus === 'draft').length
})
},
/** 取消按钮 */
cancel() {
this.open = false
this.reset()
},
/** 表单重置 */
reset() {
this.form = {
id: null,
applicationNo: null,
projectName: null,
principal: null,
year: new Date().getFullYear(),
departmentId: null,
departmentName: null,
projectCategory: null,
implementationPeriod: null,
projectBackground: null,
projectObjective: null,
projectScope: null,
projectContent: null,
implementationPlan: null,
expectedResults: null,
riskAssessment: null,
totalPerformanceGoal: null,
currentYearGoal: null,
totalInvestment: 0,
totalMunicipalFiscal: 0,
totalSelfFunding: 0,
totalOtherFunding: 0,
currentYearTotal: 0,
currentYearMunicipal: 0,
currentYearSelf: 0,
currentYearOther: 0,
nextYearTotal: 0,
nextYearMunicipal: 0,
nextYearSelf: 0,
nextYearOther: 0,
futureYearTotal: 0,
futureYearMunicipal: 0,
futureYearSelf: 0,
futureYearOther: 0,
performanceIndicators: [],
approvalStatus: 'draft'
}
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
this.getStatistics() // 搜索时也更新统计数据
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryRef")
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = "添加项目申报"
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const id = row.id || this.ids
getProjectApplication(id).then(response => {
this.form = response.data
this.open = true
this.title = "修改项目申报"
})
},
/** 查看详情 */
handleView(row) {
this.currentId = row.id
this.viewOpen = true
},
/** 提交按钮 */
submitForm() {
this.$refs["applicationFormRef"].validate((valid) => {
if (valid) {
if (this.form.id != null) {
updateProjectApplication(this.form).then(response => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
this.getStatistics() // 修改后更新统计
})
} else {
addProjectApplication(this.form).then(response => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()
this.getStatistics() // 新增后更新统计
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids
this.$modal.confirm('是否确认删除项目申报编号为"' + ids + '"的数据项?').then(function() {
return delProjectApplication(ids)
}).then(() => {
this.getList()
this.getStatistics() // 删除后更新统计
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
},
/** 提交审批 */
handleSubmit(row) {
const applications = row?.applicationNo ? [row.applicationNo] : this.applicationList.filter(item => this.ids.includes(item.id)).map(item => item.applicationNo)
this.$modal.confirm('是否确认提交申报编号为"' + applications.join('", "') + '"的数据项?').then(() => {
return submitApplication(row?.id || this.ids)
}).then(() => {
this.getList()
this.getStatistics() // 提交后更新统计
this.$modal.msgSuccess("提交成功")
}).catch(() => {})
},
/** 撤回申请 */
handleWithdraw(row) {
this.$modal.confirm('是否确认撤回该申报?').then(() => {
// 调用撤回接口
this.$modal.msgSuccess("撤回成功")
this.getList()
this.getStatistics() // 撤回后更新统计
}).catch(() => {})
},
/** 导出按钮操作 */
handleExport() {
this.download('cw/application/export', {
...this.queryParams
}, `application_${new Date().getTime()}.xlsx`)
},
/** 格式化金额 */
formatAmount(amount) {
if (!amount) return '0.00'
return parseFloat(amount).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
},
/** 获取状态标签类型 */
getStatusTagType(status) {
const statusMap = {
draft: 'info',
submitted: 'warning',
approving: 'warning',
approved: 'success',
rejected: 'danger',
returned: 'warning',
cancelled: 'info'
}
return statusMap[status] || 'info'
},
/** 获取状态文本 */
getStatusText(status) {
const statusMap = {
draft: '草稿',
submitted: '已提交',
approving: '审批中',
approved: '已通过',
rejected: '已拒绝',
returned: '已退回',
cancelled: '已取消'
}
return statusMap[status] || '未知'
}
}
}
</script>
<style scoped lang="scss">
/* 继承后勤管理模块的样式风格 */
.page-container {
padding: 20px;
/* 统计卡片样式 */
.stats-row {
margin-bottom: 20px;
}
.stat-card {
border-radius: 12px;
transition: all 0.3s ease;
height: 90px;
cursor: pointer;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.stat-content {
display: flex;
align-items: center;
padding: 5px 16px;
height: 100%;
box-sizing: border-box;
.stat-icon {
font-size: 28px;
margin-right: 15px;
opacity: 0.9;
}
.stat-info {
flex: 1;
.stat-value {
font-size: 24px;
font-weight: bold;
line-height: 1;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
opacity: 0.8;
line-height: 1;
}
}
}
&.stat-card-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
&.stat-card-success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
&.stat-card-warning {
background: linear-gradient(135deg, #ff9a56 0%, #ffad56 100%);
color: white;
}
&.stat-card-info {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
color: #333;
}
}
/* 卡片样式 */
.button-card, .table-card {
margin-bottom: 20px;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
:deep(.el-card__header) {
border-bottom: 1px solid rgba(93, 135, 255, 0.1);
background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
padding: 16px 20px;
}
:deep(.el-card__body) {
padding: 20px;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.toolbar-title {
font-size: 18px;
font-weight: 600;
color: #1e293b;
display: flex;
align-items: center;
&::before {
content: '';
width: 4px;
height: 20px;
background: linear-gradient(135deg, rgb(93, 135, 255), rgb(73, 190, 255));
border-radius: 2px;
margin-right: 12px;
}
}
.toolbar-actions {
display: flex;
gap: 12px;
align-items: center;
}
}
/* 表格样式 */
.table-container {
width: 100%;
overflow-x: auto;
}
.scroll-table {
width: 100%;
:deep(.el-table__header-wrapper) {
.el-table__header {
th {
background-color: #f8f9fa;
color: #333;
font-weight: 600;
}
}
}
:deep(.el-table__body-wrapper) {
.el-table__row {
&:hover {
background-color: #f5f7fa;
}
}
}
}
/* 操作按钮样式 */
.action-buttons {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
white-space: nowrap;
.el-button {
width: 32px !important;
height: 32px !important;
padding: 0 !important;
display: flex;
align-items: center;
justify-content: center;
}
}
/* 金额文本样式 */
.amount-text {
font-weight: 600;
color: #e74c3c;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 0 0 0;
margin-top: 16px;
}
/* 对话框样式 */
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.page-container {
padding: 10px;
.stats-row {
.stat-card {
margin-bottom: 10px;
}
}
.toolbar-actions {
flex-direction: column;
gap: 8px !important;
}
}
}
</style>
+505
View File
@@ -0,0 +1,505 @@
<template>
<div class="application-summary" v-loading="loading">
<!-- 基本信息 -->
<el-card class="summary-card">
<template #header>
<div class="card-header">
<span>项目基本信息</span>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="申报编号">
{{ currentApplication.applicationNo || '系统自动生成' }}
</el-descriptions-item>
<el-descriptions-item label="项目名称">
{{ currentApplication.projectName }}
</el-descriptions-item>
<el-descriptions-item label="项目负责人">
{{ currentApplication.principal }}
</el-descriptions-item>
<el-descriptions-item label="联系电话">
{{ currentApplication.contactPhone }}
</el-descriptions-item>
<el-descriptions-item label="预算年度">
{{ currentApplication.year }}
</el-descriptions-item>
<el-descriptions-item label="项目周期">
{{ formatDateRange(currentApplication.startDate, currentApplication.endDate) }}
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 项目特征 -->
<el-card class="summary-card">
<template #header>
<div class="card-header">
<span>项目特征</span>
</div>
</template>
<el-row :gutter="20">
<el-col :span="8">
<el-tag :type="currentApplication.targetSme ? 'success' : 'info'">
小微企业发展: {{ currentApplication.targetSme ? '是' : '否' }}
</el-tag>
</el-col>
<el-col :span="8">
<el-tag :type="currentApplication.targetMicroSme ? 'success' : 'info'">
微小企业成长: {{ currentApplication.targetMicroSme ? '是' : '否' }}
</el-tag>
</el-col>
<el-col :span="8">
<el-tag :type="currentApplication.importEquipment ? 'success' : 'info'">
进口设备: {{ currentApplication.importEquipment ? '是' : '否' }}
</el-tag>
</el-col>
</el-row>
</el-card>
<!-- 项目内容 -->
<el-card class="summary-card" v-if="hasProjectContent">
<template #header>
<div class="card-header">
<span>项目内容</span>
</div>
</template>
<el-descriptions :column="1" border>
<el-descriptions-item label="申报理由" v-if="currentApplication.applicationReason">
<div class="content-text">{{ currentApplication.applicationReason }}</div>
</el-descriptions-item>
<el-descriptions-item label="建设内容" v-if="currentApplication.constructionContent">
<div class="content-text">{{ currentApplication.constructionContent }}</div>
</el-descriptions-item>
<el-descriptions-item label="总体绩效目标" v-if="currentApplication.totalPerformanceGoal">
<div class="content-text">{{ currentApplication.totalPerformanceGoal }}</div>
</el-descriptions-item>
<el-descriptions-item label="当年完成目标" v-if="currentApplication.currentYearGoal">
<div class="content-text">{{ currentApplication.currentYearGoal }}</div>
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 资金预算汇总 -->
<el-card class="summary-card">
<template #header>
<div class="card-header">
<span>资金预算汇总</span>
<el-tag type="warning" size="small">单位:万元</el-tag>
</div>
</template>
<!-- 总体预算 -->
<div class="budget-section">
<h4>总体预算</h4>
<el-row :gutter="20" class="budget-row">
<el-col :span="6">
<div class="budget-item">
<div class="budget-label">总投资</div>
<div class="budget-value highlight">{{ formatAmount(currentApplication.totalInvestment) }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="budget-item">
<div class="budget-label">市财政资金</div>
<div class="budget-value">{{ formatAmount(currentApplication.totalMunicipalFiscal) }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="budget-item">
<div class="budget-label">单位自筹</div>
<div class="budget-value">{{ formatAmount(currentApplication.totalSelfFunding) }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="budget-item">
<div class="budget-label">其他资金</div>
<div class="budget-value">{{ formatAmount(currentApplication.totalOtherFunding) }}</div>
</div>
</el-col>
</el-row>
</div>
<!-- 分年度预算 -->
<div class="budget-section">
<h4>分年度预算明细</h4>
<el-table :data="budgetBreakdown" border>
<el-table-column prop="year" label="年度" width="120" align="center"/>
<el-table-column prop="total" label="总投资" align="right">
<template #default="scope">
{{ formatAmount(scope.row.total) }}
</template>
</el-table-column>
<el-table-column prop="municipal" label="市财政资金" align="right">
<template #default="scope">
{{ formatAmount(scope.row.municipal) }}
</template>
</el-table-column>
<el-table-column prop="self" label="单位自筹" align="right">
<template #default="scope">
{{ formatAmount(scope.row.self) }}
</template>
</el-table-column>
<el-table-column prop="other" label="其他资金" align="right">
<template #default="scope">
{{ formatAmount(scope.row.other) }}
</template>
</el-table-column>
</el-table>
</div>
</el-card>
<!-- 绩效指标设置 -->
<el-card class="summary-card" v-if="hasPerformanceIndicators">
<template #header>
<div class="card-header">
<span>绩效指标设置</span>
<el-tag type="warning" size="small">分值占比总和:{{ performanceIndicatorTotal }}%</el-tag>
</div>
</template>
<el-table :data="validPerformanceIndicators" border style="width: 100%">
<el-table-column label="指标类型" width="120" align="center">
<template #default="scope">
<el-tag>{{ scope.row.indicatorTypeName }}</el-tag>
</template>
</el-table-column>
<el-table-column label="绩效目标名称" min-width="200">
<template #default="scope">
<div class="content-text">{{ scope.row.goalName }}</div>
</template>
</el-table-column>
<el-table-column label="目标值" width="120" align="center">
<template #default="scope">
<div class="content-text">{{ scope.row.targetValue }}</div>
</template>
</el-table-column>
<el-table-column label="分值占比" width="100" align="center">
<template #default="scope">
<el-tag type="primary">{{ parseFloat(scope.row.weightPercentage) || 0 }}%</el-tag>
</template>
</el-table-column>
<el-table-column label="详细说明" min-width="150">
<template #default="scope">
<div class="content-text" v-if="scope.row.description">{{ scope.row.description }}</div>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 备注信息 -->
<el-card class="summary-card" v-if="currentApplication.remark">
<template #header>
<div class="card-header">
<span>备注信息</span>
</div>
</template>
<div class="content-text">{{ currentApplication.remark }}</div>
</el-card>
</div>
</template>
<script setup>
import { computed, ref, watch, onMounted } from 'vue';
import { getProjectApplication } from '@/api/cw/projectApplication';
defineOptions({
name: 'ApplicationSummary'
});
const props = defineProps({
application: {
type: Object,
default: () => ({})
},
applicationId: {
type: [String, Number],
default: null
}
});
// 内部数据状态
const internalApplication = ref({});
const loading = ref(false);
// 计算属性:优先使用传入的application,如果没有则使用内部获取的数据
const currentApplication = computed(() => {
// 如果直接传入了application对象且不为空,则使用它
if (props.application && Object.keys(props.application).length > 0) {
console.log('=== 使用传入的application对象 ===');
console.log('application数据:', props.application);
return props.application;
}
// 否则使用内部获取的数据
console.log('=== 使用内部获取的application数据 ===');
console.log('internalApplication数据:', internalApplication.value);
return internalApplication.value;
});
// 获取申报详情数据
const fetchApplicationData = async (id) => {
if (!id) return;
console.log('=== ApplicationSummary 开始获取数据 ===');
console.log('applicationId:', id);
loading.value = true;
try {
const response = await getProjectApplication(id);
console.log('=== ApplicationSummary API调用成功 ===');
console.log('API响应:', response);
console.log('响应数据:', response.data);
console.log('响应数据类型:', typeof response.data);
// 特别检查绩效指标字段
if (response.data) {
console.log('=== API响应中的绩效指标检查 ===');
console.log('response.data.performanceIndicators:', response.data.performanceIndicators);
console.log('performanceIndicators类型:', typeof response.data.performanceIndicators);
console.log('是否为数组:', Array.isArray(response.data.performanceIndicators));
// 检查所有可能的绩效指标字段名
console.log('可能的字段名检查:');
console.log('- performanceIndicators:', response.data.performanceIndicators);
console.log('- performance_indicators:', response.data.performance_indicators);
console.log('- indicators:', response.data.indicators);
console.log('- performanceIndicatorList:', response.data.performanceIndicatorList);
// 列出所有字段
console.log('响应数据的所有字段:', Object.keys(response.data));
}
internalApplication.value = response.data || {};
} catch (error) {
console.error('=== ApplicationSummary API调用失败 ===');
console.error('错误信息:', error);
internalApplication.value = {};
} finally {
loading.value = false;
}
};
// 监听applicationId变化
watch(() => props.applicationId, (newId) => {
if (newId && (!props.application || Object.keys(props.application).length === 0)) {
fetchApplicationData(newId);
}
}, { immediate: true });
// 组件挂载时检查是否需要获取数据
onMounted(() => {
console.log('=== ApplicationSummary 组件挂载 ===');
console.log('props.application:', props.application);
console.log('props.applicationId:', props.applicationId);
// 如果没有传入application但有applicationId,则获取数据
if (props.applicationId && (!props.application || Object.keys(props.application).length === 0)) {
fetchApplicationData(props.applicationId);
}
});
// 判断是否有项目内容
const hasProjectContent = computed(() => {
const app = currentApplication.value;
return app.applicationReason ||
app.constructionContent ||
app.totalPerformanceGoal ||
app.currentYearGoal;
});
// 判断是否有绩效指标
const hasPerformanceIndicators = computed(() => {
const app = currentApplication.value;
console.log('=== 绩效指标详细检查 ===');
console.log('完整app对象:', app);
console.log('app.performanceIndicators类型:', typeof app.performanceIndicators);
console.log('app.performanceIndicators值:', app.performanceIndicators);
console.log('是否为数组:', Array.isArray(app.performanceIndicators));
if (app.performanceIndicators) {
console.log('绩效指标数组长度:', app.performanceIndicators.length);
console.log('绩效指标数组内容:', JSON.stringify(app.performanceIndicators, null, 2));
} else {
console.log('绩效指标数据为空或未定义');
}
const hasIndicators = app.performanceIndicators && app.performanceIndicators.length > 0;
console.log('最终判断 - 是否有绩效指标:', hasIndicators);
return hasIndicators;
});
// 计算绩效指标总分值占比
const performanceIndicatorTotal = computed(() => {
const app = currentApplication.value;
if (!app.performanceIndicators || app.performanceIndicators.length === 0) {
return 0;
}
const total = app.performanceIndicators.reduce((sum, indicator) => {
const weight = parseFloat(indicator.weightPercentage) || 0;
return sum + weight;
}, 0);
return Math.round(total * 100) / 100; // 保留两位小数
});
// 过滤掉无效的绩效指标(分值占比为0的)
const validPerformanceIndicators = computed(() => {
const app = currentApplication.value;
console.log('=== validPerformanceIndicators 计算 ===');
console.log('app.performanceIndicators:', app.performanceIndicators);
if (!app.performanceIndicators) {
console.log('没有绩效指标数据,返回空数组');
return [];
}
const validIndicators = app.performanceIndicators.filter(indicator => {
const weight = parseFloat(indicator.weightPercentage) || 0;
console.log(`指标 ${indicator.goalName}: 权重=${indicator.weightPercentage}, 解析后权重=${weight}, 是否有效=${weight > 0}`);
return weight > 0;
});
console.log('有效绩效指标数量:', validIndicators.length);
console.log('有效绩效指标详情:', validIndicators);
return validIndicators;
});
// 分年度预算明细
const budgetBreakdown = computed(() => {
const app = currentApplication.value;
const currentYear = app.year || new Date().getFullYear();
console.log('=== 预算数据检查 ===');
console.log('application数据:', app);
console.log('当年预算字段:', {
currentYearTotal: app.currentYearTotal,
currentYearMunicipal: app.currentYearMunicipal,
currentYearSelf: app.currentYearSelf,
currentYearOther: app.currentYearOther
});
return [
{
year: `${currentYear}年`,
total: app.currentYearTotal || 0,
municipal: app.currentYearMunicipal || 0,
self: app.currentYearSelf || 0,
other: app.currentYearOther || 0
},
{
year: `${parseInt(currentYear) + 1}年`,
total: app.nextYearTotal || 0,
municipal: app.nextYearMunicipal || 0,
self: app.nextYearSelf || 0,
other: app.nextYearOther || 0
},
{
year: '以后年度',
total: app.futureYearTotal || 0,
municipal: app.futureYearMunicipal || 0,
self: app.futureYearSelf || 0,
other: app.futureYearOther || 0
}
].filter(item => item.total > 0); // 只显示有预算的年度
});
// 格式化金额
const formatAmount = (amount) => {
if (!amount || amount === 0) return '0.00';
return Number(amount).toFixed(2);
};
// 格式化日期范围
const formatDateRange = (startDate, endDate) => {
if (!startDate && !endDate) return '未设置';
if (!startDate) return `截止 ${endDate}`;
if (!endDate) return `${startDate} 开始`;
return `${startDate} 至 ${endDate}`;
};
</script>
<style scoped>
.application-summary {
padding: 0;
}
.summary-card {
margin-bottom: 20px;
}
.summary-card:last-child {
margin-bottom: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 500;
}
.content-text {
line-height: 1.6;
word-break: break-word;
white-space: pre-wrap;
}
.budget-section {
margin-bottom: 24px;
}
.budget-section:last-child {
margin-bottom: 0;
}
.budget-section h4 {
margin: 0 0 16px 0;
font-size: 16px;
color: #333;
border-left: 4px solid #409eff;
padding-left: 12px;
}
.budget-row {
margin-bottom: 16px;
}
.budget-item {
text-align: center;
padding: 16px;
border: 1px solid #ebeef5;
border-radius: 4px;
background-color: #fafafa;
}
.budget-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.budget-value {
font-size: 18px;
font-weight: 600;
color: #333;
}
.budget-value.highlight {
color: #409eff;
font-size: 20px;
}
.el-tag {
margin-right: 8px;
}
:deep(.el-descriptions__label) {
width: 120px;
font-weight: 500;
}
:deep(.el-table td, .el-table th) {
text-align: center;
}
:deep(.el-table .cell) {
padding: 0 8px;
}
</style>
+869
View File
@@ -0,0 +1,869 @@
<template>
<div class="p-2 page-container">
<!-- 搜索区域 -->
<transition :enter-active-class="animate.searchAnimate.enter" :leave-active-class="animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="申报编号" prop="applicationNo">
<el-input v-model="queryParams.applicationNo" placeholder="请输入申报编号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="申报人" prop="applicantName">
<el-input v-model="queryParams.applicantName" placeholder="请输入申报人姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="审批状态" prop="approvalStatus">
<el-select v-model="queryParams.approvalStatus" placeholder="请选择审批状态" clearable style="width: 100%">
<template v-if="cw_approval_status && cw_approval_status.length > 0">
<el-option v-for="dict in cw_approval_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</template>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="审批人" prop="approverName">
<el-input v-model="queryParams.approverName" placeholder="请输入审批人姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="提交时间">
<el-date-picker
v-model="daterangeSubmitTime"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleQuery"
style="width: 100%"
></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 快捷操作按钮 -->
<el-row :gutter="10" class="mb-[10px]">
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showPendingApprovals">
<el-icon class="quick-icon"><Clock /></el-icon>
<span>待我审批</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showMyApprovals">
<el-icon class="quick-icon"><User /></el-icon>
<span>我已审批</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showDepartmentApprovals">
<el-icon class="quick-icon"><OfficeBuilding /></el-icon>
<span>部门审批</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="quick-action-card">
<div class="quick-action" @click="showApprovalStatistics">
<el-icon class="quick-icon"><DataAnalysis /></el-icon>
<span>审批统计</span>
</div>
</el-card>
</el-col>
</el-row>
<!-- 表格区域 -->
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Check"
:disabled="multiple"
@click="handleBatchApprove"
v-hasPermi="['cw:application:approve']"
>批量通过</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Close"
:disabled="multiple"
@click="handleBatchReject"
v-hasPermi="['cw:application:approve']"
>批量拒绝</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['cw:approvalRecord:export']">导出</el-button>
</el-col>
<right-toolbar :showSearch="showSearch" @update:showSearch="showSearch = $event" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table
v-loading="loading"
border
:data="approvalList"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
style="width: 100%"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="申报编号" align="center" prop="applicationNo" width="140" show-overflow-tooltip />
<el-table-column label="项目名称" align="center" prop="projectName" min-width="200" show-overflow-tooltip />
<el-table-column label="申报人" align="center" prop="applicantName" width="100" />
<el-table-column label="申报部门" align="center" prop="deptName" width="120" show-overflow-tooltip />
<el-table-column label="总投资(万元)" align="center" prop="totalInvestment" width="120">
<template #default="scope">
<span class="amount-text">{{ formatAmount(scope.row.totalInvestment) }}</span>
</template>
</el-table-column>
<el-table-column label="当前审批人" align="center" prop="currentApproverName" width="120" />
<el-table-column label="审批状态" align="center" prop="approvalStatus" width="100">
<template #default="scope">
<dict-tag :options="cw_approval_status" :value="scope.row.approvalStatus" v-if="cw_approval_status && cw_approval_status.length > 0" />
</template>
</el-table-column>
<el-table-column label="提交时间" align="center" prop="submitTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.submitTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column label="审批时间" align="center" prop="approvalTime" width="180">
<template #default="scope">
<span>{{ scope.row.approvalTime ? parseTime(scope.row.approvalTime, '{y}-{m}-{d} {h}:{i}') : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="220" class-name="small-padding fixed-width" fixed="right">
<template #default="scope">
<el-tooltip content="查看详情" placement="top">
<el-button link type="info" icon="View" @click="handleView(scope.row)"></el-button>
</el-tooltip>
<!-- 待审批状态可以进行审批操作 -->
<template v-if="scope.row.approvalStatus === 'submitted' || scope.row.approvalStatus === 'approving'">
<el-tooltip content="通过" placement="top">
<el-button
link
type="success"
icon="Check"
@click="handleApprove(scope.row)"
v-hasPermi="['cw:application:approve']"
></el-button>
</el-tooltip>
<el-tooltip content="拒绝" placement="top">
<el-button
link
type="danger"
icon="Close"
@click="handleReject(scope.row)"
v-hasPermi="['cw:application:approve']"
></el-button>
</el-tooltip>
<el-tooltip content="退回" placement="top">
<el-button
link
type="warning"
icon="RefreshLeft"
@click="handleReturn(scope.row)"
v-hasPermi="['cw:application:approve']"
></el-button>
</el-tooltip>
</template>
<!-- 所有状态都可以查看审批记录 -->
<el-tooltip content="审批记录" placement="top">
<el-button link type="primary" icon="Document" @click="handleApprovalHistory(scope.row)"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page="queryParams.pageNum"
:limit="queryParams.pageSize"
@update:page="queryParams.pageNum = $event"
@update:limit="queryParams.pageSize = $event"
@pagination="getList"
/>
</el-card>
<!-- 审批对话框 -->
<el-dialog
v-model="approvalDialog.visible"
:title="approvalDialog.title"
width="800px"
append-to-body
@close="closeApprovalDialog"
>
<el-form ref="approvalFormRef" :model="approvalForm" :rules="approvalRules" label-width="120px">
<!-- 申报信息展示 -->
<el-form-item label="申报信息">
<el-descriptions :column="2" border>
<el-descriptions-item label="申报编号">{{ currentApplication.applicationNo }}</el-descriptions-item>
<el-descriptions-item label="项目名称">{{ currentApplication.projectName }}</el-descriptions-item>
<el-descriptions-item label="申报人">{{ currentApplication.applicantName }}</el-descriptions-item>
<el-descriptions-item label="申报部门">{{ currentApplication.deptName }}</el-descriptions-item>
<el-descriptions-item label="总投资">{{ formatAmount(currentApplication.totalInvestment) }} 万元</el-descriptions-item>
<el-descriptions-item label="预算年度">{{ currentApplication.year }}</el-descriptions-item>
</el-descriptions>
</el-form-item>
<!-- 审批决定 -->
<el-form-item label="审批决定" prop="approvalResult">
<el-radio-group v-model="approvalForm.approvalResult">
<el-radio value="approved">
<el-icon class="approve-icon"><Check /></el-icon>
通过
</el-radio>
<el-radio value="rejected">
<el-icon class="reject-icon"><Close /></el-icon>
拒绝
</el-radio>
<el-radio value="returned">
<el-icon class="return-icon"><RefreshLeft /></el-icon>
退回
</el-radio>
</el-radio-group>
</el-form-item>
<!-- 审批意见 -->
<el-form-item label="审批意见" prop="approvalComment">
<el-input
v-model="approvalForm.approvalComment"
type="textarea"
:rows="4"
placeholder="请输入审批意见(必填)"
maxlength="500"
show-word-limit
/>
</el-form-item>
<!-- 下一审批人(如果需要) -->
<el-form-item label="下一审批人" prop="nextApprover" v-if="showNextApprover">
<el-select v-model="approvalForm.nextApprover" placeholder="请选择下一审批人" style="width: 100%">
<el-option
v-for="approver in approverList"
:key="approver.userId"
:label="approver.userName"
:value="approver.userId"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeApprovalDialog">取消</el-button>
<el-button type="primary" @click="submitApproval" :loading="approvalSubmitting">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog
v-model="detailDialog.visible"
:title="detailDialog.title"
width="90%"
:close-on-click-modal="false"
>
<application-summary :application="currentApplication" />
<template #footer>
<div class="dialog-footer">
<el-button
v-if="canApprove(currentApplication)"
type="primary"
@click="approveFromDetail"
>审批</el-button>
<el-button @click="detailDialog.visible = false">关闭</el-button>
</div>
</template>
</el-dialog>
<!-- 审批记录对话框 -->
<el-dialog
v-model="historyDialog.visible"
title="审批记录"
width="900px"
append-to-body
>
<el-timeline>
<el-timeline-item
v-for="(record, index) in approvalHistory"
:key="index"
:icon="getHistoryIcon(record.approvalResult)"
:type="getHistoryType(record.approvalResult)"
:timestamp="parseTime(record.approvalTime, '{y}-{m}-{d} {h}:{i}')"
placement="top"
>
<el-card shadow="hover">
<div class="history-header">
<span class="approver-name">{{ record.approverName }}</span>
<el-tag :type="getStatusTagType(record.approvalResult)">
{{ getApprovalResultText(record.approvalResult) }}
</el-tag>
</div>
<div class="history-content" v-if="record.approvalComment">
<p>{{ record.approvalComment }}</p>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
<template #footer>
<div class="dialog-footer">
<el-button @click="historyDialog.visible = false">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import {
listApprovalRecord,
getApprovalRecord,
approveApplication,
batchApproveApplication,
getApprovalHistory,
getPendingApprovals,
getMyApprovals,
getDepartmentApprovals,
getAvailableApprovers,
exportApprovalRecord
} from "@/api/cw/approvalRecord";
import { useDict } from "@/utils/dict";
import ApplicationSummary from "../application/summary.vue";
export default {
name: 'CwApprovalRecord',
components: {
ApplicationSummary
},
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 审批列表
approvalList: [],
// 时间范围
daterangeSubmitTime: [],
// 动画配置
animate: {
searchAnimate: {
enter: 'animate__animated animate__fadeIn',
leave: 'animate__animated animate__fadeOut'
}
},
// 字典数据
cw_approval_status: [],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
applicationNo: '',
projectName: '',
applicantName: '',
approvalStatus: '',
approverName: '',
submitTimeBegin: '',
submitTimeEnd: ''
},
// 审批对话框
approvalDialog: {
visible: false,
title: ''
},
// 详情对话框
detailDialog: {
visible: false,
title: ''
},
// 历史记录对话框
historyDialog: {
visible: false
},
// 当前申报信息
currentApplication: {},
// 审批表单
approvalForm: {
applicationId: null,
approvalResult: '',
approvalComment: '',
nextApprover: null
},
// 审批表单验证
approvalRules: {
approvalResult: [
{ required: true, message: '请选择审批结果', trigger: 'change' }
],
approvalComment: [
{ required: true, message: '请输入审批意见', trigger: 'blur' },
{ min: 5, max: 500, message: '审批意见长度在 5 到 500 个字符', trigger: 'blur' }
]
},
// 审批提交状态
approvalSubmitting: false,
// 审批人列表
approverList: [],
// 审批历史
approvalHistory: []
};
},
computed: {
// 是否显示下一审批人选择
showNextApprover() {
return this.approvalForm.approvalResult === 'approved';
}
},
created() {
this.loadDictData();
this.getList();
},
methods: {
/** 加载字典数据 */
async loadDictData() {
try {
const dictData = useDict('cw_approval_status');
// 监听字典数据变化
this.$watch(() => dictData.cw_approval_status, (newVal) => {
this.cw_approval_status = newVal || [];
}, { immediate: true });
} catch (error) {
console.error('加载审批状态字典数据失败:', error);
}
},
/** 查询审批列表 */
getList() {
this.loading = true;
// 处理时间范围
this.queryParams.submitTimeBegin = this.daterangeSubmitTime[0] || '';
this.queryParams.submitTimeEnd = this.daterangeSubmitTime[1] || '';
listApprovalRecord(this.queryParams).then(response => {
this.approvalList = response.rows;
this.total = response.total;
this.loading = false;
}).catch(error => {
console.error('获取审批列表失败:', error);
this.loading = false;
});
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.daterangeSubmitTime = [];
this.$refs.queryFormRef.resetFields();
this.handleQuery();
},
/** 多选框选中数据 */
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
/** 快捷操作 */
showPendingApprovals() {
this.queryParams = {
...this.queryParams,
approvalStatus: 'submitted',
currentApprover: 'current'
};
this.handleQuery();
},
showMyApprovals() {
this.queryParams = {
...this.queryParams,
approverName: 'current'
};
this.handleQuery();
},
showDepartmentApprovals() {
this.queryParams = {
...this.queryParams,
deptId: 'current'
};
this.handleQuery();
},
showApprovalStatistics() {
this.$router.push('/cw/approvalRecord/statistics');
},
/** 查看详情 */
handleView(row) {
const id = row.id || row.applicationId;
getApprovalRecord(id).then(response => {
this.currentApplication = response.data;
this.detailDialog.visible = true;
this.detailDialog.title = "申报详情";
}).catch(error => {
console.error('获取详情失败:', error);
this.$modal.msgError('获取详情失败');
});
},
/** 从详情页审批 */
approveFromDetail() {
this.detailDialog.visible = false;
this.handleApprove(this.currentApplication);
},
/** 单个审批通过 */
handleApprove(row) {
this.openApprovalDialog('approved', row, '审批通过');
},
/** 单个审批拒绝 */
handleReject(row) {
this.openApprovalDialog('rejected', row, '审批拒绝');
},
/** 单个审批退回 */
handleReturn(row) {
this.openApprovalDialog('returned', row, '审批退回');
},
/** 批量审批通过 */
handleBatchApprove() {
if (this.ids.length === 0) {
this.$modal.msgWarning('请选择要审批的记录');
return;
}
this.openBatchApprovalDialog('approved', '批量审批通过');
},
/** 批量审批拒绝 */
handleBatchReject() {
if (this.ids.length === 0) {
this.$modal.msgWarning('请选择要审批的记录');
return;
}
this.openBatchApprovalDialog('rejected', '批量审批拒绝');
},
/** 打开审批对话框 */
openApprovalDialog(result, row, title) {
this.currentApplication = row;
this.approvalForm = {
applicationId: row.id,
approvalResult: result,
approvalComment: '',
nextApprover: null
};
// 如果是通过状态,加载可选审批人
if (result === 'approved') {
this.loadAvailableApprovers(row.id);
}
this.approvalDialog.visible = true;
this.approvalDialog.title = title;
},
/** 打开批量审批对话框 */
openBatchApprovalDialog(result, title) {
this.approvalForm = {
applicationIds: this.ids,
approvalResult: result,
approvalComment: '',
nextApprover: null
};
this.approvalDialog.visible = true;
this.approvalDialog.title = title;
},
/** 加载可选审批人 */
loadAvailableApprovers(applicationId) {
getAvailableApprovers(applicationId).then(response => {
this.approverList = response.data || [];
}).catch(error => {
console.error('获取审批人列表失败:', error);
this.approverList = [];
});
},
/** 关闭审批对话框 */
closeApprovalDialog() {
this.approvalDialog.visible = false;
this.approvalForm = {
applicationId: null,
approvalResult: '',
approvalComment: '',
nextApprover: null
};
this.approverList = [];
if (this.$refs.approvalFormRef) {
this.$refs.approvalFormRef.resetFields();
}
},
/** 提交审批 */
submitApproval() {
this.$refs.approvalFormRef.validate(valid => {
if (valid) {
this.approvalSubmitting = true;
const apiCall = this.approvalForm.applicationIds
? batchApproveApplication(this.approvalForm)
: approveApplication(this.approvalForm);
apiCall.then(response => {
this.$modal.msgSuccess("审批成功");
this.closeApprovalDialog();
this.getList();
}).catch(error => {
console.error('审批失败:', error);
this.$modal.msgError('审批失败');
}).finally(() => {
this.approvalSubmitting = false;
});
}
});
},
/** 查看审批历史 */
handleApprovalHistory(row) {
getApprovalHistory(row.id || row.applicationId).then(response => {
this.approvalHistory = response.data || [];
this.historyDialog.visible = true;
}).catch(error => {
console.error('获取审批历史失败:', error);
this.$modal.msgError('获取审批历史失败');
// 显示模拟数据
this.approvalHistory = [
{
approverName: '系统',
approvalResult: 'submitted',
approvalComment: '申报材料已提交,等待审批',
approvalTime: new Date().toISOString(),
stepName: '申报提交',
stepOrder: 1
}
];
this.historyDialog.visible = true;
});
},
/** 导出 */
handleExport() {
exportApprovalRecord(this.queryParams).then(() => {
this.$modal.msgSuccess('导出成功');
}).catch(error => {
console.error('导出失败:', error);
this.$modal.msgError('导出失败');
});
},
/** 判断是否可以审批 */
canApprove(row) {
return row.approvalStatus === 'submitted' || row.approvalStatus === 'approving';
},
/** 格式化金额 */
formatAmount(amount) {
return amount ? amount.toFixed(2) : '0.00';
},
/** 获取历史记录图标 */
getHistoryIcon(result) {
const iconMap = {
'submitted': 'Upload',
'approved': 'Check',
'rejected': 'Close',
'returned': 'RefreshLeft'
};
return iconMap[result] || 'Document';
},
/** 获取历史记录类型 */
getHistoryType(result) {
const typeMap = {
'submitted': 'primary',
'approved': 'success',
'rejected': 'danger',
'returned': 'warning'
};
return typeMap[result] || 'info';
},
/** 获取状态标签类型 */
getStatusTagType(status) {
const statusMap = {
'submitted': 'warning',
'approved': 'success',
'rejected': 'danger',
'returned': 'warning'
};
return statusMap[status] || 'info';
},
/** 获取审批结果文本 */
getApprovalResultText(result) {
const textMap = {
'submitted': '已提交',
'approved': '通过',
'rejected': '拒绝',
'returned': '退回'
};
return textMap[result] || result;
}
}
};
</script>
<style lang="scss" scoped>
.page-container {
min-height: calc(100vh - 84px);
}
// 快捷操作卡片
.quick-action-card {
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid #e4e7ed;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-color: #409EFF;
}
.quick-action {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
.quick-icon {
font-size: 28px;
color: #409EFF;
margin-bottom: 8px;
}
span {
font-size: 14px;
color: #606266;
font-weight: 500;
}
}
}
// 表格卡片
.button-card.table-card {
border-radius: 8px;
border: 1px solid #e4e7ed;
}
// 金额文本
.amount-text {
font-weight: 600;
color: #E6A23C;
}
// 审批对话框样式
.approve-icon {
color: #67C23A;
margin-right: 4px;
}
.reject-icon {
color: #F56C6C;
margin-right: 4px;
}
.return-icon {
color: #E6A23C;
margin-right: 4px;
}
// 审批历史样式
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.approver-name {
font-weight: 600;
color: #2c3e50;
}
}
.history-content {
color: #606266;
line-height: 1.6;
p {
margin: 0;
}
}
// 响应式设计
@media (max-width: 768px) {
.quick-action {
padding: 15px;
.quick-icon {
font-size: 24px;
}
span {
font-size: 12px;
}
}
}
</style>
+319
View File
@@ -0,0 +1,319 @@
<template>
<div class="config-detail">
<!-- 基本信息 -->
<el-card class="detail-card mb-4">
<template #header>
<div class="card-header">
<el-icon><InfoFilled /></el-icon>
<span>基本信息</span>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="配置名称">
{{ config.configName }}
</el-descriptions-item>
<el-descriptions-item label="审批类型">
<el-tag :type="getApprovalTypeTag(config.approvalType)">
{{ getApprovalTypeText(config.approvalType) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="适用范围">
{{ getScopeText(config.applicableScope) }}
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="config.status === '1' ? 'success' : 'danger'">
{{ config.status === '1' ? '启用' : '禁用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="排序">
{{ config.orderNum }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ parseTime(config.createTime, '{y}-{m}-{d} {h}:{i}') }}
</el-descriptions-item>
</el-descriptions>
<div v-if="config.description" class="mt-4">
<h4 class="section-title">配置描述</h4>
<p class="description-text">{{ config.description }}</p>
</div>
</el-card>
<!-- 审批流程 -->
<el-card class="detail-card" v-if="config.steps && config.steps.length > 0">
<template #header>
<div class="card-header">
<el-icon><Share /></el-icon>
<span>审批流程</span>
<el-tag type="info" size="small">共 {{ config.steps.length }} 个步骤</el-tag>
</div>
</template>
<el-steps :active="config.steps.length" finish-status="success" align-center>
<el-step
v-for="(step, index) in config.steps"
:key="index"
:title="step.stepName"
:description="`${getApprovalModeText(step.approvalMode)} | ${step.timeoutHours}小时`"
/>
</el-steps>
<div class="steps-detail mt-6">
<div
v-for="(step, index) in config.steps"
:key="index"
class="step-detail-item"
>
<el-card shadow="hover" class="step-detail-card">
<template #header>
<div class="step-header">
<span class="step-number">步骤 {{ index + 1 }}</span>
<span class="step-name">{{ step.stepName }}</span>
<el-tag :type="getModeTagType(step.approvalMode)" size="small">
{{ getApprovalModeText(step.approvalMode) }}
</el-tag>
</div>
</template>
<el-row :gutter="20">
<el-col :span="8">
<div class="step-info">
<label class="info-label">审批方式:</label>
<span>{{ getApprovalModeText(step.approvalMode) }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="step-info">
<label class="info-label">超时时间:</label>
<span>{{ step.timeoutHours }} 小时</span>
</div>
</el-col>
<el-col :span="8">
<div class="step-info">
<label class="info-label">审批人数:</label>
<span>{{ step.approvers ? step.approvers.length : 0 }} 人</span>
</div>
</el-col>
</el-row>
<div class="approvers-section mt-4" v-if="step.approvers && step.approvers.length > 0">
<label class="info-label">审批人员:</label>
<div class="approvers-list">
<el-tag
v-for="(approver, approverIndex) in step.approverNames"
:key="approverIndex"
class="approver-tag"
>
{{ approver }}
</el-tag>
</div>
</div>
<div v-else-if="config.approvalType === 'custom'" class="custom-approver-notice">
<el-alert
title="自选审批人"
description="申报人在提交时自行选择审批人"
type="info"
show-icon
:closable="false"
/>
</div>
</el-card>
</div>
</div>
</el-card>
<!-- 无流程步骤时的提示 -->
<el-card class="detail-card" v-else>
<el-empty description="暂无配置的审批流程步骤" />
</el-card>
</div>
</template>
<script>
export default {
name: 'ApproverConfigDetail',
props: {
config: {
type: Object,
default: () => ({})
}
},
methods: {
/** 获取审批类型标签 */
getApprovalTypeTag(type) {
const typeMap = {
'fixed': 'success',
'role': 'warning',
'dept': 'info',
'custom': 'primary'
};
return typeMap[type] || 'info';
},
/** 获取审批类型文本 */
getApprovalTypeText(type) {
const textMap = {
'fixed': '固定审批人',
'role': '角色审批',
'dept': '部门审批',
'custom': '自选审批人'
};
return textMap[type] || type;
},
/** 获取适用范围文本 */
getScopeText(scope) {
const scopeMap = {
'all': '全部',
'dept': '部门',
'project_type': '项目类型',
'amount_range': '金额范围'
};
return scopeMap[scope] || scope;
},
/** 获取审批方式文本 */
getApprovalModeText(mode) {
const modeMap = {
'single': '单人审批',
'multi_sign': '多人会签',
'multi_or': '多人或签'
};
return modeMap[mode] || mode;
},
/** 获取审批方式标签类型 */
getModeTagType(mode) {
const typeMap = {
'single': 'primary',
'multi_sign': 'success',
'multi_or': 'warning'
};
return typeMap[mode] || 'info';
}
}
};
</script>
<style lang="scss" scoped>
.config-detail {
.detail-card {
border-radius: 8px;
border: 1px solid #e4e7ed;
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #2c3e50;
}
}
.section-title {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #2c3e50;
}
.description-text {
margin: 0;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
color: #606266;
line-height: 1.6;
}
.steps-detail {
.step-detail-item {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.step-detail-card {
border: 1px solid #e4e7ed;
.step-header {
display: flex;
align-items: center;
gap: 12px;
.step-number {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.step-name {
font-weight: 600;
color: #2c3e50;
flex: 1;
}
}
}
.step-info {
display: flex;
align-items: center;
margin-bottom: 8px;
.info-label {
font-weight: 600;
color: #909399;
margin-right: 8px;
min-width: 80px;
}
}
.approvers-section {
border-top: 1px solid #f0f0f0;
padding-top: 16px;
.info-label {
font-weight: 600;
color: #909399;
margin-bottom: 8px;
display: block;
}
.approvers-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
.approver-tag {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
border: none;
}
}
}
.custom-approver-notice {
border-top: 1px solid #f0f0f0;
padding-top: 16px;
}
}
}
.mb-4 {
margin-bottom: 16px;
}
.mt-4 {
margin-top: 16px;
}
.mt-6 {
margin-top: 24px;
}
</style>
Loaded 100 of 662 files, more files were not shown because too many files have changed in this diff. Show more