上报功能开发完成

pull/838/head
wuxingyu 2025-12-06 22:06:08 +08:00
parent bf502e76d5
commit 1f6f6d58ba
36 changed files with 7311 additions and 0 deletions

View File

@ -1,6 +1,9 @@
<script lang="ts" setup>
import { isDark } from '@/utils/is'
import { useAppStore } from '@/store/modules/app'
import { useDeptStore } from '@/store/modules/dept'
import { useUserListStore } from '@/store/modules/userList'
import { useRewardsPunishReasonStore } from '@/store/modules/rewardsPunishReason'
import { useDesign } from '@/hooks/web/useDesign'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import routerSearch from '@/components/RouterSearch/index.vue'
@ -10,10 +13,31 @@ defineOptions({ name: 'APP' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('app')
const appStore = useAppStore()
const deptStore = useDeptStore()
const currentSize = computed(() => appStore.getCurrentSize)
const greyMode = computed(() => appStore.getGreyMode)
const { wsCache } = useCache()
//
const initDeptInfo = async () => {
await deptStore.setDeptInfoAction()
}
initDeptInfo()
//
const userListStore = useUserListStore()
const initUserListInfo = async () => {
await userListStore.setUserListAction()
}
initUserListInfo()
//
const rewardsPunishReasonStore = useRewardsPunishReasonStore()
const initRewardsPunishReasonInfo = async () => {
await rewardsPunishReasonStore.setRewardsPunishReasonAction()
}
initRewardsPunishReasonInfo()
//
const setDefaultTheme = () => {
let isDarkTheme = wsCache.get(CACHE_KEY.IS_DARK)

View File

@ -0,0 +1,28 @@
import request from '@/config/axios'
/**
* API
*/
// 查询上报详情
export const getMyReportDetail = (id: string) => {
return request.get({
url: `/submit/myReporting/get`,
params: { id }
})
}
// 保存评论
export const saveComment = (reportingId: string, commentVO: any) => {
return request.post({
url: `/submit/comment/${reportingId}`,
data: commentVO
})
}
// 删除评论
export const deleteComment = (commentId: string) => {
return request.delete({
url: `/submit/comment/${commentId}`
})
}

View File

@ -0,0 +1,27 @@
// api/submit/companyReport/index.ts
import { getMyReportPage, exportMyReport } from '../myReporting'
import { getMyReportDetail } from '../common/common'
import type { QueryParams } from '../myReporting/types'
// 查询公司通报列表(复用我的上报列表接口,通过参数区分)
export const getCompanyReportPage = (params: QueryParams) => {
// 设置参数以获取公司通报数据
return getMyReportPage({
...params,
isOwnReport: false,
isOwnApprove: false
})
}
// 查询公司通报详情(复用我的上报详情接口)
export const getCompanyReportDetail = getMyReportDetail
// 导出公司通报(复用我的上报导出接口)
export const exportCompanyReport = (params: QueryParams) => {
// 设置参数以导出公司通报数据
return exportMyReport({
...params,
isOwnReport: false,
isOwnApprove: false
})
}

View File

@ -0,0 +1,21 @@
// api/submit/myApprove/index.ts
import request from '@/config/axios'
import type { QueryParams } from '../myReporting/types'
import type { ApproveParams } from './types'
import { getMyReportPage, getRuleScore, exportMyReport } from '../myReporting'
// 查询审批列表 - 复用myReporting的查询接口
export const getApproveList = (params: QueryParams) => {
return getMyReportPage(params)
}
// 审批上报
export const approveMyReport = (data: ApproveParams) => {
return request.post({
url: '/submit/myApprove/approve',
data
})
}
// 获取规则分值 - 复用myReporting的规则分值接口
export { getRuleScore, exportMyReport }

View File

@ -0,0 +1,53 @@
// api/submit/myApprove/types.ts
// 用户积分记录
export interface UserIntegralRecord {
userId: string;
userName: string;
score: number | null;
}
// 审批请求参数
export interface ApproveParams {
id: string;
action: string; // APPROVED 或 REFUSE
comment?: string;
userIntegralRecords?: UserIntegralRecord[];
}
// 审批列表查询参数
export interface ApproveListParams {
pageNo: number;
pageSize: number;
title?: string;
deptId?: string;
personName?: string;
status?: string;
startTime?: string;
endTime?: string;
dateRange?: string[];
}
// 审批记录详情
export interface ApproveRecord {
id: string;
title: string;
deptId: string;
deptName: string;
reportType: string;
reportTypeName: string;
personName: string;
personId?: string;
remark?: string;
status: number | string;
approverId?: string;
approverName?: string;
approveTime?: string;
approveComment?: string;
createBy?: string;
createTime?: string;
updateBy?: string;
updateTime?: string;
userIntegralRecords?: UserIntegralRecord[];
fillingTime?: string;
}

View File

@ -0,0 +1,60 @@
// api/submit/myReporting/index.ts
import request from '@/config/axios'
import type { QueryParams, SaveParams, RuleScoreResponse } from './types'
// 查询我的上报列表
export const getMyReportPage = (params: QueryParams) => {
return request.get({
url: '/submit/myReporting/page',
params
})
}
// 创建我的上报
export const createMyReport = (data: SaveParams) => {
return request.post({
url: '/submit/myReporting/create',
data
})
}
// 更新我的上报
export const updateMyReport = (data: SaveParams) => {
return request.put({
url: '/submit/myReporting/update',
data
})
}
// 删除我的上报
export const deleteMyReport = (id: string) => {
return request.delete({
url: `/submit/myReporting/delete`,
params: { id }
})
}
// 导出我的上报
export const exportMyReport = (params: QueryParams) => {
return request.download({
url: '/submit/myReporting/export-excel',
params
})
}
// 提交上报
export const commitMyReport = (id: string) => {
return request.post({
url: `/submit/myReporting/commit`,
params: { id }
})
}
// 获取规则分值
export const getRuleScore = (reasonId: string, personId: string) => {
return request.get<RuleScoreResponse>({
url: `/submit/ruleManage/score/${reasonId}/${personId}`
})
}

View File

@ -0,0 +1,81 @@
// api/submit/myReporting/types.ts
export interface UserIntegralRecord {
userId: string;
userName: string;
score: number | null;
}
export interface Reason {
id: string;
name?: string;
}
export interface RuleScoreResponse {
type: string;
scores: number[];
}
export interface ReportingCommentDTO {
comment: string;
createUserName: string;
createTime: string;
}
export interface MyReportDTO {
id: string;
title: string;
deptId: string;
deptName: string;
reportType: string;
reportTypeName: string;
personName: string;
personId?: string;
remark?: string;
imageUrls?: string[];
status: number | string;
approverId?: string;
approverName?: string;
approveTime?: string;
approveComment?: string;
createBy?: string;
createTime?: string;
updateBy?: string;
updateTime?: string;
reason?: Reason;
userIntegralRecords?: UserIntegralRecord[];
comments?: ReportingCommentDTO[];
fillingTime?: string;
imageIds?: string;
}
export interface QueryParams {
pageNo: number;
pageSize: number;
title?: string;
deptId?: string;
personName?: string;
reasonId?: string;
status?: string;
isOwnReport: boolean;
isOwnApprove:boolean;
startTime?: string;
endTime?: string;
dateRange?: string[];
}
export interface SaveParams {
id?: string;
title: string;
deptId: string;
deptName?: string;
reasonId?: string;
personName: string;
personId?: string;
remark?: string;
content?: string;
imageUrls?: string[];
imageIds?: string;
reason?: Reason;
userIntegralRecords?: UserIntegralRecord[];
comments?: Array<{ comment: string; createUserName: string }>;
}

View File

@ -0,0 +1,43 @@
import request from '@/config/axios'
import type { PaySlipDTO, QueryParams, PageResponse } from './types'
// 下载工资条导入模板
export const downloadImportTemplate = () => {
return request.download({ url: '/submit/paySlip/download/importTemplate' })
}
// 导入工资条
export const importPaySlip = (data: FormData) => {
return request.post({ url: '/submit/paySlip/import', data, headers: { 'Content-Type': 'multipart/form-data' } })
}
// 查询工资条列表
export const getPaySlipPage = (params: QueryParams) => {
return request.get<PageResponse<PaySlipDTO>>({ url: '/submit/paySlip/page', params })
}
// 查询工资条详情
export const getPaySlipDetail = (id: string) => {
return request.get<PaySlipDTO>({ url: `/submit/paySlip/detail/${id}` })
}
// 删除工资条
export const deletePaySlip = (id: string) => {
return request.delete({ url: `/submit/paySlip/${id}` })
}
// 签名工资条
export const signPaySlip = (id: string) => {
return request.post({ url: `/submit/paySlip/sign/${id}` })
}
// 发送短信
export const sendMsm = (id: string) => {
return request.get({ url: `/submit/paySlip/sendMsm/${id}` })
}
// 批量发送短信
export const batchSendMsm = (ids: string[]) => {
// 直接将 ids 数组作为请求体发送
return request.post({ url: '/submit/paySlip/batch/sendMsm', data: ids });
};

View File

@ -0,0 +1,61 @@
// 工资条相关类型定义
export enum YesOrNotEnum {
YES = 'YES', // 是
NO = 'NO' // 否
}
// 工资条DTO
export interface PaySlipDTO {
id: string; // 主键
month?: string; // 月份
year?: string; // 年份
fullYearMonth?: string; // 完整月份,格式:年-月
employeeId?: string; // 工号
mobileNo?: string; // 手机号
employeeName?: string; // 真实姓名
salary?: number; // 工资
attendanceDays?: number; // 出勤天数
attendanceSalary?: number; // 出勤工资
communicationFees?: number; // 通讯费
overtimePay?: number; // 加班费
jobSubsidies?: number; // 岗位补贴
fullAttendanceBonus?: number; // 全勤奖
mealExpenses?: number; // 扣餐费
housingSupplement?: number; // 房补
supplementFees?: number; // 补发
insurancePremiumExpenses?: number; // 扣保险金
penaltyDeductionExpenses?: number; // 扣罚
marginExpenses?: number; // 扣保证金
refundOfDeposit?: number; // 退回保证金
salariesPayable?: number; // 应发工资
msmSend?: string; // 是否已发送短信 (Y/N)
msmSendTime?: string; // 短信发送时间
signed?: string; // 是否已签名 (Y/N)
signedImageIds?: string; // 签名图片IDs
signedTime?: string; // 签名时间
arrangeSigningDate?: string; // 安排签署日期
performance?: number; // 绩效
seniorityAward?: number; // 工龄奖
workingClothes?: number; // 工服费
}
// 工资条查询参数
export interface QueryParams {
pageNo: number; // 页码
pageSize: number; // 每页条数
month?: string; // 月份
year?: string; // 年份
yearMonth?: string; // 完整月份,格式:年-月
employeeId?: string; // 工号
mobileNo?: string; // 手机号
employeeName?: string; // 真实姓名
signed?: string; // 是否已签名 (Y/N)
msmSend?: string; // 是否已发送短信 (Y/N)
}
// 分页响应
export interface PageResponse<T> {
list: T[];
total: number;
}

View File

@ -0,0 +1,32 @@
import request from '@/config/axios'
import type { ReportManageDTO, QueryParams, SaveParams, PageResponse } from './types'
// 查询报表管理列表
export const getReportManagePage = (params: QueryParams) => {
return request.get<PageResponse<ReportManageDTO>>({ url: '/submit/reportManage/page', params })
}
// 查询报表管理详情
export const getReportManageDetail = (id: string) => {
return request.get<ReportManageDTO>({ url: `/submit/reportManage/${id}` })
}
// 创建报表管理
export const createReportManage = (data: SaveParams) => {
return request.post({ url: '/submit/reportManage/create', data })
}
// 更新报表管理
export const updateReportManage = (data: SaveParams) => {
return request.put({ url: '/submit/reportManage/update', data })
}
// 删除报表管理
export const deleteReportManage = (id: string) => {
return request.delete({ url: `/submit/reportManage/delete/${id}` })
}
// 根据类型查询报表管理列表
export const getReportManageListByType = (type: string) => {
return request.get<ReportManageDTO[]>({ url: '/submit/reportManage/list-by-type', params: { type } })
}

View File

@ -0,0 +1,39 @@
// 报表管理相关类型定义
// 报表管理DTO
export interface ReportManageDTO {
id: string; // 主键
code: string; // 编码
name: string; // 名称
orderNum: number; // 排序序号
enable: number; // 状态1启用 0禁用
remark?: string; // 描述
createTime?: string; // 创建时间
updateTime?: string; // 更新时间
}
// 报表管理查询参数
export interface QueryParams {
pageNo: number; // 页码
pageSize: number; // 每页条数
code?: string; // 编码
name?: string; // 名称
enable?: string; // 状态
}
// 报表管理保存参数
export interface SaveParams {
id?: string; // 主键
code: string; // 编码
name: string; // 名称
orderNum: number; // 排序序号
enable: number; // 状态1启用 0禁用
remark?: string; // 描述
type: 'REWARDS_PUNISH_REASON';
}
// 分页响应
export interface PageResponse<T> {
list: T[];
total: number;
}

View File

@ -0,0 +1,32 @@
import request from '@/config/axios'
import type { IntegralRule, QueryParams, SaveParams, PageResponse } from './types'
// 查询积分规则列表
export const getIntegralRulePage = (params: QueryParams) => {
return request.get<PageResponse<IntegralRule>>({ url: '/submit/ruleManage/page', params })
}
// 查询积分规则详情
export const getIntegralRuleDetail = (id: string) => {
return request.get<IntegralRule>({ url: `/submit/ruleManage/get/${id}` })
}
// 根据事项ID和用户ID获取规则分值
export const getRuleScore = (reasonId: string, userId: string) => {
return request.get({ url: `/submit/ruleManage/getRuleScore/${reasonId}/${userId}` })
}
// 创建积分规则
export const createIntegralRule = (data: SaveParams) => {
return request.post({ url: '/submit/ruleManage/create', data })
}
// 更新积分规则
export const updateIntegralRule = (data: SaveParams) => {
return request.put({ url: '/submit/ruleManage/update', data })
}
// 删除积分规则
export const deleteIntegralRule = (id: string) => {
return request.delete({ url: `/submit/ruleManage/delete/${id}` })
}

View File

@ -0,0 +1,41 @@
// 积分规则类型定义
export interface IntegralRule {
id: string
title: string
reason: {
id: string
name: string
}
type: string
score: number
scoreRange: string[]
formula: string
}
// 查询参数
export interface QueryParams {
title?: string
reasonId?: string
type?: string
pageNo?: number
pageSize?: number
}
// 保存参数
export interface SaveParams {
id?: string
title: string
reasonId: string
type: string
score?: number
scoreRange?: string[]
formula?: string
}
// 分页响应
export interface PageResponse<T> {
list: T[]
total: number
totalPages: number
}

View File

@ -0,0 +1,25 @@
import request from '@/config/axios'
// 用户积分API接口
export const userScoreApi = {
// 查询用户积分排行
getUserScoreStatic: (params: any) => {
return request.get({ url: '/submit/userScore/static', params })
},
// 同步用户积分数据
syncUserScore: () => {
return request.get({ url: '/submit/userScore/init' })
},
// 导出用户积分排行
exportUserScoreStatic: (params: any) => {
return request.download({ url: '/submit/userScore/static/export', params })
},
// 查询用户积分明细
getUserScoreList: (params: any) => {
return request.get({ url: '/submit/userScore/list', params })
},
// 导出用户积分明细
exportUserScoreList: (params: any) => {
return request.download({ url: '/submit/userScore/export', params })
}
}

View File

@ -0,0 +1,77 @@
// 用户积分列表请求参数
export interface UserScoreListReqVO {
// 部门名称
deptName?: string;
// 部门Id
orgId?: number;
// 用户名称
userName?: string;
// 用户Id
userId?: string;
// 开始月份格式yyyy-MM
beginMonth?: string;
// 结束月份格式yyyy-MM
endMonth?: string;
// 排名前N位
top?: number;
// 页码
pageNo?: number;
// 每页条数
pageSize?: number;
}
// 用户积分列表响应数据
export interface UserScoreListRespVO {
// 排名
ranking: number;
// 部门名称
deptName: string;
// 用户名称
userName: string;
// 积分
score: number;
// 用户ID
userId: string;
}
// 用户积分详情请求参数
export interface UserScoreDetailReqVO {
// 用户ID
userId: string;
// 开始月份格式yyyy-MM
beginMonth: string;
// 结束月份格式yyyy-MM
endMonth: string;
}
// 用户积分详情响应数据
export interface UserScoreDetailRespVO {
// 用户ID
userId: string;
// 排名
ranking: number;
// 总积分
totalScore: number;
// 用户名称
userName: string;
// 部门名称
deptName: string;
// 开始月份格式yyyy-MM
beginMonth: string;
// 结束月份格式yyyy-MM
endMonth: string;
// 积分明细列表
scoreDetails: ScoreDetail[];
}
// 积分明细
export interface ScoreDetail {
// 积分
score: number;
// 发生日期
happenedDate: string;
// 积分类型
type: string;
// 积分原因
reason: string;
}

View File

@ -11,6 +11,12 @@ export const CACHE_KEY = {
ROLE_ROUTERS: 'roleRouters',
USER: 'user',
VisitTenantId: 'visitTenantId',
// 部门相关(新增)
DEPT: 'dept',
// 用户列表相关(新增)
USER_LIST: 'userList',
// 奖惩原因相关(新增)
REWARDS_PUNISH_REASON: 'rewardsPunishReason',
// 系统设置
IS_DARK: 'isDark',
LANG: 'lang',

93
src/store/modules/dept.ts Normal file
View File

@ -0,0 +1,93 @@
import { store } from '@/store'
import { defineStore } from 'pinia'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { getSimpleDeptList } from '@/api/system/dept'
import { handleTree } from '@/utils/tree'
import type { DeptVO } from '@/api/system/dept'
const { wsCache } = useCache()
interface DeptState {
deptList: DeptVO[]
deptTree: DeptVO[]
isSetDept: boolean
}
export const useDeptStore = defineStore('admin-dept', {
state: (): DeptState => ({
deptList: [],
deptTree: [],
isSetDept: false
}),
getters: {
getDeptList(): DeptVO[] {
return this.deptList
},
getDeptTree(): DeptVO[] {
return this.deptTree
},
getIsSetDept(): boolean {
return this.isSetDept
}
},
actions: {
/**
*
*/
async setDeptInfoAction() {
let deptInfo = wsCache.get(CACHE_KEY.DEPT)
if (!deptInfo) {
const deptList = await getSimpleDeptList()
const deptTree = handleTree(deptList)
deptInfo = {
deptList,
deptTree
}
wsCache.set(CACHE_KEY.DEPT, deptInfo)
} else {
// 有缓存时,尝试更新部门信息
try {
const deptList = await getSimpleDeptList()
const deptTree = handleTree(deptList)
deptInfo = { deptList, deptTree }
} catch (error) {
// 更新失败时使用缓存数据
}
}
this.deptList = deptInfo.deptList
this.deptTree = deptInfo.deptTree
this.isSetDept = true
wsCache.set(CACHE_KEY.DEPT, deptInfo)
},
/**
*
*/
async updateDeptInfoAction() {
// 强制更新部门信息,不使用缓存
const deptList = await getSimpleDeptList()
const deptTree = handleTree(deptList)
this.deptList = deptList
this.deptTree = deptTree
this.isSetDept = true
const deptInfo = { deptList, deptTree }
wsCache.set(CACHE_KEY.DEPT, deptInfo)
},
/**
*
*/
resetDeptState() {
this.deptList = []
this.deptTree = []
this.isSetDept = false
wsCache.delete(CACHE_KEY.DEPT)
}
},
persist: false
})
/**
* 使 store
*/
export const useDeptStoreWithOut = () => {
return useDeptStore(store)
}

View File

@ -0,0 +1,119 @@
import { store } from '@/store'
import { defineStore } from 'pinia'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { getReportManageListByType } from '@/api/submit/systemConfig/reportManage'
import type { ReportManageDTO } from '@/api/submit/systemConfig/reportManage/types'
const { wsCache } = useCache()
interface RewardsPunishReasonState {
// 存储奖惩原因列表
rewardsPunishReasonList: Array<{
id: string
name: string
}>
// 是否已设置奖惩原因
isSetRewardsPunishReason: boolean
}
export const useRewardsPunishReasonStore = defineStore('admin-rewardsPunishReason', {
state: (): RewardsPunishReasonState => ({
rewardsPunishReasonList: [],
isSetRewardsPunishReason: false
}),
getters: {
/**
*
*/
getRewardsPunishReasonList(): Array<{ id: string; name: string }> {
return this.rewardsPunishReasonList
},
/**
*
*/
getIsSetRewardsPunishReason(): boolean {
return this.isSetRewardsPunishReason
}
},
actions: {
/**
*
*/
async setRewardsPunishReasonAction() {
let rewardsPunishReasonInfo = wsCache.get(CACHE_KEY.REWARDS_PUNISH_REASON)
if (!rewardsPunishReasonInfo) {
// 无缓存时,从接口获取
const response = await getReportManageListByType('REWARDS_PUNISH_REASON')
const reasons = this.extractRewardsPunishReason(response)
rewardsPunishReasonInfo = {
rewardsPunishReasonList: reasons
}
wsCache.set(CACHE_KEY.REWARDS_PUNISH_REASON, rewardsPunishReasonInfo)
} else {
// 有缓存时,尝试更新数据
try {
const response = await getReportManageListByType('REWARDS_PUNISH_REASON')
const reasons = this.extractRewardsPunishReason(response)
rewardsPunishReasonInfo = {
rewardsPunishReasonList: reasons
}
} catch (error) {
// 更新失败时使用缓存数据
}
}
this.rewardsPunishReasonList = rewardsPunishReasonInfo.rewardsPunishReasonList
this.isSetRewardsPunishReason = true
wsCache.set(CACHE_KEY.REWARDS_PUNISH_REASON, rewardsPunishReasonInfo)
},
/**
*
*/
extractRewardsPunishReason(reportList: ReportManageDTO[]): Array<{ id: string; name: string }> {
// 使用Map去重
const reasonMap = new Map<string, string>()
reportList.forEach(report => {
reasonMap.set(report.id, report.name)
})
// 转换为数组格式
return Array.from(reasonMap.entries()).map(([id, name]) => ({
id,
name
}))
},
/**
*
*/
async updateRewardsPunishReasonAction() {
// 强制更新数据,不使用缓存
const response = await getReportManageListByType('REWARDS_PUNISH_REASON')
const reasons = this.extractRewardsPunishReason(response)
this.rewardsPunishReasonList = reasons
this.isSetRewardsPunishReason = true
const rewardsPunishReasonInfo = {
rewardsPunishReasonList: reasons
}
wsCache.set(CACHE_KEY.REWARDS_PUNISH_REASON, rewardsPunishReasonInfo)
},
/**
*
*/
resetRewardsPunishReasonState() {
this.rewardsPunishReasonList = []
this.isSetRewardsPunishReason = false
wsCache.delete(CACHE_KEY.REWARDS_PUNISH_REASON)
}
},
persist: false
})
/**
* 使 store
*/
export const useRewardsPunishReasonStoreWithOut = () => {
return useRewardsPunishReasonStore(store)
}

View File

@ -0,0 +1,80 @@
import { store } from '@/store'
import { defineStore } from 'pinia'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { getSimpleUserList } from '@/api/system/user'
import type { UserVO } from '@/api/system/user'
const { wsCache } = useCache()
interface UserListState {
userList: UserVO[]
isSetUserList: boolean
}
export const useUserListStore = defineStore('admin-userList', {
state: (): UserListState => ({
userList: [],
isSetUserList: false
}),
getters: {
getUserList(): UserVO[] {
return this.userList
},
getIsSetUserList(): boolean {
return this.isSetUserList
}
},
actions: {
/**
*
*/
async setUserListAction() {
let userListInfo = wsCache.get(CACHE_KEY.USER_LIST)
if (!userListInfo) {
const userList = await getSimpleUserList()
userListInfo = {
userList
}
wsCache.set(CACHE_KEY.USER_LIST, userListInfo)
} else {
// 有缓存时,尝试更新用户列表信息
try {
const userList = await getSimpleUserList()
userListInfo = { userList }
} catch (error) {
// 更新失败时使用缓存数据
}
}
this.userList = userListInfo.userList
this.isSetUserList = true
wsCache.set(CACHE_KEY.USER_LIST, userListInfo)
},
/**
*
*/
async updateUserListAction() {
// 强制更新用户列表信息,不使用缓存
const userList = await getSimpleUserList()
this.userList = userList
this.isSetUserList = true
const userListInfo = { userList }
wsCache.set(CACHE_KEY.USER_LIST, userListInfo)
},
/**
*
*/
resetUserListState() {
this.userList = []
this.isSetUserList = false
wsCache.delete(CACHE_KEY.USER_LIST)
}
},
persist: false
})
/**
* 使 store
*/
export const useUserListStoreWithOut = () => {
return useUserListStore(store)
}

22
src/store/static.js Normal file
View File

@ -0,0 +1,22 @@
export const enumsType = {
reportStatus:[
{value:'STAGING',text:'暂存'},
{value:'AUDIT',text:'待审核'},
{value:'APPROVED',text:'审核通过'},
{value:'REVIEW_FAILED',text:'审核不通过'},
],
approveActions:[
{value:'PASS',text:'通过'},
{value:'REFUSE',text:'拒绝'},
],
trueOrFalse:[
{value:true,text:'是'},
{value:false,text:'否'},
],
integralRuleTypeEnum:[
{value:'FIXED_VALUE',text:'固定分值'},
{value:'MANUAL_VALUE',text:'手工分值'},
{value:'CALC_VALUE',text:'动态计算分值'},
{value:'CHOOSE_VALUE',text:'选择分值'},
],
}

View File

@ -158,6 +158,7 @@ import { useDesign } from '@/hooks/web/useDesign'
import { useAppStore } from '@/store/modules/app'
import { useIcon } from '@/hooks/web/useIcon'
import { usePermissionStore } from '@/store/modules/permission'
import { useDictStore } from '@/store/modules/dict'
import * as LoginApi from '@/api/login'
import * as authUtil from '@/utils/auth'
@ -256,6 +257,8 @@ const tryLogin = async () => {
const res = await LoginApi.socialLogin(type, code, state)
authUtil.setToken(res)
const dictStore = useDictStore()
await dictStore.setDictMap()
router.push({ path: redirect || '/' })
} catch (err) {}
@ -302,6 +305,8 @@ const handleLogin = async (params) => {
authUtil.removeLoginForm()
}
authUtil.setToken(res)
const dictStore = useDictStore()
await dictStore.setDictMap()
if (!redirect) {
redirect = '/'
}

View File

@ -156,6 +156,7 @@ import { useIcon } from '@/hooks/web/useIcon'
import * as authUtil from '@/utils/auth'
import { usePermissionStore } from '@/store/modules/permission'
import { useDictStore } from '@/store/modules/dict'
import * as LoginApi from '@/api/login'
import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
@ -272,6 +273,8 @@ const handleLogin = async (params: any) => {
authUtil.removeLoginForm()
}
authUtil.setToken(res)
const dictStore = useDictStore()
await dictStore.setDictMap()
if (!redirect.value) {
redirect.value = '/'
}

View File

@ -0,0 +1,481 @@
<template>
<el-drawer
v-model="dialogVisible"
title="数据详情"
size="38%"
direction="rtl"
:close-on-click-modal="true"
:close-on-press-escape="true"
:show-close="true"
>
<!-- 移除自定义 header使用默认header -->
<div class="main-container">
<!-- 基础信息 -->
<div class="group-warp">
<el-divider content-position="left">
<span class="group-name">基础信息</span>
</el-divider>
<div class="group-container">
<el-descriptions :column="2" size="default" border>
<el-descriptions-item label="标题" :span="2">
{{ detailData.title || "-" }}
</el-descriptions-item>
<el-descriptions-item label="部门">
{{ detailData.deptName || "-" }}
</el-descriptions-item>
<el-descriptions-item label="事项">
{{ detailData.reason?.name || "-" }}
</el-descriptions-item>
<el-descriptions-item label="涉事人">
{{ detailData.personName || "-" }}
</el-descriptions-item>
<el-descriptions-item label="执行人">
{{ detailData.executorName || "-" }}
</el-descriptions-item>
<el-descriptions-item label="填报时间">
{{ formatDateTime(detailData.fillingTime) || "-" }}
</el-descriptions-item>
<el-descriptions-item label="审批人">
{{ detailData.approveUserName || "-" }}
</el-descriptions-item>
<el-descriptions-item label="审批时间">
{{ formatDateTime(detailData.approveTime) || "-" }}
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusTagType(detailData.status)">
{{ getStatusText(detailData.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">
{{ detailData.remark || "-" }}
</el-descriptions-item>
<template v-if="detailData.userIntegralRecords && detailData.userIntegralRecords.length > 0">
<el-descriptions-item
v-for="(item, index) in detailData.userIntegralRecords"
:key="index"
:label="'积分'"
>
<div class="user-score-detail">
<span>{{ item.userName || '-' }}</span>
<span v-if="item.happenedDate">
{{ ' 于 ' + item.happenedDate.slice(0, 10) }}
</span>
<span class="user-score-detail-value">
<el-tag size="small" :type="item.score > 0 ? 'success' : 'danger'">
{{ item.score >= 0 ? ('+' + item.score) : item.score }}
</el-tag>
</span>
</div>
</el-descriptions-item>
</template>
</el-descriptions>
</div>
</div>
<!-- 附件区域 -->
<div class="group-warp" v-if="detailData.imageUrls && detailData.imageUrls.length > 0">
<el-divider content-position="left">
<span class="group-name">附件</span>
</el-divider>
<div class="group-container">
<div class="image-container">
<div class="image-container-item" v-for="(url, index) in detailData.imageUrls" :key="index">
<el-image
:src="getImageSrc(url)"
fit="contain"
@click="showImage(getImageSrc(url))"
>
<template #error>
<div class="image-slot">
<el-icon>
<Picture />
</el-icon>
</div>
</template>
</el-image>
</div>
</div>
</div>
</div>
<!-- 评论区域 -->
<div class="group-warp">
<el-divider content-position="left">
<span class="group-name">评论信息</span>
</el-divider>
<div class="group-container comment-container">
<div class="b-white" v-if="showCommentInput === false">
<el-button type="primary" size="small" @click="showCommentInput = true">
<el-icon>
<Plus />
</el-icon>
我要评论
</el-button>
</div>
<div class="add-comment-form" v-if="showCommentInput">
<el-input
v-model="commentText"
type="textarea"
maxlength="1000"
:autosize="{ minRows: 3, maxRows: 6 }"
placeholder="请输入评论内容"
resize="none"
/>
<div class="dialog-footer">
<el-button @click="showCommentInput = false" size="small">
<el-icon>
<CloseBold />
</el-icon>
取消
</el-button>
<el-button type="primary" @click="addComment" :loading="commentLoading" size="small">
<el-icon>
<Select />
</el-icon>
提交
</el-button>
</div>
</div>
<div class="comment-list">
<template v-if="detailData.comments && detailData.comments.length > 0">
<el-timeline>
<el-timeline-item v-for="(comment, index) in detailData.comments" :key="index" :timestamp="formatDateTime(comment.createTime)" placement="top">
<el-card>
<div class="card-header" v-if="comment.createUserName === userId">
<el-icon class="comment-delete" color="#eb333d" @click="handleDeleteComment(index)">
<Delete />
</el-icon>
</div>
<h4 class="comment-content">{{ comment.comment }}</h4>
<p class="comment-user-info">{{ comment.createUserName + ' 提交于 ' + formatDateTime(comment.createTime) }}</p>
</el-card>
</el-timeline-item>
</el-timeline>
</template>
<template v-else>
<el-empty description="暂无评论" />
</template>
</div>
</div>
</div>
</div>
<!-- 图片预览 -->
<el-image-viewer
v-if="imageViewerVisible"
:url-list="[imageViewerUrl]"
@close="imageViewerVisible = false"
/>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getMyReportDetail, saveComment, deleteComment } from '@/api/submit/common/common'
import { useUserStore } from '@/store/modules/user'
import { Picture, Plus, CloseBold, Select, Delete } from '@element-plus/icons-vue'
import { enumsType } from '@/store/static'
// Props
const props = defineProps({
visible: {
type: Boolean,
default: false
},
reportId: {
type: String,
default: ''
}
})
// Emits
const emit = defineEmits<{
'update:visible': [value: boolean]
'refresh': []
}>()
//
const dialogVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
})
const detailData = ref({
id: '',
title: '',
deptId: '',
deptName: '',
reasonId: '',
reason: { name: '' },
personName: '',
executorName: '',
comment: '',
remark: '',
imageUrls: [] as string[],
status: '',
approveUserId: '',
approveUserName: '',
approveTime: '',
fillingTime: '',
userIntegralRecords: [] as any[],
comments: [] as any[],
depId: '',
type: '',
happenedDate: '',
approverId: '',
approverName: '',
approverTime: '',
createTime: '',
createBy: '',
createUserName: ''
})
const commentText = ref('')
const showCommentInput = ref(false)
const commentLoading = ref(false)
const imageViewerVisible = ref(false)
const imageViewerUrl = ref('')
const userStore = useUserStore()
const userId = computed(() => userStore.getUserInfo?.id)
// reportId
watch(() => props.reportId, (newId) => {
if (newId && props.visible) {
loadDetailData(newId)
}
}, { immediate: true })
// visible
watch(() => props.visible, (newVisible) => {
if (newVisible && props.reportId) {
loadDetailData(props.reportId)
}
})
//
const loadDetailData = async (id: string) => {
try {
console.log('开始加载详情数据reportId:', id)
const response = await getMyReportDetail(id)
console.log('接口返回数据:', response)
if (response) {
response.comments = response.comments || []
response.userIntegralRecords = response.userIntegralRecords || []
response.imageUrls = response.imageUrls || []
detailData.value = response
console.log('赋值给detailData的数据:', detailData.value)
} else {
console.error('接口返回数据格式错误:', response)
detailData.value = {
comments: [],
userIntegralRecords: [],
imageUrls: []
}
}
} catch (error) {
console.error('获取详情失败:', error)
ElMessage.error('获取详情失败')
}
}
// URL
const getImageSrc = (url: string) => {
if (!url) return ''
if (url.startsWith('http://') || url.startsWith('https://')) {
return url
}
if (url.startsWith('/')) {
return window.location.origin + url
}
return window.location.origin + '/' + url
}
//
const showImage = (url: string) => {
imageViewerUrl.value = getImageSrc(url)
imageViewerVisible.value = true
}
//
const formatDateTime = (dateTime: any) => {
if (!dateTime) return ''
const date = new Date(dateTime)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
//
const getStatusTagType = (status: any) => {
const statusMap: Record<string, string> = {
'STAGING': 'warning',
'AUDIT': 'primary',
'APPROVED': 'success',
'REVIEW_FAILED': 'danger'
}
return statusMap[status] || 'info'
}
//
const getStatusText = (status: any) => {
const statusItem = enumsType.reportStatus.find(item => item.value === status)
return statusItem ? statusItem.text : '-'
}
//
const addComment = async () => {
if (!commentText.value.trim()) {
ElMessage.warning('请输入评论内容')
return
}
try {
commentLoading.value = true
await saveComment(props.reportId, {
comment: commentText.value.trim()
})
ElMessage.success('评论成功')
commentText.value = ''
showCommentInput.value = false
await loadDetailData(props.reportId)
} catch (error) {
console.error('评论失败:', error)
ElMessage.error('评论失败')
} finally {
commentLoading.value = false
}
}
//
const handleDeleteComment = async (index: number) => {
try {
const comment = detailData.value.comments[index]
if (!comment) return
await ElMessageBox.confirm('确定删除该评论吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await deleteComment(comment.id)
ElMessage.success('删除成功')
await loadDetailData(props.reportId)
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
}
}
</script>
<style scoped>
.main-container {
padding: 0px 40px;
}
.group-name {
color: #A8ABB2
}
.group-warp {
padding-bottom: 10px;
}
.group-warp:last-child {
padding-bottom: 0px;
}
.group-container {
padding: 10px 15px;
}
.group-container .user-score-detail-value {
display: inline-block;
padding-left: 10px;
}
.group-container .image-container {
display: flex;
justify-content: flex-start;
align-items: center;
}
.group-container .image-container-item {
display: flex;
justify-content: center;
width: 110px;
height: 110px;
margin-right: 15px;
cursor: pointer;
}
.group-container .image-container-item .image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100px;
height: 100px;
color: var(--el-text-color-secondary);
font-size: 30px;
}
.group-container .image-container-item .image-slot .el-icon {
font-size: 30px;
}
.comment-container .b-white {
display: flex;
flex-direction: row-reverse;
}
.comment-container .comment-list {
margin-top: 10px;
max-height: 500px;
overflow-y: auto;
}
.comment-container .comment-list .card-header {
display: flex;
justify-content: flex-end;
}
.comment-container .comment-list .comment-delete {
cursor: pointer;
}
.comment-container .comment-list .comment-content {
line-height: 21px;
font-size: 14px;
}
.comment-container .comment-list .comment-user-info {
margin-top: 10px;
color: #A8ABB2;
font-size: 14px;
}
.comment-container .comment-list .el-card {
margin-bottom: 2px;
}
.comment-container .add-comment-form .dialog-footer {
display: flex;
justify-content: flex-end;
}
:deep(.el-descriptions__label){
width: 80px;
}
:deep(.el-descriptions__content){
min-width: 200px;
}
</style>

View File

@ -0,0 +1,512 @@
<template>
<div class="app-container">
<!-- 搜索区域 -->
<div class="filter-container">
<el-card shadow="never">
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="标题">
<el-input
v-model="queryParams.title"
placeholder="请输入标题"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="部门">
<el-select
v-model="queryParams.deptId"
placeholder="请选择部门"
clearable
style="width: 200px"
>
<el-option
v-for="dept in deptOptions"
:key="dept.id"
:label="dept.name"
:value="dept.id"
/>
</el-select>
</el-form-item>
<el-form-item label="涉事人">
<el-input
v-model="queryParams.personName"
placeholder="请输入涉事人"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="事项">
<el-select
v-model="queryParams.reasonId"
placeholder="请选择事项"
clearable
style="width: 200px"
>
<el-option
v-for="item in reportTypeOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="填报时间">
<el-date-picker
v-model="createTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery" :loading="loading">
查询
</el-button>
<el-button @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<!-- 表格区域 -->
<div class="table-container">
<el-card shadow="never">
<!-- 表格操作区域 -->
<div class="table-header">
<el-button
@click="handleExport"
:loading="exportLoading"
type="success"
plain
>
<el-icon class="mr5"><Download /></el-icon>
导出
</el-button>
</div>
<!-- 表格 -->
<el-table
v-loading="loading"
:data="reportList"
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
stripe
style="width: 100%"
>
<el-table-column
label="标题"
prop="title"
min-width="200"
align="center"
show-overflow-tooltip
/>
<el-table-column
label="部门"
prop="deptName"
min-width="120"
align="center"
/>
<el-table-column
label="涉事人"
prop="personName"
min-width="120"
align="center"
/>
<el-table-column label="事项" min-width="120" align="center">
<template #default="{ row }">
{{ row.reportTypeName || row.reason?.name || '-' }}
</template>
</el-table-column>
<el-table-column label="分值" min-width="80" align="center">
<template #default="scope">
{{ scope.row.userIntegralRecords?.[0]?.score || '-' }}
</template>
</el-table-column>
<el-table-column
label="备注"
prop="remark"
min-width="250"
align="center"
show-overflow-tooltip
>
<template #default="scope">
{{ scope.row.remark || '-' }}
</template>
</el-table-column>
<el-table-column label="填报时间" min-width="160" align="center">
<template #default="scope">
{{ formatDisplayTime(scope.row.fillingTime) }}
</template>
</el-table-column>
<el-table-column label="状态" min-width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
label="操作"
min-width="120"
align="center"
fixed="right"
>
<template #default="scope">
<span class="table-opera-item" @click="handleView(scope.row)" :title="'查看'">
<el-icon class="table-opera-icon"><View /></el-icon>
</span>
</template>
</el-table-column>
</el-table>
<!-- 空状态 -->
<template v-if="!loading && reportList.length === 0">
<el-empty description="暂无公司通报数据" :image-size="200" />
</template>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
</div>
<!-- 查看详情组件 -->
<DetailComponent
v-model:visible="detailDialogVisible"
:report-id="selectedReportId"
@refresh="getList"
/>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { View, Download } from '@element-plus/icons-vue'
import { useRoute } from 'vue-router'
import Pagination from '@/components/Pagination/index.vue'
import DetailComponent from '../common/detail.vue'
import { useDeptStore } from '@/store/modules/dept'
import { useRewardsPunishReasonStore } from '@/store/modules/rewardsPunishReason'
import { enumsType } from '@/store/static'
import { getMyReportPage, exportMyReport } from '@/api/submit/myReporting'
import type { MyReportDTO, QueryParams } from '@/api/submit/myReporting/types'
// Store
const deptStore = useDeptStore()
const rewardsPunishReasonStore = useRewardsPunishReasonStore()
// Router
const route = useRoute()
//
const deptOptions = computed(() => deptStore.getDeptList)
const reportTypeOptions = computed(() => rewardsPunishReasonStore.getRewardsPunishReasonList)
//
const reportList = ref<MyReportDTO[]>([])
const total = ref<number>(0)
const loading = ref<boolean>(true)
const exportLoading = ref<boolean>(false)
const createTimeRange = ref<string[] | null>(null)
const detailDialogVisible = ref<boolean>(false)
const selectedReportId = ref<string>('')
//
const queryParams = reactive<QueryParams>({
pageNo: 1,
pageSize: 10,
title: '',
deptId: '',
personName: '',
reasonId: '',
status: '',
startTime: undefined,
endTime: undefined,
isOwnReport: false,
isOwnApprove: false
})
//
const getStatusTagType = (status: string | number) => {
const typeMap: Record<string | number, string> = {
'STAGING': 'warning', 1: 'warning',
'AUDIT': 'primary', 2: 'primary',
'APPROVED': 'success', 3: 'success',
'REVIEW_FAILED': 'danger', 4: 'danger'
}
return typeMap[status] || 'info'
}
const getStatusText = (status: string | number) => {
const statusItem = enumsType.reportStatus.find(item => item.value === status)
return statusItem ? statusItem.text : '未知状态'
}
const formatDateTime = (dateTime: string | undefined) => {
if (!dateTime) return ''
const date = new Date(dateTime)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
const formatDisplayTime = (time: string | undefined) => {
return time ? formatDateTime(time) : '-'
}
//
onMounted(async () => {
await initData()
})
//
const initData = async () => {
try {
//
await Promise.all([
!deptStore.getIsSetDept ? deptStore.setDeptInfoAction() : Promise.resolve(),
!rewardsPunishReasonStore.getIsSetRewardsPunishReason ? rewardsPunishReasonStore.setRewardsPunishReasonAction() : Promise.resolve()
])
await getList()
} catch (error) {
console.error('初始化数据失败:', error)
ElMessage.error('初始化数据失败')
}
}
//
const getList = async () => {
loading.value = true
queryParams.status = 'APPROVED'
try {
const response = await getMyReportPage(queryParams)
reportList.value = response.list || []
total.value = response.total || 0
} catch (error) {
console.error('获取公司通报列表失败:', error)
ElMessage.error('获取公司通报列表失败')
} finally {
loading.value = false
}
}
//
const handleQuery = () => {
queryParams.pageNo = 1
//
if (createTimeRange.value && createTimeRange.value.length === 2) {
queryParams.startTime = createTimeRange.value[0] + 'T00:00:00'
queryParams.endTime = createTimeRange.value[1] + 'T23:59:59'
} else {
queryParams.startTime = undefined
queryParams.endTime = undefined
}
getList()
}
//
const resetQuery = () => {
Object.assign(queryParams, {
pageNo: 1,
pageSize: 10,
title: '',
deptId: '',
personName: '',
reasonId: '',
status: '',
startTime: undefined,
endTime: undefined,
isOwnReport: false,
isOwnApprove: false
})
createTimeRange.value = null
getList()
}
//
const handleView = (row: MyReportDTO) => {
selectedReportId.value = row.id
detailDialogVisible.value = true
}
//
watch(
[() => route.path, () => route.fullPath],
() => {
detailDialogVisible.value = false
},
{ immediate: true }
)
//
const handleExport = async () => {
exportLoading.value = true
try {
const response = await exportMyReport(queryParams)
//
if (response instanceof Blob) {
// Blob URL
const blob = new Blob([response], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
})
const url = window.URL.createObjectURL(blob)
//
const link = document.createElement('a')
link.href = url
// 使
const filename = getFilenameFromResponse(response) || `公司通报数据.xlsx`
link.download = filename
//
document.body.appendChild(link)
link.click()
//
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} else {
ElMessage.success('导出请求已发送')
}
} catch (error) {
console.error('导出失败:', error)
ElMessage.error('导出失败')
} finally {
exportLoading.value = false
}
}
//
const getFilenameFromResponse = (response: any) => {
try {
// Responseheaders
if (response.headers) {
const contentDisposition = response.headers.get('content-disposition')
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?(.+?)\"?/i)
if (filenameMatch && filenameMatch[1]) {
return decodeURIComponent(filenameMatch[1])
}
}
}
} catch (e) {
console.warn('获取文件名失败:', e)
}
return null
}
</script>
<style scoped>
.app-container {
padding: 20px;
}
.filter-container {
margin-bottom: 20px;
}
.table-container {
margin-top: 20px;
}
.table-header {
margin-bottom: 15px;
display: flex;
justify-content: flex-start;
align-items: center;
}
:deep(.el-card) {
border: 1px solid #e4e7ed;
border-radius: 4px;
}
:deep(.el-card__body) {
padding: 20px;
}
:deep(.el-table) {
border: 1px solid #e4e7ed;
border-radius: 4px;
}
:deep(.el-table th) {
background-color: #f5f7fa;
font-weight: 600;
}
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
background-color: #fafafa;
}
:deep(.el-form-item__label) {
font-weight: 500;
color: #606266;
}
/* 操作列样式 */
.table-opera-item {
cursor: pointer;
color: #409eff;
transition: color 0.3s;
display: inline-block;
}
.table-opera-item:hover {
color: #66b1ff;
}
.table-opera-icon {
font-size: 18px;
vertical-align: middle;
}
.mr5 {
margin-right: 5px;
}
.ml10 {
margin-left: 10px;
}
.delete-icon {
color: #eb333d;
}
.delete-icon:hover {
color: #f56c6c;
}
.loading-icon {
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>

View File

@ -0,0 +1,759 @@
<template>
<el-dialog
:title="optType === 'approve' ? '审批' : 'Dialog'"
:model-value="dialogVisible"
:close-on-click-modal="false"
width="600px"
@update:model-value="handleDialogUpdate"
>
<el-form size="default" @submit.prevent :model="entity" ref="formRef" :rules="rules" class="member" label-position="top">
<!-- 基础信息区域 -->
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="标题" class="labelTop">
<el-input
size="default"
class="fw"
v-model="entity.title"
disabled
placeholder="-"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="部门" class="labelTop">
<el-input
v-model="entity.deptName"
disabled
placeholder="-"
:suffix-icon="Check"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="涉事人" class="labelTop">
<el-input
v-model="entity.personName"
disabled
placeholder="-"
:suffix-icon="Check"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="事项" class="labelTop">
<el-input
v-model="entity.reason.name"
disabled
placeholder="-"
:suffix-icon="Check"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" class="labelTop">
<el-input
type="textarea"
maxlength="1000"
:autosize="{ minRows: 2, maxRows: 4 }"
class="fw"
v-model="entity.remark"
disabled
placeholder="请输入"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 分值设置区域 -->
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="分值" class="labelTop new-label" required>
<div class="score-instruction">
<el-text size="small" type="info">请为相关人员进行分值设置-10到10分支持0.5分间隔</el-text>
</div>
<div v-for="(item, index) in entity.userIntegralRecords" :key="index" class="res-item">
<el-select
v-model="item.userName"
filterable
placeholder="请选择用户"
clearable
:disabled="index === 0"
class="user-select"
@change="handleUserChange(index, $event)"
>
<el-option
v-for="user in persons"
:key="user.id"
:label="user.name"
:value="user.id"
/>
</el-select>
<!-- 带加减按钮的分值输入 -->
<div class="score-control">
<el-button
@click="decreaseScore(index)"
:disabled="item.score <= -10"
class="score-btn decrease-btn"
size="small"
>
-
</el-button>
<el-input
size="default"
class="score-input"
v-model="item.score"
type="number"
:min="-10"
:max="10"
:step="0.5"
@change="handleScoreChange(item, index)"
@blur="handleScoreBlur(item, index, true)"
/>
<el-button
@click="increaseScore(index)"
:disabled="item.score >= 10"
class="score-btn increase-btn"
size="small"
>
+
</el-button>
</div>
<!-- 操作按钮 -->
<div class="action-icons">
<el-icon
v-if="index === 0"
@click="addUserScore"
class="action-icon add-icon"
title="添加人员"
>
<CirclePlus />
</el-icon>
<el-icon
v-else
@click="removeUserScore(index)"
class="action-icon remove-icon"
title="移除"
>
<Remove />
</el-icon>
</div>
</div>
</el-form-item>
</el-col>
</el-row>
<!-- 审批结论 -->
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="审批结论" class="labelTop" prop="action">
<el-select
v-model="entity.action"
filterable
placeholder="请选择"
clearable
class="action-select"
>
<el-option
v-for="item in approveActions"
:key="item.code"
:label="item.name"
:value="item.code"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 审批意见 -->
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="审批意见" class="labelTop">
<el-input
type="textarea"
maxlength="500"
:autosize="{ minRows: 2, maxRows: 4 }"
class="fw"
v-model="entity.comment"
placeholder="请输入审批意见"
clearable
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 底部按钮 -->
<template #footer>
<span class="dialog-footer">
<el-button @click="cancel"></el-button>
<el-button type="primary" @click="submit" :loading="submitting">
确定
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { CirclePlus, Remove, Check } from '@element-plus/icons-vue'
import { useUserListStore } from '@/store/modules/userList'
import { useDeptStore } from '@/store/modules/dept'
import { useRewardsPunishReasonStore } from '@/store/modules/rewardsPunishReason'
import type { FormInstance, FormRules } from 'element-plus'
import type { MyReportDTO } from '@/api/submit/myReporting/types'
import { approveMyReport } from '@/api/submit/myApprove'
// Props
const props = defineProps<{
dialogVisible: boolean
optType: 'approve' | 'view'
formData: MyReportDTO
}>()
// Emits
const emit = defineEmits<{
'update:dialogVisible': [value: boolean]
success: []
}>()
// Store
const userListStore = useUserListStore()
const deptStore = useDeptStore()
const rewardsPunishReasonStore = useRewardsPunishReasonStore()
// Ref
const formRef = ref<FormInstance>()
const submitting = ref(false)
//
const entity = reactive({
id: '',
title: '',
deptId: '',
deptName: '',
personId: '',
personName: '',
reason: {
id: '',
name: ''
},
userIntegralRecords: [{
userId: '',
userName: '',
score: 0
}],
remark: '',
comment: '',
action: '',
status: 'AUDIT'
})
//
const rules = reactive<FormRules>({
action: [
{ required: true, message: '请选择审批结论', trigger: 'blur' }
]
})
//
const persons = computed(() => {
return userListStore.getUserList?.map(user => ({
id: user.id,
name: user.nickname || user.realName || user.username
})) || []
})
const deptOptions = computed(() => {
return deptStore.getDeptList || []
})
const reasonOptions = computed(() => {
return rewardsPunishReasonStore.getRewardsPunishReasonList || []
})
const approveActions = computed(() => [
{ code: 'PASS', name: '通过' },
{ code: 'REFUSE', name: '驳回' }
])
//
const increaseScore = (index: number) => {
const currentScore = entity.userIntegralRecords[index].score
const newScore = Math.min(currentScore + 0.5, 10)
entity.userIntegralRecords[index].score = newScore
validateScore(entity.userIntegralRecords[index], index, true)
}
//
const decreaseScore = (index: number) => {
const currentScore = entity.userIntegralRecords[index].score
const newScore = Math.max(currentScore - 0.5, -10)
entity.userIntegralRecords[index].score = newScore
validateScore(entity.userIntegralRecords[index], index, true)
}
//
const validateScore = (item: any, index: number, showMessage: boolean = false) => {
let score = item.score
//
if (typeof score !== 'number') {
score = parseFloat(score)
if (isNaN(score)) score = 0
item.score = score
}
//
if (score < -10) {
item.score = -10
if (showMessage) {
ElMessage.warning('分值不能小于-10')
}
return
}
if (score > 10) {
item.score = 10
if (showMessage) {
ElMessage.warning('分值不能大于10')
}
return
}
// 0.5
if (score !== 0) {
const remainder = (score * 10) % 5
if (Math.abs(remainder) > 0.01 && Math.abs(remainder) < 4.99) {
// 0.5
const correctedScore = Math.round(score * 2) / 2
item.score = correctedScore
if (showMessage) {
ElMessage.warning({
message: `分值必须是0.5的倍数,已自动调整为 ${correctedScore}`,
duration: 2000
})
}
}
}
}
//
const handleDialogUpdate = (visible: boolean) => {
emit('update:dialogVisible', visible)
}
//
const addUserScore = () => {
entity.userIntegralRecords.push({
userId: '',
userName: '',
score: 0
})
}
//
const removeUserScore = (index: number) => {
if (entity.userIntegralRecords.length > 1) {
entity.userIntegralRecords.splice(index, 1)
}
}
//
const handleUserChange = (index: number, userId: string) => {
if (userId) {
const user = persons.value.find(u => u.id === userId)
if (user) {
entity.userIntegralRecords[index].userName = user.name
entity.userIntegralRecords[index].userId = userId
}
} else {
entity.userIntegralRecords[index].userName = ''
entity.userIntegralRecords[index].userId = ''
}
}
//
const handleScoreChange = (item: any, index: number) => {
validateScore(item, index, false)
}
//
const handleScoreBlur = (item: any, index: number, showMessage: boolean = false) => {
validateScore(item, index, showMessage)
}
//
const cancel = () => {
emit('update:dialogVisible', false)
}
//
const submit = async () => {
if (!formRef.value) return
submitting.value = true
try {
await formRef.value.validate(async (valid) => {
if (valid) {
//
const invalidRecords = entity.userIntegralRecords.filter(item => !item.userId)
if (invalidRecords.length > 0) {
ElMessage.error('请为所有分值项选择用户')
submitting.value = false
return
}
//
const userIds = entity.userIntegralRecords.map(item => item.userId)
const uniqueUserIds = [...new Set(userIds)]
if (userIds.length !== uniqueUserIds.length) {
ElMessage.error('请勿选择重复的用户')
submitting.value = false
return
}
// 0.5
const invalidScores = entity.userIntegralRecords.filter((item: any) => {
if (item.score === 0 || item.score === 0.0) return false
const remainder = Math.abs((item.score * 10) % 5)
return remainder > 0.01 && remainder < 4.99
})
if (invalidScores.length > 0) {
ElMessage.error('分值必须是0.5的倍数')
submitting.value = false
return
}
try {
//
const params = {
action: entity.action,
comment: entity.comment || '',
userIntegralRecords: entity.userIntegralRecords.map(item => ({
userId: item.userId,
userName: item.userName,
score: item.score
}))
}
//
await approveMyReport({ reportId: entity.id, ...params })
ElMessage.success('审批成功')
emit('success')
emit('update:dialogVisible', false)
} catch (error: any) {
console.error('审批失败:', error)
ElMessage.error(error.msg || '审批失败,请重试')
}
}
})
} finally {
submitting.value = false
}
}
// props
watch(
() => props.formData,
(newVal) => {
if (newVal && newVal.id) {
//
const deptName = newVal.deptName || (newVal as any).depthName || ''
const deptId = newVal.deptId || (newVal as any).depthId || ''
// 1.
Object.assign(entity, {
id: newVal.id || '',
title: newVal.title || '',
deptId: deptId,
deptName: deptName,
personId: newVal.personId || '',
personName: newVal.personName || '',
remark: newVal.remark || '',
comment: '',
action: '',
status: newVal.status || 'AUDIT'
})
// 2.
let reasonName = ''
let reasonId = ''
//
if (newVal.reason && typeof newVal.reason === 'object') {
reasonId = newVal.reason.id || ''
reasonName = newVal.reason.name || ''
} else if ((newVal as any).reasonName) {
// reasonName
reasonName = (newVal as any).reasonName
reasonId = newVal.reasonId || ''
} else if (newVal.reasonId) {
reasonId = newVal.reasonId
const reasonOption = reasonOptions.value.find(r => r.id === reasonId)
reasonName = reasonOption?.name || ''
}
entity.reason = {
id: reasonId,
name: reasonName || '-'
}
// 3.
if (newVal.userIntegralRecords && Array.isArray(newVal.userIntegralRecords) && newVal.userIntegralRecords.length > 0) {
// 使
entity.userIntegralRecords = newVal.userIntegralRecords.map(item => ({
userId: item.userId || '',
userName: item.userName || '',
score: item.score || 0
}))
} else {
//
entity.userIntegralRecords = [{
userId: newVal.personId || '',
userName: newVal.personName || '',
score: 0
}]
}
}
},
{ deep: true, immediate: true }
)
//
watch(
() => props.dialogVisible,
(newVal) => {
if (!newVal) {
//
setTimeout(() => {
Object.assign(entity, {
id: '',
title: '',
deptId: '',
deptName: '',
personId: '',
personName: '',
reason: { id: '', name: '' },
userIntegralRecords: [{ userId: '', userName: '', score: 0 }],
remark: '',
comment: '',
action: '',
status: 'AUDIT'
})
}, 300)
}
}
)
//
onMounted(async () => {
//
if (!deptStore.getIsSetDept) {
await deptStore.setDeptInfoAction()
}
//
if (!userListStore.getIsSetUserList) {
await userListStore.setUserListAction()
}
//
if (!rewardsPunishReasonStore.getIsSetRewardsPunishReason) {
await rewardsPunishReasonStore.setRewardsPunishReasonAction()
}
})
</script>
<style scoped>
.member {
width: 100%;
margin-bottom: 20px;
}
.labelTop {
margin-bottom: 16px;
}
.labelTop :deep(.el-form-item__label) {
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.fw {
width: 100%;
}
.score-instruction {
margin-bottom: 12px;
}
.new-label {
.res-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e4e7ed;
&:last-child {
margin-bottom: 0;
}
.user-select {
flex: 1;
min-width: 200px;
}
/* 带加减按钮的分值输入 */
.score-control {
flex: 0 0 160px;
display: flex;
align-items: center;
gap: 4px;
}
.score-btn {
width: 32px;
height: 32px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: bold;
border-radius: 4px;
}
.decrease-btn {
color: #f56c6c;
border-color: #fde2e2;
background-color: #fef0f0;
}
.decrease-btn:hover {
background-color: #fde2e2;
border-color: #f56c6c;
}
.increase-btn {
color: #67c23a;
border-color: #e1f3d8;
background-color: #f0f9eb;
}
.increase-btn:hover {
background-color: #e1f3d8;
border-color: #67c23a;
}
.score-input {
flex: 1;
text-align: center;
}
.score-input :deep(.el-input__inner) {
text-align: center;
font-weight: 500;
}
/* 操作图标样式 */
.action-icons {
flex: 0 0 40px;
display: flex;
justify-content: center;
align-items: center;
}
.action-icon {
width: 28px;
height: 28px;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
font-size: 18px;
}
.add-icon {
color: #67c23a;
background-color: #f0f9eb;
border: 1px solid #e1f3d8;
}
.add-icon:hover {
background-color: #e1f3d8;
transform: scale(1.1);
box-shadow: 0 2px 4px rgba(103, 194, 58, 0.2);
}
.remove-icon {
color: #f56c6c;
background-color: #fef0f0;
border: 1px solid #fde2e2;
}
.remove-icon:hover {
background-color: #fde2e2;
transform: scale(1.1);
box-shadow: 0 2px 4px rgba(245, 108, 108, 0.2);
}
}
}
.action-select {
width: 100%;
}
.status-display {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #eee;
text-align: center;
}
.status-display .el-tag {
font-size: 14px;
padding: 8px 16px;
}
:deep(.el-input.is-disabled .el-input__wrapper) {
background-color: #f5f7fa;
cursor: not-allowed;
}
:deep(.el-textarea.is-disabled .el-textarea__inner) {
background-color: #f5f7fa;
cursor: not-allowed;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>

View File

@ -0,0 +1,560 @@
<template>
<div class="app-container">
<!-- 筛选区域 -->
<div class="filter-container">
<el-card shadow="never">
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="标题">
<el-input
v-model="queryParams.title"
placeholder="请输入"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="部门">
<el-select
v-model="queryParams.deptId"
placeholder="请选择"
clearable
style="width: 200px"
>
<el-option
v-for="dept in deptOptions"
:key="dept.id"
:label="dept.name"
:value="dept.id"
/>
</el-select>
</el-form-item>
<el-form-item label="涉事人">
<el-input
v-model="queryParams.personName"
placeholder="请输入"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="事项">
<el-select
v-model="queryParams.reasonId"
placeholder="请选择"
clearable
style="width: 200px"
>
<el-option
v-for="item in reportTypeOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select
v-model="queryParams.status"
placeholder="请选择"
clearable
style="width: 200px"
@change="handleStatusChange"
>
<el-option
v-for="item in reportStatus"
:key="item.value"
:label="item.text"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="填报时间">
<el-date-picker
v-model="createTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery" :loading="loading">
查询
</el-button>
<el-button @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<!-- 表格区域 -->
<div class="table-container">
<el-card shadow="never">
<div class="table-header">
<el-button @click="handleExport" :loading="exportLoading">
导出
</el-button>
</div>
<el-table
v-loading="loading"
:data="reportList"
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
stripe
style="width: 100%"
>
<template #empty>
<el-empty description="暂无审批数据" :image-size="200">
<el-button type="primary" @click="handleQuery"></el-button>
</el-empty>
</template>
<el-table-column
label="标题"
prop="title"
min-width="200"
align="center"
show-overflow-tooltip
/>
<el-table-column
label="部门"
prop="deptName"
min-width="120"
align="center"
/>
<el-table-column
label="涉事人"
prop="personName"
min-width="120"
align="center"
/>
<el-table-column label="事项" min-width="120" align="center">
<template #default="{ row }">
{{ row.reportTypeName || row.reason?.name || '-' }}
</template>
</el-table-column>
<el-table-column
label="备注"
prop="remark"
min-width="250"
align="center"
show-overflow-tooltip
>
<template #default="scope">
{{ scope.row.remark || '-' }}
</template>
</el-table-column>
<el-table-column
label="执行人"
prop="executorName"
min-width="120"
align="center"
/>
<el-table-column label="填报时间" min-width="160" align="center">
<template #default="scope">
{{ formatDisplayTime(scope.row.fillingTime) }}
</template>
</el-table-column>
<el-table-column label="状态" min-width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
label="审批人"
prop="approveUserName"
min-width="120"
align="center"
/>
<el-table-column
label="审批时间"
prop="approveTime"
min-width="160"
align="center"
/>
<el-table-column
label="操作"
min-width="120"
align="center"
fixed="right"
>
<template #default="scope">
<span
class="table-opera-item"
@click="handleApprove(scope.row)"
v-if="scope.row.status === 'AUDIT' || scope.row.status === 2 || scope.row.status === '待审批'"
:title="'审批'"
style="z-index: 10; position: relative;"
>
<el-icon class="table-opera-icon"><Notification /></el-icon>
</span>
<span
class="table-opera-item ml10"
@click="handleView(scope.row)"
:title="'查看'"
style="z-index: 10; position: relative;"
>
<el-icon class="table-opera-icon"><View /></el-icon>
</span>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
</div>
<!-- 引入审批弹窗组件 -->
<Approve
:dialog-visible="dialogVisible"
:opt-type="optType"
:form-data="formData"
@update:dialog-visible="handleDialogVisibleUpdate"
@success="handleApproveSuccess"
/>
<!-- 查看详情组件 -->
<DetailComponent
v-model:visible="detailDialogVisible"
:report-id="selectedReportId"
@refresh="getList"
/>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { View, Notification } from '@element-plus/icons-vue'
import { useRoute } from 'vue-router'
import DetailComponent from '../common/detail.vue'
import { useRewardsPunishReasonStore } from '@/store/modules/rewardsPunishReason'
import { useDeptStore } from '@/store/modules/dept'
import Pagination from '@/components/Pagination/index.vue'
import Approve from './approve.vue'
import type { MyReportDTO, QueryParams } from '@/api/submit/myReporting/types'
import { getApproveList, exportMyReport } from '@/api/submit/myApprove'
import { enumsType } from '@/store/static'
// Store
const rewardsPunishReasonStore = useRewardsPunishReasonStore()
const deptStore = useDeptStore()
// Router
const route = useRoute()
//
const deptOptions = computed(() => deptStore.getDeptList)
const reportTypeOptions = computed(() => rewardsPunishReasonStore.getRewardsPunishReasonList)
const reportStatus = computed(() => {
return enumsType.reportStatus.filter(item => item.value !== 'STAGING' && item.value !== 1)
})
//
const reportList = ref<MyReportDTO[]>([])
const total = ref<number>(0)
const loading = ref<boolean>(true)
const exportLoading = ref<boolean>(false)
const createTimeRange = ref<string[] | null>(null)
const dialogVisible = ref<boolean>(false)
const detailDialogVisible = ref<boolean>(false)
const selectedReportId = ref<string>('')
const optType = ref<'approve'>('approve')
const formData = ref<MyReportDTO>({
id: '',
title: '',
deptId: '',
deptName: '',
reasonId: '',
reportTypeName: '',
personName: '',
content: '',
remark: '',
imageUrls: [],
status: 'AUDIT',
approverId: '',
approverName: '',
approveTime: '',
approveComment: '',
createBy: '',
createTime: '',
userIntegralRecords: [],
comments: []
})
//
const queryParams = reactive<QueryParams>({
pageNo: 1,
pageSize: 10,
title: '',
deptId: '',
personName: '',
reasonId: '',
status: 'AUDIT',
startTime: undefined,
endTime: undefined
})
//
const handleDialogVisibleUpdate = (visible: boolean) => {
dialogVisible.value = visible
}
//
const getStatusTagType = (status: string | number) => {
const typeMap: Record<string | number, string> = {
AUDIT: 'primary',
2: 'primary',
APPROVED: 'success',
3: 'success',
REVIEW_FAILED: 'danger',
4: 'danger'
}
return typeMap[status] || 'info'
}
const getStatusText = (status: string | number) => {
const statusItem = enumsType.reportStatus.find(item => item.value === status)
return statusItem ? statusItem.text : '未知状态'
}
const formatDateTime = (dateTime: string | undefined) => {
if (!dateTime) return ''
const date = new Date(dateTime)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
const formatDisplayTime = (time: string | undefined) => {
return time ? formatDateTime(time) : '-'
}
//
const handleStatusChange = () => {
queryParams.isOwnApprove = queryParams.status === 'AUDIT' || queryParams.status === 2 ? false : true
}
//
onMounted(async () => {
await initData()
})
//
const initData = async () => {
try {
//
await Promise.all([
!rewardsPunishReasonStore.getIsSetRewardsPunishReason
? rewardsPunishReasonStore.setRewardsPunishReasonAction()
: Promise.resolve(),
!deptStore.getIsSetDept
? deptStore.setDeptInfoAction()
: Promise.resolve()
])
await getList()
} catch (error) {
console.error('初始化数据失败:', error)
ElMessage.error('初始化数据失败')
}
}
//
const handleApproveSuccess = () => {
getList()
}
//
const getList = async () => {
loading.value = true
try {
const response = await getApproveList(queryParams)
reportList.value = response.list || []
total.value = response.total || 0
} catch (error) {
console.error('获取列表失败:', error)
ElMessage.error('获取列表失败')
} finally {
loading.value = false
}
}
//
const handleQuery = () => {
queryParams.pageNo = 1
//
if (createTimeRange.value && createTimeRange.value.length === 2) {
queryParams.startTime = `${createTimeRange.value[0]}T00:00:00`
queryParams.endTime = `${createTimeRange.value[1]}T23:59:59`
} else {
queryParams.startTime = undefined
queryParams.endTime = undefined
}
getList()
}
//
const resetQuery = () => {
Object.assign(queryParams, {
pageNo: 1,
pageSize: 10,
title: '',
deptId: '',
personName: '',
reasonId: '',
status: 'AUDIT',
startTime: undefined,
endTime: undefined
})
createTimeRange.value = null
getList()
}
// -
const handleApprove = (row: MyReportDTO) => {
optType.value = 'approve'
//
formData.value = row
console.log('传递给弹窗的数据:', formData.value)
dialogVisible.value = true
}
//
const handleView = (row: MyReportDTO) => {
selectedReportId.value = row.id
detailDialogVisible.value = true
}
//
watch(
[() => route.path, () => route.fullPath],
() => {
detailDialogVisible.value = false
},
{ immediate: true }
)
//
const handleExport = async () => {
exportLoading.value = true
try {
const response = await exportMyReport(queryParams)
//
if (response instanceof Blob) {
const blob = new Blob([response], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
})
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
const filename = '审批数据.xlsx'
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} else {
ElMessage.success('导出请求已发送')
}
} catch (error) {
console.error('导出失败:', error)
ElMessage.error('导出失败')
} finally {
exportLoading.value = false
}
}
</script>
<style scoped>
.app-container {
padding: 20px;
}
.filter-container {
margin-bottom: 20px;
}
.table-container {
margin-top: 20px;
}
.table-header {
margin-bottom: 15px;
display: flex;
justify-content: flex-start;
align-items: center;
gap: 10px;
}
.table-opera-item {
cursor: pointer;
color: #409eff;
transition: color 0.3s;
display: inline-block;
}
.table-opera-item:hover {
color: #66b1ff;
}
.table-opera-icon {
font-size: 18px;
vertical-align: middle;
}
.ml10 {
margin-left: 10px;
}
/* 使用 :deep() 简化样式 */
:deep(.el-card) {
border: 1px solid #e4e7ed;
border-radius: 4px;
}
:deep(.el-card__body) {
padding: 20px;
}
:deep(.el-table) {
border: 1px solid #e4e7ed;
border-radius: 4px;
}
:deep(.el-table th) {
background-color: #f5f7fa;
font-weight: 600;
}
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
background-color: #fafafa;
}
:deep(.el-form-item__label) {
font-weight: 500;
color: #606266;
}
</style>

View File

@ -0,0 +1,478 @@
<template>
<el-dialog
:title="dialogTitle"
v-model="visible"
width="800px"
append-to-body
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="标题:" prop="title">
<el-input
v-model="form.title"
placeholder="请输入标题"
style="width: 100%"
maxlength="50"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="部门:" prop="deptId">
<el-select
v-model="form.deptId"
placeholder="请选择部门"
style="width: 100%"
filterable
@change="handleDeptChange"
>
<el-option
v-for="dept in deptOptions"
:key="dept.id"
:label="dept.name"
:value="String(dept.id)"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="涉事人:" prop="personId">
<el-select
v-model="form.personId"
placeholder="请选择涉事人"
style="width: 100%"
filterable
@change="handlePersonChange"
>
<el-option
v-for="item in personOptions"
:key="item.id"
:label="item.nickname"
:value="String(item.id)"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="事项:" prop="reasonId">
<el-select
v-model="form.reasonId"
placeholder="请选择事项"
style="width: 100%"
@change="handleReasonChange"
>
<el-option
v-for="item in reportTypeOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="分值:" prop="score">
<el-input-number
v-if="showScoreInput"
v-model="form.userIntegralRecords[0].score"
:min="-10"
:max="10"
:precision="1"
:step="0.5"
placeholder="请输入分值"
style="width: 100%"
:disabled="scoreDisabled"
/>
<el-select
v-if="showScoreSelect"
v-model="form.userIntegralRecords[0].score"
placeholder="请选择分值"
style="width: 100%"
:disabled="scoreDisabled"
>
<el-option
v-for="score in ruleScore.scores"
:key="score"
:label="score"
:value="score"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="图片:">
<UploadImgs
v-model="form.imageUrls"
:limit="5"
:file-size="10"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注:">
<el-input
v-model="form.remark"
type="textarea"
placeholder="请输入备注"
:rows="3"
resize="none"
style="width: 100%"
maxlength="200"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button
@click="handleClose"
size="large"
style="width: 100px;"
:disabled="submitLoading"
>
取消
</el-button>
<el-button
type="primary"
@click="submitForm"
size="large"
style="width: 100px;"
:loading="submitLoading"
>
确定
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { useRewardsPunishReasonStore } from '@/store/modules/rewardsPunishReason'
import { useDeptStore } from '@/store/modules/dept'
import { UploadImgs } from '@/components/UploadFile'
import type { MyReportDTO, SaveParams, UserIntegralRecord, RuleScoreResponse } from '@/api/submit/myReporting/types'
import { createMyReport, updateMyReport, getRuleScore } from '@/api/submit/myReporting'
import { getUserPage } from '@/api/system/user'
import { enumsType } from '@/store/static'
// Props
interface Props {
dialogVisible: boolean
optType: 'add' | 'edit'
formData?: MyReportDTO
}
const props = withDefaults(defineProps<Props>(), {
dialogVisible: false,
optType: 'add',
formData: () => ({})
})
// Emits
const emit = defineEmits<{
(e: 'update:dialogVisible', value: boolean): void
(e: 'success'): void
}>()
// Store
const rewardsPunishReasonStore = useRewardsPunishReasonStore()
const deptStore = useDeptStore()
//
const deptOptions = computed(() => deptStore.getDeptList)
//
const submitLoading = ref<boolean>(false)
const personLoading = ref<boolean>(false)
const dialogTitle = ref<string>('新增上报')
const formRef = ref<FormInstance>()
const visible = ref<boolean>(props.dialogVisible)
//
watch(
() => visible.value,
(newVal) => {
emit('update:dialogVisible', newVal)
}
)
// dialogVisible
watch(
() => props.dialogVisible,
(newVal) => {
visible.value = newVal
}
)
const personOptions = ref<any[]>([])
const reportTypeOptions = ref<any[]>([])
//
const ruleScore = ref<RuleScoreResponse>({ type: '', scores: [] })
const showScoreInput = ref(true)
const showScoreSelect = ref(false)
const scoreDisabled = ref(true)
//
const getInitialForm = (): SaveParams => ({
id: undefined,
title: '',
deptId: '',
deptName: '',
reasonId: '',
personName: '',
personId: '',
remark: '',
content: '',
imageUrls: [],
userIntegralRecords: [{
userId: '',
userName: '',
score: null
}],
comments: []
})
const form = reactive<SaveParams>(getInitialForm())
//
const rules = reactive<FormRules>({
title: [
{ required: true, message: '标题不能为空', trigger: 'blur' },
{ min: 1, max: 50, message: '标题长度在 1 到 50 个字符', trigger: 'blur' }
],
deptId: [{ required: true, message: '部门不能为空', trigger: 'change' }],
reasonId: [{ required: true, message: '事项不能为空', trigger: 'change' }],
personId: [{ required: true, message: '涉事人不能为空', trigger: 'change' }],
'userIntegralRecords[0].score': [{ required: true, message: '分值不能为空', trigger: 'blur' }]
})
//
watch(visible, (newVal) => {
if (newVal) {
dialogTitle.value = props.optType === 'add' ? '新增上报' : '编辑上报'
//
loadReportTypes()
if (props.optType === 'edit' && props.formData) {
//
// userIntegralRecords
const integralRecord = props.formData.userIntegralRecords && props.formData.userIntegralRecords.length > 0
? props.formData.userIntegralRecords[0]
: { userId: '', userName: '', score: null }
//
const filteredComments = (props.formData.comments || []).map(comment => ({
comment: comment.comment,
createUserName: comment.createUserName
}));
Object.assign(form, {
id: props.formData.id,
title: props.formData.title,
deptId: String(props.formData.deptId),
deptName: props.formData.deptName,
reasonId: String(props.formData.reportType || props.formData.reasonId),
personName: props.formData.personName,
personId: String(props.formData.personId),
remark: props.formData.remark,
imageUrls: props.formData.imageUrls || [],
userIntegralRecords: [integralRecord],
comments: filteredComments
})
//
if (form.deptId) {
loadPersonOptions(form.deptId)
}
} else {
//
Object.assign(form, getInitialForm())
}
}
}, { immediate: true })
//
const loadReportTypes = async () => {
try {
//
if (!rewardsPunishReasonStore.getIsSetRewardsPunishReason) {
await rewardsPunishReasonStore.setRewardsPunishReasonAction()
}
reportTypeOptions.value = rewardsPunishReasonStore.getRewardsPunishReasonList
} catch (error) {
console.error('加载事项类型失败:', error)
reportTypeOptions.value = []
}
}
//
const loadPersonOptions = async (deptId: string | number) => {
if (!deptId) {
personOptions.value = []
return
}
personLoading.value = true
try {
const response = await getUserPage({
pageNo: 1,
pageSize: 100,
deptId
})
personOptions.value = response.list || []
} catch (error) {
console.error('获取部门用户失败:', error)
ElMessage.error('获取部门用户失败')
personOptions.value = []
} finally {
personLoading.value = false
//
if (form.personId) {
const selectedPerson = personOptions.value.find(item => String(item.id) === form.personId)
if (selectedPerson) {
form.personName = selectedPerson.nickname
form.userIntegralRecords[0].userId = Number(form.personId!) || ''
form.userIntegralRecords[0].userName = form.personName
}
}
}
}
//
const loadScore = async () => {
if (!form.personId || !form.reasonId) {
return
}
scoreDisabled.value = true
try {
const response = await getRuleScore(form.reasonId, form.personId)
ruleScore.value = response
switch (ruleScore.value.type) {
case 'MANUAL_VALUE':
form.userIntegralRecords[0].score = null
showScoreSelect.value = false
showScoreInput.value = true
break
case 'FIXED_VALUE':
case 'CALC_VALUE':
showScoreSelect.value = false
showScoreInput.value = true
form.userIntegralRecords[0].score = ruleScore.value.scores[0] || null
break
case 'CHOOSE_VALUE':
showScoreSelect.value = true
showScoreInput.value = false
form.userIntegralRecords[0].score = null
break
default:
showScoreSelect.value = false
showScoreInput.value = true
}
} catch (error) {
console.error('加载分值规则失败:', error)
form.userIntegralRecords[0].score = null
showScoreSelect.value = false
showScoreInput.value = true
} finally {
scoreDisabled.value = false
}
}
//
const handlePersonChange = () => {
const selectedPerson = personOptions.value.find(item => String(item.id) === form.personId)
if (selectedPerson) {
form.personName = selectedPerson.nickname
form.userIntegralRecords[0].userId = Number(form.personId!) || ''
form.userIntegralRecords[0].userName = form.personName
}
loadScore()
}
//
const handleDeptChange = async () => {
//
form.personId = ''
form.personName = ''
form.userIntegralRecords[0].userId = ''
form.userIntegralRecords[0].userName = ''
//
const selectedDept = deptOptions.value.find(dept => String(dept.id) === form.deptId)
if (selectedDept) {
form.deptName = selectedDept.name
} else {
form.deptName = ''
}
await loadPersonOptions(form.deptId)
loadScore()
}
//
const handleReasonChange = () => {
loadScore()
}
//
const handleClose = () => {
visible.value = false
}
//
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
if (props.optType === 'add') {
await createMyReport(form)
ElMessage.success('新增成功')
} else {
await updateMyReport(form)
ElMessage.success('修改成功')
}
emit('success')
handleClose()
} catch (error: any) {
console.error('提交失败:', error)
ElMessage.error(error.msg || '操作失败')
} finally {
submitLoading.value = false
}
}
})
}
</script>

View File

@ -0,0 +1,700 @@
<template>
<div class="app-container">
<!-- 筛选区域 -->
<div class="filter-container">
<el-card shadow="never">
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="标题">
<el-input
v-model="queryParams.title"
placeholder="请输入"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="部门">
<el-select
v-model="queryParams.deptId"
placeholder="请选择"
clearable
style="width: 200px"
>
<el-option
v-for="dept in deptOptions"
:key="dept.id"
:label="dept.name"
:value="dept.id"
/>
</el-select>
</el-form-item>
<el-form-item label="涉事人">
<el-input
v-model="queryParams.personName"
placeholder="请输入"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="事项">
<el-select
v-model="queryParams.reasonId"
placeholder="请选择"
clearable
style="width: 200px"
>
<el-option
v-for="item in reportTypeOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select
v-model="queryParams.status"
placeholder="请选择"
clearable
style="width: 200px"
>
<el-option
v-for="item in enumsType.reportStatus"
:key="item.value"
:label="item.text"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="填报时间">
<el-date-picker
v-model="createTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery" :loading="loading">
查询
</el-button>
<el-button @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<!-- 表格区域 -->
<div class="table-container">
<el-card shadow="never">
<div class="table-header">
<el-button type="primary" @click="handleAdd">
新增
</el-button>
<el-button @click="handleExport" :loading="exportLoading">
导出
</el-button>
</div>
<el-table
v-loading="loading"
:data="reportList"
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
stripe
style="width: 100%"
>
<el-table-column
label="标题"
prop="title"
min-width="200"
align="center"
show-overflow-tooltip
/>
<el-table-column
label="部门"
prop="deptName"
min-width="120"
align="center"
/>
<el-table-column
label="涉事人"
prop="personName"
min-width="120"
align="center"
/>
<el-table-column label="事项" min-width="120" align="center">
<template #default="{ row }">
{{ row.reportTypeName || row.reason?.name || '-' }}
</template>
</el-table-column>
<el-table-column label="分值" min-width="80" align="center">
<template #default="scope">
{{ scope.row.userIntegralRecords?.[0]?.score || '-' }}
</template>
</el-table-column>
<el-table-column
label="备注"
prop="remark"
min-width="250"
align="center"
show-overflow-tooltip
>
<template #default="scope">
{{ scope.row.remark || '-' }}
</template>
</el-table-column>
<el-table-column label="填报时间" min-width="160" align="center">
<template #default="scope">
{{ formatDisplayTime(scope.row.fillingTime) }}
</template>
</el-table-column>
<el-table-column label="状态" min-width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
label="审批人"
prop="approverName"
min-width="120"
align="center"
/>
<el-table-column
label="审批时间"
prop="approveTime"
min-width="160"
align="center"
/>
<el-table-column
label="操作"
min-width="120"
align="center"
fixed="right"
>
<template #default="scope">
<span class="table-opera-item" @click="handleView(scope.row)" :title="'查看'">
<el-icon class="table-opera-icon"><View /></el-icon>
</span>
<span v-if="scope.row.status === 1 || scope.row.status === 'STAGING'" class="table-opera-item ml10" @click="handleEdit(scope.row)" :title="'编辑'">
<el-icon class="table-opera-icon"><Edit /></el-icon>
</span>
<span v-if="scope.row.status === 1 || scope.row.status === 'STAGING'" class="table-opera-item ml10" @click="handleCommit(scope.row.id)" :title="'提交'">
<el-icon class="table-opera-icon" :class="{ 'loading-icon': commitLoading === scope.row.id }">
<UploadFilled /></el-icon>
</span>
<span v-if="scope.row.status === 1 || scope.row.status === 'STAGING'" class="table-opera-item ml10" @click="handleDelete(scope.row.id)" :title="'删除'">
<el-icon class="table-opera-icon delete-icon"><Delete /></el-icon>
</span>
</template>
</el-table-column>
</el-table>
<!-- 空状态 -->
<template v-if="!loading && reportList.length === 0">
<el-empty description="暂无上报数据" :image-size="200">
<el-button type="primary" @click="handleAdd"></el-button>
</el-empty>
</template>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
</div>
<!-- 引入新增/编辑弹窗组件 -->
<AddReport
v-model:dialogVisible="dialogVisible"
:optType="optType"
:formData="formData"
@success="handleAddEditSuccess"
/>
<!-- 查看详情组件 -->
<DetailComponent
v-model:visible="detailDialogVisible"
:report-id="selectedReportId"
@refresh="getList"
/>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref, computed, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { View, Edit, UploadFilled, Delete } from '@element-plus/icons-vue'
import { useRoute } from 'vue-router'
import DetailComponent from '../common/detail.vue'
import { useRewardsPunishReasonStore } from '@/store/modules/rewardsPunishReason'
import { useDeptStore } from '@/store/modules/dept'
import Pagination from '@/components/Pagination/index.vue'
import AddReport from './add-report.vue'
import type { MyReportDTO, QueryParams } from '@/api/submit/myReporting/types'
import { getMyReportPage, deleteMyReport, exportMyReport, commitMyReport } from '@/api/submit/myReporting'
import { getMyReportDetail } from '@/api/submit/common/common'
import { enumsType } from '@/store/static'
// Store
const rewardsPunishReasonStore = useRewardsPunishReasonStore()
const deptStore = useDeptStore()
// Router
const route = useRoute()
//
const deptOptions = computed(() => deptStore.getDeptList)
//
const reportList = ref<MyReportDTO[]>([])
const reportTypeOptions = ref<any[]>([])
const total = ref<number>(0)
const loading = ref<boolean>(true)
const exportLoading = ref<boolean>(false)
const commitLoading = ref<string>('')
const createTimeRange = ref<string[] | null>(null)
const dialogVisible = ref<boolean>(false)
const detailDialogVisible = ref<boolean>(false)
const selectedReportId = ref<string>('')
const optType = ref<'add' | 'edit'>('add')
const formData = ref<MyReportDTO>({
id: '',
title: '',
deptId: '',
deptName: '',
reasonId: '',
reportTypeName: '',
personName: '',
content: '',
remark: '',
imageUrls: [],
status: 1,
approverId: '',
approverName: '',
approveTime: '',
approveComment: '',
createBy: '',
createTime: '',
userIntegralRecords: [],
comments: []
})
// - 使 reasonId
const queryParams = reactive<QueryParams>({
pageNo: 1,
pageSize: 10,
title: '',
deptId: '',
personName: '',
reasonId: '', // 使 reasonId
status: '',
startTime: undefined,
endTime: undefined,
isOwnReport: true
})
//
const getStatusTagType = (status: string | number) => {
const typeMap: Record<string | number, string> = {
'STAGING': 'warning', 1: 'warning',
'AUDIT': 'primary', 2: 'primary',
'APPROVED': 'success', 3: 'success',
'REVIEW_FAILED': 'danger', 4: 'danger'
}
return typeMap[status] || 'info'
}
const getStatusText = (status: string | number) => {
const statusItem = enumsType.reportStatus.find(item => item.value === status)
return statusItem ? statusItem.text : '未知状态'
}
const formatDateTime = (dateTime: string | undefined) => {
if (!dateTime) return ''
const date = new Date(dateTime)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
const formatDisplayTime = (time: string | undefined) => {
return time ? formatDateTime(time) : '-'
}
//
onMounted(async () => {
await initData()
})
//
const initData = async () => {
try {
//
await Promise.all([
!rewardsPunishReasonStore.getIsSetRewardsPunishReason ? rewardsPunishReasonStore.setRewardsPunishReasonAction() : Promise.resolve(),
!deptStore.getIsSetDept ? deptStore.setDeptInfoAction() : Promise.resolve()
])
//
reportTypeOptions.value = rewardsPunishReasonStore.getRewardsPunishReasonList
await getList()
} catch (error) {
console.error('初始化数据失败:', error)
ElMessage.error('初始化数据失败')
}
}
// /
const handleAddEditSuccess = () => {
getList()
}
//
const getList = async () => {
loading.value = true
try {
const response = await getMyReportPage(queryParams)
reportList.value = response.list || []
total.value = response.total || 0
} catch (error) {
console.error('获取列表失败:', error)
ElMessage.error('获取列表失败')
} finally {
loading.value = false
}
}
//
const handleQuery = () => {
queryParams.pageNo = 1
//
if (createTimeRange.value && createTimeRange.value.length === 2) {
queryParams.startTime = createTimeRange.value[0] + 'T00:00:00'
queryParams.endTime = createTimeRange.value[1] + 'T23:59:59'
} else {
queryParams.startTime = undefined
queryParams.endTime = undefined
}
getList()
}
//
const resetQuery = () => {
Object.assign(queryParams, {
pageNo: 1,
pageSize: 10,
title: '',
deptId: '',
personName: '',
reasonId: '', // reasonId
status: '',
startTime: undefined,
endTime: undefined,
isOwnReport: true
})
createTimeRange.value = null
getList()
}
//
const handleAdd = () => {
optType.value = 'add'
// formData
formData.value = {
id: '',
title: '',
deptId: '',
deptName: '',
reasonId: '',
reportTypeName: '',
personName: '',
content: '',
remark: '',
imageUrls: [],
status: 1,
approverId: '',
approverName: '',
approveTime: '',
approveComment: '',
createBy: '',
createTime: '',
userIntegralRecords: [],
comments: []
}
dialogVisible.value = true
}
//
const handleEdit = async (row: MyReportDTO) => {
try {
// comments
const response = await getMyReportDetail(row.id)
optType.value = 'edit'
formData.value = response
dialogVisible.value = true
} catch (error) {
console.error('获取详情失败:', error)
ElMessage.error('获取详情失败')
}
}
//
const handleView = (row: MyReportDTO) => {
selectedReportId.value = row.id
detailDialogVisible.value = true
}
//
watch(
[() => route.path, () => route.fullPath],
() => {
detailDialogVisible.value = false
},
{ immediate: true }
)
//
const handleDelete = async (id: string) => {
try {
await ElMessageBox.confirm('确定删除该上报记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await deleteMyReport(id)
ElMessage.success('删除成功')
getList()
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
}
}
//
const handleCommit = async (id: string) => {
try {
await ElMessageBox.confirm('确定提交该上报记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
commitLoading.value = id
await commitMyReport(id)
ElMessage.success('提交成功')
getList()
} catch (error) {
if (error !== 'cancel') {
console.error('提交失败:', error)
ElMessage.error('提交失败')
}
} finally {
commitLoading.value = ''
}
}
//
//
const handleExport = async () => {
exportLoading.value = true
try {
const response = await exportMyReport(queryParams)
//
if (response instanceof Blob) {
// Blob URL
const blob = new Blob([response], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
})
const url = window.URL.createObjectURL(blob)
//
const link = document.createElement('a')
link.href = url
// 使
const filename = getFilenameFromResponse(response) || `上报数据.xlsx`
link.download = filename
//
document.body.appendChild(link)
link.click()
//
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} else {
ElMessage.success('导出请求已发送')
}
} catch (error) {
console.error('导出失败:', error)
ElMessage.error('导出失败')
} finally {
exportLoading.value = false
}
}
//
const getFilenameFromResponse = (response: any) => {
try {
// Responseheaders
if (response.headers) {
const contentDisposition = response.headers.get('content-disposition')
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/i)
if (filenameMatch && filenameMatch[1]) {
return decodeURIComponent(filenameMatch[1])
}
}
}
} catch (e) {
console.warn('获取文件名失败:', e)
}
return null
}
</script>
<style scoped>
.app-container {
padding: 20px;
}
.filter-container {
margin-bottom: 20px;
}
.table-container {
margin-top: 20px;
}
.table-header {
margin-bottom: 15px;
display: flex;
justify-content: flex-start;
align-items: center;
gap: 10px;
}
:deep(.el-card) {
border: 1px solid #e4e7ed;
border-radius: 4px;
}
:deep(.el-card__body) {
padding: 20px;
}
:deep(.el-table) {
border: 1px solid #e4e7ed;
border-radius: 4px;
}
:deep(.el-table th) {
background-color: #f5f7fa;
font-weight: 600;
}
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
background-color: #fafafa;
}
:deep(.el-form-item__label) {
font-weight: 500;
color: #606266;
}
.dialog-footer {
text-align: center;
padding-top: 10px;
border-top: 1px solid #e4e7ed;
}
.image-preview {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.comments-container {
max-height: 200px;
overflow-y: auto;
}
.comment-item {
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.comment-item:last-child {
border-bottom: none;
}
.comment-content {
margin-bottom: 4px;
line-height: 1.4;
}
/* 操作列样式 */
.table-opera-item {
cursor: pointer;
color: #409eff;
transition: color 0.3s;
display: inline-block;
}
.table-opera-item:hover {
color: #66b1ff;
}
.table-opera-icon {
font-size: 18px;
vertical-align: middle;
}
.ml10 {
margin-left: 10px;
}
.delete-icon {
color: #eb333d;
}
.delete-icon:hover {
color: #f56c6c;
}
.loading-icon {
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>

View File

@ -0,0 +1,213 @@
<template>
<el-dialog
title="导入数据"
v-model="showDialog"
:before-close="handleClose"
:close-on-click-modal="false"
>
<el-form
size="default"
@submit.prevent
:model="entity"
ref="addForm"
:rules="rules"
class="member"
label-position="left"
>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="月份" class="labelTop" prop="yearMonth">
<el-date-picker
v-model="entity.yearMonth"
type="month"
placeholder="请选择..."
value-format="YYYY-MM"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="文件" class="labelTop" prop="file">
<el-upload
ref="uploadFileRef"
:limit="1"
:auto-upload="false"
drag
action="none"
accept=".xlsx"
:on-exceed="exceedFile"
:file-list="fileList"
:show-file-list="true"
:on-remove="handleRemove"
:on-change="handleChange"
>
<el-icon class="el-icon--upload">
<UploadFilled />
</el-icon>
<div class="el-upload__text">
Drop file here or <em>click to upload</em>
</div>
<template #tip>
<div class="el-upload__tip">
<el-link type="primary" @click="downloadTemplate" target="_blank">
<template #default>
<el-icon>
<Download />
</el-icon>
下载模板
</template>
</el-link>
仅支持 .xlsx 格式文件
</div>
</template>
</el-upload>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer v-if="optType !== 'view'">
<span class="dialog-footer">
<el-button @click="handleClose"></el-button>
<el-button type="primary" @click="save"></el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { ElMessage, ElLoading } from 'element-plus'
import { UploadFilled, Download } from '@element-plus/icons-vue'
import { useI18n } from 'vue-i18n'
import { importPaySlip, downloadImportTemplate } from '@/api/submit/paySlip'
const props = defineProps({
dialogVisible: {
type: Boolean,
default: false
},
optType: {
type: String,
default: 'import'
}
})
const emit = defineEmits(['handle-close'])
const { t: $t } = useI18n()
const showDialog = computed({
get: () => props.dialogVisible,
set: (val) => emit('handle-close', val)
})
const addForm = ref()
const uploadFileRef = ref()
const entity = reactive({
yearMonth: '',
file: ''
})
const fileList = ref([])
const rules = {
yearMonth: [
{ required: true, message: '请选择月份', trigger: 'change' }
],
file: [
{ required: true, message: '请选择文件', trigger: 'change' }
]
}
const exceedFile = () => {
ElMessage.warning('只能上传一个文件')
}
const handleRemove = (file) => {
fileList.value = []
entity.file = ''
}
const handleChange = (file) => {
fileList.value = [file]
entity.file = file.raw
}
const handleClose = () => {
showDialog.value = false
resetForm()
}
const resetForm = () => {
if (addForm.value) {
addForm.value.resetFields()
}
fileList.value = []
entity.file = ''
}
const downloadTemplate = () => {
downloadImportTemplate().then((res) => {
const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '工资条导入模板.xlsx'
link.click()
window.URL.revokeObjectURL(url)
})
}
const save = () => {
addForm.value.validate((valid) => {
if (valid) {
if (!entity.file) {
ElMessage.warning('请选择文件')
return
}
const loading = ElLoading.service({
lock: true,
text: '导入中...',
background: 'rgba(0, 0, 0, 0.7)'
})
const formData = new FormData()
formData.append('file', entity.file)
// yearMonthyearmonth
if (entity.yearMonth) {
const [year, month] = entity.yearMonth.split('-')
formData.append('year', year)
formData.append('month', month)
}
importPaySlip(formData).then(() => {
loading.close()
ElMessage.success('导入成功')
showDialog.value = false
resetForm()
emit('handle-close')
}).catch(() => {
loading.close()
ElMessage.error('导入失败')
})
}
})
}
</script>
<style lang="scss" scoped>
.dialog-footer {
text-align: center;
}
.member {
padding: 20px;
background-color: #fafafa;
}
.labelTop {
.el-form-item__label {
padding-top: 0;
}
}
</style>

View File

@ -0,0 +1,538 @@
<template>
<div class="manage-container">
<div class="wrap">
<el-form
class="wrap b-white top"
:model="search"
ref="searchForm"
label-width="120px"
label-position="right"
size="default"
>
<el-row :gutter="24">
<el-col :span="4">
<el-form-item class="label1" label="月份">
<el-date-picker
v-model="search.yearMonth"
type="month"
placeholder="请选择..."
value-format="YYYY-MM"
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item class="label1" label="工号">
<el-input
v-model="search.employeeId"
:maxlength="20"
auto-complete="off"
placeholder="请输入"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item class="label1" label="手机号">
<el-input
v-model="search.mobileNo"
:maxlength="30"
auto-complete="off"
placeholder="请输入"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item class="label1" label="姓名">
<el-input
v-model="search.employeeName"
:maxlength="30"
auto-complete="off"
placeholder="请输入"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item class="label1" label="已发短信">
<el-select v-model="search.msmSend" placeholder="请选择..." clearable>
<el-option
v-for="(item, index) in yesOrNoList"
:key="index"
:label="item.name"
:value="item.code"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="4">
<el-form-item class="label1" label="已签署">
<el-select v-model="search.signed" placeholder="请选择..." clearable>
<el-option
v-for="(item, index) in yesOrNoList"
:key="index"
:label="item.name"
:value="item.code"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6" :offset="14" class="mt4">
<el-button size="small" type="primary" @click="searchFun">
<el-icon class="btn-icon">
<Search />
</el-icon>
查询
</el-button>
<el-button size="small" type @click="reset">
<el-icon class="btn-icon">
<RefreshLeft />
</el-icon>
重置
</el-button>
</el-col>
</el-row>
</el-form>
</div>
<div class="line"></div>
<div class="wrap">
<div class="table-content">
<div class="b-white">
<el-button type="primary" size="small" @click="importData">
<el-icon class="btn-icon">
<UploadFilled />
</el-icon>
导入
</el-button>
<el-button
type="primary"
size="small"
class="ml10"
@click="batchSend"
:disabled="!canBatchSend"
>
<el-icon class="btn-icon">
<Message />
</el-icon>
批量发送
</el-button>
</div>
<el-table
border
stripe
size="default"
ref="table"
class="mt16"
v-loading="listLoading"
:data="tableListData"
@selection-change="handleSelectionChange"
>
<template #empty>
<el-empty description="暂无数据" />
</template>
<el-table-column type="selection" width="55" :selectable="isRowSelectable" />
<el-table-column show-overflow-tooltip label="月份" prop="fullYearMonth" />
<el-table-column show-overflow-tooltip label="工号" prop="employeeId" />
<el-table-column
show-overflow-tooltip
label="手机号"
prop="mobileNo"
min-width="100px"
/>
<el-table-column show-overflow-tooltip label="姓名" prop="employeeName" />
<el-table-column show-overflow-tooltip label="工资" prop="salary" />
<el-table-column show-overflow-tooltip label="出勤天数" prop="attendanceDays" />
<el-table-column show-overflow-tooltip label="出勤工资" prop="attendanceSalary" />
<el-table-column show-overflow-tooltip label="通讯费" prop="communicationFees" />
<el-table-column show-overflow-tooltip label="加班费" prop="overtimePay" />
<el-table-column show-overflow-tooltip label="岗位补贴" prop="jobSubsidies" />
<el-table-column show-overflow-tooltip label="全勤奖" prop="fullAttendanceBonus" />
<el-table-column show-overflow-tooltip label="扣餐费" prop="mealExpenses" />
<el-table-column show-overflow-tooltip label="房补" prop="housingSupplement" />
<el-table-column show-overflow-tooltip label="补发" prop="supplementFees" />
<el-table-column show-overflow-tooltip label="扣保险" prop="insurancePremiumExpenses" />
<el-table-column show-overflow-tooltip label="扣罚" prop="penaltyDeductionExpenses" />
<el-table-column show-overflow-tooltip label="扣保证金" prop="marginExpenses" />
<el-table-column show-overflow-tooltip label="退回保证金" prop="refundOfDeposit" />
<el-table-column show-overflow-tooltip label="应发工资" prop="salariesPayable" />
<el-table-column show-overflow-tooltip label="安排签署日期" prop="arrangeSigningDate" />
<el-table-column show-overflow-tooltip label="发送时间" prop="msmSendTime" />
<el-table-column show-overflow-tooltip label="签署时间" prop="signedTime" />
<el-table-column fixed="right" label="操作">
<template #default="{ row }">
<span
class="my-primary"
@click="deleteRow(row)"
v-if="row.msmSend === 'N' && row.signed === 'N'"
>
<el-icon class="table-opera-icon" title="删除">
<Delete color="#eb333d" />
</el-icon>
</span>
<span class="my-primary ml10" @click="handleSendMsm(row)" v-if="row.msmSend === 'N'">
<el-icon class="table-opera-icon" title="发送">
<Message />
</el-icon>
</span>
<span class="my-primary ml10" @click="view(row)">
<el-icon class="table-opera-icon" title="查看">
<View />
</el-icon>
</span>
</template>
</el-table-column>
</el-table>
<el-pagination
class="mt16 tr"
@size-change="handleSizeChange"
:page-sizes="[10, 20, 50, 100, 200]"
@current-change="pageList"
:current-page="pageData.pageNum"
:page-size="pageData.pageSize"
:total="pageData.total"
layout="->,total, sizes, prev, pager, next, jumper"
:page-count="pageData.pages"
/>
</div>
<ImportPaySlip :optType="optType" :dialogVisible="dialogVisible" @handle-close="close" />
<PaySlipDetail
ref="paySlipDetail"
:dialog-visible="detailDialogVisible"
@update:dialog-visible="detailDialogVisible = $event"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, RefreshLeft, UploadFilled, Delete, Message, View } from '@element-plus/icons-vue'
import { useI18n } from 'vue-i18n'
import ImportPaySlip from './import.vue'
import PaySlipDetail from './paySlipDetail.vue'
import { getPaySlipPage, deletePaySlip, sendMsm, batchSendMsm } from '@/api/submit/paySlip'
import { PaySlipDTO, QueryParams } from '@/api/submit/paySlip/types'
const { t: $t } = useI18n()
const searchForm = ref()
const table = ref()
const paySlipDetail = ref()
//
interface SearchParams {
yearMonth: string;
employeeId: string;
employeeName: string;
mobileNo: string;
signed: string; // 使'Y'/'N'
msmSend: string; // 使'Y'/'N'
year: string;
month: string;
}
const search = reactive<SearchParams>({
yearMonth: '',
employeeId: '',
employeeName: '',
mobileNo: '',
signed: '',
msmSend: '',
year: '',
month: ''
})
const listLoading = ref(false)
const tableListData = ref([])
const pageData = reactive({
total: 0,
pages: 1,
pageNum: 1,
pageSize: 10
})
const dialogVisible = ref(false)
const optType = ref('import')
const detailDialogVisible = ref(false)
//
const selectionData = ref([])
//
const canBatchSend = computed(() => {
if (!selectionData.value || selectionData.value.length === 0) {
return false
}
//
return selectionData.value.some(item => item?.msmSend === 'N')
})
//
const handleSelectionChange = (selection) => {
selectionData.value = selection || []
}
//
const batchSend = () => {
if (!selectionData.value || selectionData.value.length === 0) {
ElMessage.warning('请先选择要发送的数据')
return
}
//
const unSendList = selectionData.value.filter(item => item?.msmSend === 'N')
if (!unSendList || unSendList.length === 0) {
ElMessage.warning('所选数据中没有未发送的记录')
return
}
//
ElMessageBox.confirm('确定要批量发送选中的记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const ids = unSendList.map(item => item.id)
batchSendMsm(ids).then(() => {
ElMessage.success('批量发送成功')
pageList() //
}).catch(() => {
ElMessage.error('批量发送失败')
})
}).catch(() => {
//
})
}
const yesOrNoList = [
{ code: 'Y', name: '是' },
{ code: 'N', name: '否' }
]
onMounted(() => {
pageList()
})
const searchFun = () => {
pageData.pageNum = 1
pageList()
}
const reset = () => {
Object.assign(search, {
yearMonth: '',
employeeId: '',
employeeName: '',
mobileNo: '',
signed: '',
msmSend: ''
})
pageData.pageNum = 1
pageList()
}
const pageList = () => {
listLoading.value = true
//
const query: QueryParams = {
//
pageNo: pageData.pageNum,
pageSize: pageData.pageSize,
//
employeeId: search.employeeId,
employeeName: search.employeeName,
mobileNo: search.mobileNo
}
// yearMonthyearmonth
if (search.yearMonth) {
const [year, month] = search.yearMonth.split('-')
query.year = year
query.month = month
} else {
// yearMonth使yearmonth
query.year = search.year
query.month = search.month
}
// signed'Y'/'N'
if (search.signed) {
query.signed = search.signed
}
// msmSend'Y'/'N'
if (search.msmSend) {
query.msmSend = search.msmSend
}
getPaySlipPage(query).then(res => {
listLoading.value = false
tableListData.value = res.list; // list
pageData.total = res.total; // total
pageData.pages = Math.ceil(res.total / pageData.pageSize);
}).catch(() => {
listLoading.value = false
})
}
const handleSizeChange = (size: number) => {
pageData.pageSize = size
pageData.pageNum = 1
pageList()
}
const importData = () => {
dialogVisible.value = true
}
const close = () => {
dialogVisible.value = false
pageList()
}
const deleteRow = (row: PaySlipDTO) => {
ElMessageBox.confirm('确定要删除这条记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deletePaySlip(row.id).then(() => {
ElMessage.success('删除成功')
pageList()
}).catch(() => {
ElMessage.error('删除失败')
})
}).catch(() => {})
}
const handleSendMsm = (row: PaySlipDTO) => {
ElMessageBox.confirm('确定要发送短信吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
sendMsm(row.id).then(() => {
ElMessage.success('发送成功')
pageList()
}).catch(() => {
ElMessage.error('发送失败')
})
}).catch(() => {})
}
const view = (row: PaySlipDTO) => {
paySlipDetail.value.setEntity(row)
detailDialogVisible.value = true
}
//
const isRowSelectable = (row: PaySlipDTO) => {
//
// 1. msmSendYsignedY
// 2. msmSendYsignedN
// msmSendY
return row.msmSend !== 'Y'
}
</script>
<style lang="scss" scoped>
.manage-container {
width: 100%;
min-height: calc(100vh - 60px);
background-color: #f5f7fa;
padding: 20px;
//
.wrap {
margin-bottom: 20px;
}
// 线
.line {
height: 1px;
background-color: #e8e8e8;
margin-bottom: 20px;
}
//
.table-content {
.b-white {
background-color: #fff;
padding: 16px;
border-radius: 4px;
margin-bottom: 0;
}
}
//
.mt16 {
margin-top: 16px;
}
.mt4 {
margin-top: 4px;
}
.tr {
text-align: right;
}
.ml10 {
margin-left: 10px;
}
//
.my-primary {
color: #409eff;
cursor: pointer;
&:hover {
color: #66b1ff;
}
}
//
.btn-icon {
margin-right: 4px;
}
//
.table-opera-icon {
font-size: 18px;
}
//
.el-table {
border-radius: 4px;
//
th {
background-color: #fafafa;
font-weight: 500;
text-align: center;
}
//
td {
text-align: center;
}
//
.el-table__row {
transition: background-color 0.2s ease;
&:hover {
background-color: #f5f7fa;
}
}
}
//
.el-pagination {
margin-top: 20px;
text-align: right;
}
}
</style>

View File

@ -0,0 +1,199 @@
<template>
<div>
<el-drawer
:model-value="dialogVisible"
@update:model-value="handleClose"
title="工资条详情"
size="50%"
:close-on-click-modal="true"
:destroy-on-close="true"
direction="rtl"
>
<div class="main-container">
<div class="group-warp">
<div class="title">{{ entity.fullYearMonth }}工资条</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="月份">{{ entity.fullYearMonth }}</el-descriptions-item>
<el-descriptions-item label="工号">{{ entity.employeeId }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ entity.employeeName }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ entity.mobileNo }}</el-descriptions-item>
<el-descriptions-item label="工资">{{ entity.salary }}</el-descriptions-item>
<el-descriptions-item label="出勤天数">{{ entity.attendanceDays }}</el-descriptions-item>
<el-descriptions-item label="出勤工资">{{ entity.attendanceSalary }}</el-descriptions-item>
<el-descriptions-item label="通讯费">{{ entity.communicationFees }}</el-descriptions-item>
<el-descriptions-item label="加班费">{{ entity.overtimePay }}</el-descriptions-item>
<el-descriptions-item label="岗位补贴">{{ entity.jobSubsidies }}</el-descriptions-item>
<el-descriptions-item label="全勤奖">{{ entity.fullAttendanceBonus }}</el-descriptions-item>
<el-descriptions-item label="扣餐费">{{ entity.mealExpenses }}</el-descriptions-item>
<el-descriptions-item label="房补">{{ entity.housingSupplement }}</el-descriptions-item>
<el-descriptions-item label="补发">{{ entity.supplementFees }}</el-descriptions-item>
<el-descriptions-item label="扣保险">{{ entity.insurancePremiumExpenses }}</el-descriptions-item>
<el-descriptions-item label="扣罚">{{ entity.penaltyDeductionExpenses }}</el-descriptions-item>
<el-descriptions-item label="扣保证金">{{ entity.marginExpenses }}</el-descriptions-item>
<el-descriptions-item label="退回保证金">{{ entity.refundOfDeposit }}</el-descriptions-item>
<el-descriptions-item label="应发工资">{{ entity.salariesPayable }}</el-descriptions-item>
<el-descriptions-item label="安排签署日期">{{ entity.arrangeSigningDate }}</el-descriptions-item>
<el-descriptions-item label="发送时间">{{ entity.msmSendTime }}</el-descriptions-item>
<el-descriptions-item label="签署时间">{{ entity.signedTime }}</el-descriptions-item>
</el-descriptions>
</div>
<div class="group-warp">
<div class="title">签名</div>
<div class="img-wrap">
<el-image
v-if="imagesArray.length > 0"
:src="imagesArray[0]"
fit="cover"
@click="showImage(0)"
class="img"
/>
<el-empty v-else description="暂无签名" />
</div>
</div>
</div>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { formatDate } from '@/utils/formatTime'
const props = defineProps({
dialogVisible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:dialogVisible'])
const { t: $t } = useI18n()
const entity = reactive({
fullYearMonth: '',
employeeId: '',
employeeName: '',
mobileNo: '',
salary: '',
attendanceDays: '',
attendanceSalary: '',
communicationFees: '',
overtimePay: '',
jobSubsidies: '',
fullAttendanceBonus: '',
mealExpenses: '',
housingSupplement: '',
supplementFees: '',
insurancePremiumExpenses: '',
penaltyDeductionExpenses: '',
marginExpenses: '',
refundOfDeposit: '',
salariesPayable: '',
arrangeSigningDate: '',
msmSendTime: '',
signedTime: '',
signatureUrl: ''
})
const imagesArray = ref([])
watch(() => props.dialogVisible, (newVal) => {
if (!newVal) {
resetForm()
}
})
const setEntity = (data) => {
//
const formattedData = JSON.parse(JSON.stringify(data))
//
if (formattedData.msmSendTime) {
formattedData.msmSendTime = formatDate(new Date(formattedData.msmSendTime))
}
if (formattedData.signedTime) {
formattedData.signedTime = formatDate(new Date(formattedData.signedTime))
}
Object.assign(entity, formattedData)
if (formattedData.signatureUrl) {
imagesArray.value = [formattedData.signatureUrl]
} else {
imagesArray.value = []
}
}
const resetForm = () => {
Object.assign(entity, {
fullYearMonth: '',
employeeId: '',
employeeName: '',
mobileNo: '',
salary: '',
attendanceDays: '',
attendanceSalary: '',
communicationFees: '',
overtimePay: '',
jobSubsidies: '',
fullAttendanceBonus: '',
mealExpenses: '',
housingSupplement: '',
supplementFees: '',
insurancePremiumExpenses: '',
penaltyDeductionExpenses: '',
marginExpenses: '',
refundOfDeposit: '',
salariesPayable: '',
arrangeSigningDate: '',
msmSendTime: '',
signedTime: '',
signatureUrl: ''
})
imagesArray.value = []
}
const showImage = (index) => {
//
console.log('预览图片:', imagesArray.value[index])
}
const handleClose = () => {
emit('update:dialogVisible', false)
}
//
defineExpose({
setEntity
})
</script>
<style lang="scss" scoped>
.main-container {
padding: 20px;
}
.group-warp {
margin-bottom: 20px;
}
.title {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
.img-wrap {
margin-top: 10px;
.img {
width: 200px;
height: 100px;
cursor: pointer;
border: 1px solid #eee;
border-radius: 4px;
}
}
</style>

View File

@ -0,0 +1,358 @@
<template>
<div class="app-container">
<div class="filter-container">
<el-card shadow="never">
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="编码">
<el-input v-model="queryParams.code" placeholder="请输入" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="名称">
<el-input v-model="queryParams.name" placeholder="请输入" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="是否启用">
<el-select v-model="queryParams.enable" placeholder="请选择" clearable style="width: 200px">
<el-option label="是" value="1" />
<el-option label="否" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery"></el-button>
<el-button @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<div class="table-container">
<el-card shadow="never">
<div class="table-header">
<el-button type="primary" icon="ep:plus" @click="handleAdd"></el-button>
</div>
<el-table
v-loading="loading"
:data="reportManageList"
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
stripe
style="width: 100%"
>
<el-table-column label="编码" prop="code" min-width="100" align="center" />
<el-table-column label="名称" prop="name" min-width="120" align="center" />
<el-table-column label="描述" prop="remark" min-width="250" align="center" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.remark || '-' }}
</template>
</el-table-column>
<el-table-column label="排序序号" prop="orderNum" min-width="90" align="center">
<template #default="scope">
{{ scope.row.orderNum || 0 }}
</template>
</el-table-column>
<!-- 修正点 status 改为 enable -->
<el-table-column label="是否启用" prop="enable" min-width="90" align="center">
<template #default="scope">
<el-tag :type="scope.row.enable === 1 ? 'success' : 'info'">
{{ scope.row.enable === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" link icon="ep:edit" @click="handleUpdate(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
</div>
<!-- 添加或修改上报事项对话框 -->
<el-dialog :title="title" v-model="open" width="600px" append-to-body>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="编码:" prop="code">
<el-input v-model="form.code" placeholder="请输入编码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="名称:" prop="name">
<el-input v-model="form.name" placeholder="请输入名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="排序序号:" prop="orderNum">
<el-input-number
v-model="form.orderNum"
:min="0"
:max="9999"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<!-- 修正点 status 改为 enable -->
<el-form-item label="是否启用:" prop="enable">
<el-radio-group v-model="form.enable">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="描述:" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
placeholder="请输入描述"
:rows="4"
resize="none"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel" size="large" style="width: 100px;">取消</el-button>
<el-button type="primary" @click="submitForm" size="large" style="width: 100px;">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
// TypeScript ReportManageDTO, QueryParams, SaveParams
import type { ReportManageDTO, QueryParams, SaveParams } from '@/api/submit/systemConfig/reportManage/types'
import { getReportManagePage, createReportManage, updateReportManage } from '@/api/submit/systemConfig/reportManage'
import Pagination from '@/components/Pagination/index.vue'
//
const reportManageList = ref<ReportManageDTO[]>([])
const total = ref<number>(0)
const loading = ref<boolean>(true)
//
const queryParams = reactive<QueryParams>({
pageNo: 1,
pageSize: 10,
code: '',
name: '',
enable: '' // status enable
})
//
const form = reactive<SaveParams>({
code: '',
name: '',
orderNum: 0,
enable: 1, // status enable
remark: '',
type: 'REWARDS_PUNISH_REASON'
})
//
const rules = reactive<FormRules>({
code: [{ required: true, message: '编码不能为空', trigger: 'blur' }],
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
orderNum: [{ required: true, message: '排序等级不能为空', trigger: 'blur' }],
enable: [{ required: true, message: '是否启用不能为空', trigger: 'change' }] // status enable
})
//
const open = ref<boolean>(false)
const title = ref<string>('')
const formRef = ref<FormInstance>()
//
onMounted(() => {
getList()
})
//
const getList = async () => {
loading.value = true
try {
const response = await getReportManagePage(queryParams)
reportManageList.value = response.list || []
total.value = response.total || 0
} catch (error) {
console.error('获取列表失败:', error)
ElMessage.error('获取列表失败')
} finally {
loading.value = false
}
}
//
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
//
const resetQuery = () => {
Object.assign(queryParams, {
pageNo: 1,
pageSize: 10,
code: '',
name: '',
enable: '' // status enable
})
getList()
}
//
const handleAdd = () => {
open.value = true
title.value = '新增上报事项'
resetForm()
}
//
const handleUpdate = (row: ReportManageDTO) => {
open.value = true
title.value = '修改上报事项'
// status enable
Object.assign(form, {
...row,
enable: Number(row.enable) //
})
}
//
const resetForm = () => {
Object.assign(form, {
id: undefined,
code: '',
name: '',
orderNum: 0,
enable: 1, // status enable
remark: '',
type: 'REWARDS_PUNISH_REASON'
})
formRef.value?.resetFields()
}
//
const cancel = () => {
open.value = false
resetForm()
}
//
const submitForm = async () => {
const valid = await formRef.value?.validate()
if (!valid) return
try {
if (form.id) {
await updateReportManage(form)
ElMessage.success('修改成功')
} else {
await createReportManage(form)
ElMessage.success('新增成功')
}
open.value = false
getList()
} catch (error) {
console.error('提交失败:', error)
ElMessage.error(form.id ? '修改失败' : '新增失败')
}
}
</script>
<style scoped>
/* 样式部分无需修改 */
.app-container {
padding: 20px;
}
.filter-container {
margin-bottom: 20px;
}
.table-container {
margin-top: 20px;
}
.table-header {
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
:deep(.el-card) {
border: 1px solid #e4e7ed;
border-radius: 4px;
}
:deep(.el-card__body) {
padding: 20px;
}
:deep(.el-table) {
border: 1px solid #e4e7ed;
border-radius: 4px;
}
:deep(.el-table th) {
background-color: #f5f7fa;
font-weight: 600;
}
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
background-color: #fafafa;
}
/* 表单样式 */
:deep(.el-form-item__label) {
font-weight: 500;
color: #606266;
}
:deep(.el-input),
:deep(.el-input-number) {
width: 100%;
}
:deep(.el-radio-group) {
display: flex;
align-items: center;
gap: 20px;
}
.dialog-footer {
text-align: center;
padding-top: 10px;
border-top: 1px solid #e4e7ed;
}
:deep(.el-dialog__footer) {
padding: 20px;
}
</style>

View File

@ -0,0 +1,418 @@
<template>
<el-dialog
:title="dialogTitle"
:model-value="modelValue"
:before-close="handleClose"
:close-on-click-modal="false"
@update:model-value="emit('update:modelValue', $event)"
width="600px"
>
<el-form
size="default"
@submit.prevent
:model="entity"
ref="addForm"
:rules="rules"
class="member"
label-width="100px"
>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="标题:" prop="title">
<el-input
:maxlength="50"
size="default"
class="fw"
v-model="entity.title"
placeholder="请输入标题"
clearable
:disabled="isView"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="事项:" prop="reasonId">
<el-select
v-model="entity.reasonId"
filterable
placeholder="请选择事项"
clearable
:disabled="isView"
>
<el-option
v-for="item in enums['REWARDS_PUNISH_REASON']"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="规则类型:" prop="type">
<el-select
v-model="entity.type"
filterable
placeholder="请选择规则类型"
clearable
@change="initScore"
:disabled="isView"
>
<el-option
v-for="(item, index) in integralRuleTypeEnum"
:key="index"
:label="item.name"
:value="item.code"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" v-if="entity.type === 'FIXED_VALUE'">
<el-col :span="12">
<el-form-item label="分值:">
<el-input-number
size="default"
class="fw"
v-model="entity.score"
:precision="1"
:step="0.5"
:min="-10"
:max="10"
placeholder="请输入分值"
:disabled="isView"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" v-if="entity.type === 'CHOOSE_VALUE'">
<el-col :span="24">
<el-form-item label="分值范围:">
<div class="score-range">
<div class="score-range-value" v-for="(item, index) in entity.scoreRange" :key="index">
<el-input-number
size="default"
class="fw"
v-model="item.value"
:precision="1"
:step="0.5"
:min="-10"
:max="10"
placeholder="请输入分值"
:disabled="isView"
/>
<i v-if="index === 0 && !isView" @click="addUserScore">
<el-icon class="icon-opera" color="green">
<CirclePlus />
</el-icon>
</i>
<i v-else-if="!isView" @click="removeUserScore(index)">
<el-icon class="icon-opera" color="#eb333d">
<Remove />
</el-icon>
</i>
</div>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" v-if="entity.type === 'CALC_VALUE'">
<el-col :span="24">
<el-form-item label="公式(SQL)" prop="formula">
<el-input
type="textarea"
maxlength="1000"
:autosize="{ minRows: 3, maxRows: 6 }"
size="default"
class="fw"
v-model="entity.formula"
placeholder="请输入SQL公式"
:disabled="isView"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer v-if="!isView">
<span class="dialog-footer">
<el-button @click="handleClose" size="small">取消</el-button>
<el-button type="primary" size="small" @click="submitForm" :loading="loading">
确定
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed } from 'vue'
import { CirclePlus, Remove } from '@element-plus/icons-vue'
import { createIntegralRule, updateIntegralRule } from '@/api/submit/systemConfig/ruleManage'
import { ElMessage } from 'element-plus'
import type { IntegralRule } from '@/api/submit/systemConfig/ruleManage/types'
import { useRewardsPunishReasonStore } from '@/store/modules/rewardsPunishReason'
// Props
const props = defineProps<{
modelValue: boolean,
optType: string,
viewData: IntegralRule
}>()
// Emits
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'handle-close', refresh: boolean): void
}>()
//
const addForm = ref()
const loading = ref(false)
// store
const rewardsPunishReasonStore = useRewardsPunishReasonStore()
// store
const rewardsPunishReasonList = computed(() =>
rewardsPunishReasonStore.getRewardsPunishReasonList
)
//
const isView = computed(() => props.optType === 'view')
const dialogTitle = computed(() => {
switch (props.optType) {
case 'add': return '新建规则'
case 'edit': return '修改规则'
case 'view': return '规则详情'
default: return '规则'
}
})
//
const entity = reactive({
id: '',
title: '',
reasonId: '',
type: '',
score: 0,
scoreRange: [] as { value: number }[],
formula: '',
})
//
const rules = reactive({
title: [
{ required: true, message: '请输入标题', trigger: 'blur' }
],
reasonId: [
{ required: true, message: '请选择事项', trigger: 'change' }
],
type: [
{ required: true, message: '请选择规则类型', trigger: 'change' }
],
formula: [
{ required: true, message: '请输入公式', trigger: 'blur' }
]
})
// - rewardsPunishReasonStore
const enums = reactive({
REWARDS_PUNISH_REASON: rewardsPunishReasonList.value
})
//
watch(() => props.modelValue, async (newVal) => {
if (newVal) {
if (!rewardsPunishReasonStore.getIsSetRewardsPunishReason) {
await rewardsPunishReasonStore.setRewardsPunishReasonAction()
}
enums.REWARDS_PUNISH_REASON = rewardsPunishReasonList.value
}
})
//
const integralRuleTypeEnum = [
{code: 'FIXED_VALUE', name: '固定分值'},
{code: 'MANUAL_VALUE', name: '手工分值'},
{code: 'CALC_VALUE', name: '动态计算分值'},
{code: 'CHOOSE_VALUE', name: '选择分值'}
]
// modelValue
watch(() => props.modelValue, (val) => {
if (val) {
if (props.optType !== 'add') {
initializeFormData()
} else {
resetForm()
}
}
}, { immediate: true })
//
const initializeFormData = () => {
const data = props.viewData
entity.id = data.id || ''
entity.title = data.title || ''
entity.reasonId = data.reasonId || ''
entity.type = data.type || ''
entity.score = Number(data.score) || 0
entity.formula = data.formula || ''
if (data.scoreRange) {
if (Array.isArray(data.scoreRange)) {
entity.scoreRange = data.scoreRange.map(item => ({ value: Number(item) || 0 }))
} else if (typeof data.scoreRange === 'string' && data.scoreRange.trim()) {
entity.scoreRange = data.scoreRange.split(',').map(item => ({
value: Number(item.trim()) || 0
}))
} else {
entity.scoreRange = []
}
} else {
entity.scoreRange = []
}
}
//
const resetForm = () => {
Object.assign(entity, {
id: '',
title: '',
reasonId: '',
type: '',
score: 0,
scoreRange: [],
formula: '',
})
addForm.value?.clearValidate()
}
//
const removeUserScore = (index: number) => {
entity.scoreRange.splice(index, 1)
}
//
const addUserScore = () => {
entity.scoreRange.push({ value: 0 })
}
//
const initScore = () => {
entity.score = 0
entity.scoreRange = []
entity.formula = ''
if (entity.type === 'CHOOSE_VALUE') {
entity.scoreRange.push({ value: 0 })
}
}
//
const handleClose = () => {
addForm.value?.clearValidate()
emit('update:modelValue', false)
emit('handle-close', false)
}
//
const submitForm = async () => {
if (!addForm.value) return
try {
await addForm.value.validate()
loading.value = true
// params
const params: any = {
id: entity.id,
title: entity.title.trim(),
reasonId: entity.reasonId,
type: entity.type,
}
switch (entity.type) {
case 'FIXED_VALUE':
params.score = Number(entity.score) || 0
break
case 'CHOOSE_VALUE':
const scoreValues = entity.scoreRange.map(item => Number(item.value) || 0)
const uniqueScores = [...new Set(scoreValues)].filter(value => !isNaN(value))
params.scoreRange = uniqueScores.join(',')
break
case 'CALC_VALUE':
params.formula = entity.formula.trim()
break
}
// -
if (entity.id) {
await updateIntegralRule(params)
ElMessage.success('修改成功')
} else {
await createIntegralRule(params)
ElMessage.success('新增成功')
}
//
emit('update:modelValue', false)
emit('handle-close', true)
} catch (error: any) {
console.error('提交失败:', error)
const errorMsg = error?.message || error?.msg || (entity.id ? '修改失败' : '新增失败')
ElMessage.error(errorMsg)
} finally {
loading.value = false
}
}
</script>
<style lang="scss" scoped>
.score-range {
width: 100%;
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.score-range-value {
display: flex;
align-items: center;
gap: 8px;
}
.icon-opera {
cursor: pointer;
width: 20px;
height: 20px;
flex-shrink: 0;
}
.member {
padding: 10px 0;
}
:deep(.el-input-number) {
width: 100%;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>

View File

@ -0,0 +1,434 @@
<template>
<div class="manage-container">
<!-- 搜索栏区域 -->
<div class="search-container">
<el-form :model="search" ref="searchForm" inline label-width="60px" size="small">
<el-form-item label="标题">
<el-input
v-model="search.title"
:maxlength="50"
placeholder="请输入"
clearable
style="width: 180px;"
/>
</el-form-item>
<el-form-item label="事项">
<el-select
v-model="search.reasonId"
filterable
placeholder="请选择..."
clearable
style="width: 150px;"
>
<el-option
v-for="item in enums['REWARDS_PUNISH_REASON']"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="类型">
<el-select
v-model="search.type"
filterable
placeholder="请选择..."
clearable
style="width: 150px;"
>
<el-option
v-for="(item, index) in integralRuleTypeEnum"
:key="index"
:label="item.name"
:value="item.code"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="ep:search" size="small" @click="searchFun"></el-button>
<el-button icon="ep:refresh" size="small" @click="reset"></el-button>
</el-form-item>
</el-form>
</div>
<!-- 表格操作栏 + 表格区域 -->
<div class="table-wrapper">
<!-- 操作按钮 -->
<div class="table-toolbar">
<el-button type="primary" icon="ep:plus" size="small" @click="add"></el-button>
</div>
<!-- 表格 -->
<el-table
:data="tableListData"
v-loading="listLoading"
border
size="small"
:header-cell-style="{ background: '#f8f9fa', color: '#333', fontWeight: '500' }"
style="width: 100%;"
>
<template #empty>
<el-empty description="暂无数据" />
</template>
<el-table-column
prop="title"
label="标题"
min-width="180"
show-overflow-tooltip
/>
<el-table-column
label="事项"
min-width="120"
show-overflow-tooltip
>
<template #default="{ row }">
{{ getReasonName(row.reasonId) || '-' }}
</template>
</el-table-column>
<el-table-column
label="规则类型"
min-width="100"
show-overflow-tooltip
>
<template #default="{ row }">
{{ filterIntegralRuleTypeValue(row.type) }}
</template>
</el-table-column>
<!-- 操作列使用 link 类型 + 图标 -->
<el-table-column
label="操作"
width="150"
align="center"
fixed="right"
>
<template #default="{ row }">
<div class="operation-btn-group">
<!-- 编辑按钮 -->
<el-button
type="primary"
link
size="small"
@click="edit(row)"
title="编辑"
>
<el-icon><Edit /></el-icon>
</el-button>
<!-- 删除按钮 -->
<el-button
type="danger"
link
size="small"
@click="deleteRow(row)"
title="删除"
>
<el-icon><Delete /></el-icon>
</el-button>
<!-- 查看按钮 -->
<el-button
type="info"
link
size="small"
@click="view(row)"
title="查看"
>
<el-icon><View /></el-icon>
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
class="pagination"
@size-change="handleSizeChange"
@current-change="pageList"
:current-page="pageData.pageNum"
:page-size="pageData.pageSize"
:page-sizes="[10, 20, 50]"
:total="pageData.total"
layout="total, sizes, prev, pager, next, jumper"
background
size="small"
/>
</div>
</div>
<!-- 弹窗组件 -->
<AddRule
v-model="open"
:optType="optType"
:viewData="viewData"
@handle-close="onRuleClose"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import AddRule from './addRule.vue'
import { Edit, Delete, View } from '@element-plus/icons-vue'
import { getIntegralRulePage, deleteIntegralRule } from '@/api/submit/systemConfig/ruleManage'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { IntegralRule } from '@/api/submit/systemConfig/ruleManage/types'
import { useRewardsPunishReasonStore } from '@/store/modules/rewardsPunishReason'
//
const searchForm = ref()
const listLoading = ref(false)
//
const open = ref(false)
const optType = ref('add')
const viewData = ref<IntegralRule>({})
// store
const rewardsPunishReasonStore = useRewardsPunishReasonStore()
// store
const rewardsPunishReasonList = computed(() =>
rewardsPunishReasonStore.getRewardsPunishReasonList
)
//
const search = reactive({
title: '',
reasonId: '',
type: '',
})
//
const pageData = reactive({
total: 0,
pages: 1,
pageNum: 1,
pageSize: 10
})
//
const tableListData = ref<IntegralRule[]>([])
// - rewardsPunishReasonStore
const enums = reactive({
REWARDS_PUNISH_REASON: rewardsPunishReasonList.value
})
//
const integralRuleTypeEnum = [
{code: 'FIXED_VALUE', name: '固定分值'},
{code: 'MANUAL_VALUE', name: '手工分值'},
{code: 'CALC_VALUE', name: '动态计算分值'},
{code: 'CHOOSE_VALUE', name: '选择分值'}
]
// reasonId
const getReasonName = (reasonId: string) => {
if (!reasonId) return ''
const reasonItem = enums.REWARDS_PUNISH_REASON.find(item => item.id === reasonId)
return reasonItem?.name || ''
}
//
const filterIntegralRuleTypeValue = (value: string) => {
if (!value) return '-'
const item = integralRuleTypeEnum.find(item => item.code === value)
return item?.name || value
}
//
const searchFun = () => {
pageData.pageNum = 1
pageList()
}
//
const reset = () => {
Object.assign(search, {
title: '',
reasonId: '',
type: '',
})
pageData.pageNum = 1
pageList()
}
//
const handleSizeChange = (val: number) => {
pageData.pageSize = val
pageData.pageNum = 1
pageList()
}
//
onMounted(async () => {
if (!rewardsPunishReasonStore.getIsSetRewardsPunishReason) {
await rewardsPunishReasonStore.setRewardsPunishReasonAction()
}
await pageList()
})
//
const pageList = async (pageNum?: number) => {
if (pageNum !== undefined) {
pageData.pageNum = pageNum
}
tableListData.value = []
listLoading.value = true
try {
const response = await getIntegralRulePage({
...search,
pageNo: pageData.pageNum,
pageSize: pageData.pageSize,
})
if (response) {
tableListData.value = response.list || []
pageData.total = response.total || 0
pageData.pages = Math.ceil(pageData.total / pageData.pageSize) || 1
} else {
ElMessage.error('获取数据失败')
}
} catch (error) {
console.error('获取列表失败:', error)
ElMessage.error('网络错误,请稍后重试')
} finally {
listLoading.value = false
}
}
//
const add = () => {
optType.value = 'add'
viewData.value = {}
open.value = true
}
//
const view = (row: IntegralRule) => {
optType.value = 'view'
viewData.value = { ...row }
open.value = true
}
//
const edit = (row: IntegralRule) => {
optType.value = 'edit'
viewData.value = { ...row }
open.value = true
}
//
const deleteRow = async (row: IntegralRule) => {
try {
await ElMessageBox.confirm('确定删除该规则数据吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
await deleteIntegralRule(row.id!)
ElMessage.success('删除成功')
pageList()
} catch (error) {
//
}
}
//
const onRuleClose = (refresh = false) => {
if (refresh) {
pageList(pageData.pageNum)
}
}
</script>
<style lang="scss" scoped>
.manage-container {
min-height: calc(100vh - 60px);
background: #fff;
padding: 15px;
}
/* 搜索栏样式 */
.search-container {
background: #fff;
padding: 10px 15px;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 15px;
}
/* 表格区域样式 */
.table-wrapper {
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 500px;
}
/* 表格操作栏 */
.table-toolbar {
padding: 10px 15px;
border-bottom: 1px solid #f1f1f1;
}
/* 表格样式 */
:deep(.el-table) {
--el-table-row-hover-bg-color: #f8f9fa;
--el-table-border-color: #e6e6e6;
flex: 1;
.el-table__body tr {
height: 40px;
}
.el-table__header th {
height: 40px;
padding: 0 8px;
}
.el-table__body td {
padding: 0 8px;
}
}
/* 操作按钮组样式 */
.operation-btn-group {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
/* 确保链接按钮没有背景 */
:deep(.el-button--link) {
background: transparent;
border: none;
padding: 4px 6px;
min-height: auto;
.el-icon {
font-size: 16px;
}
}
}
/* 分页容器 */
.pagination-container {
padding: 15px;
display: flex;
justify-content: flex-end;
border-top: 1px solid #f1f1f1;
background: #fafafa;
}
/* 分页样式 */
.pagination {
margin: 0;
}
</style>

View File

@ -0,0 +1,383 @@
<template>
<div class="app-container">
<!-- 筛选区域 -->
<div class="filter-container">
<el-card shadow="never">
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="部门">
<el-select
v-model="queryParams.orgId"
placeholder="请选择"
clearable
style="width: 200px"
>
<el-option
v-for="dept in deptOptions"
:key="dept.id"
:label="dept.name"
:value="dept.id"
/>
</el-select>
</el-form-item>
<el-form-item label="用户名">
<el-input
v-model="queryParams.userName"
placeholder="请输入"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="月份范围">
<el-date-picker
v-model="monthRange"
type="monthrange"
range-separator="至"
start-placeholder="开始月份"
end-placeholder="结束月份"
format="YYYY-MM"
value-format="YYYY-MM"
style="width: 250px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery" :loading="listLoading">
<el-icon class="table-opera-icon"><Search /></el-icon>
查询
</el-button>
<el-button @click="resetQuery">
<el-icon class="table-opera-icon"><RefreshLeft /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<!-- 操作区域 -->
<div class="table-header">
<el-button type="primary" size="small" @click="handleExport" :loading="exportLoading">
<el-icon class="table-opera-icon"><Download /></el-icon>
导出
</el-button>
<el-button type="primary" size="small" @click="syncUser" :loading="syncLoading">
<el-icon class="table-opera-icon"><Refresh /></el-icon>
同步数据
</el-button>
</div>
<!-- 表格区域 -->
<div class="table-container">
<el-card shadow="never">
<el-table
v-loading="listLoading"
:data="tableListData"
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
stripe
style="width: 100%"
>
<template #empty>
<el-empty description="暂无数据" :image-size="200">
<el-button type="primary" @click="handleQuery"></el-button>
</el-empty>
</template>
<el-table-column show-overflow-tooltip label="排名" prop="ranking" min-width="80" align="center" />
<el-table-column show-overflow-tooltip label="部门" prop="deptName" min-width="120" align="center" />
<el-table-column show-overflow-tooltip label="用户名" prop="userName" min-width="120" align="center" />
<el-table-column show-overflow-tooltip label="积分" prop="score" min-width="100" align="center" />
<el-table-column
label="操作"
min-width="100"
align="center"
fixed="right"
>
<template #default="{row}">
<span class="table-opera-item" @click="view(row)" :title="'详情'">
<el-icon class="table-opera-icon"><View /></el-icon>
</span>
</template>
</el-table-column>
</el-table>
<pagination
v-show="pageData.total > 0"
:total="pageData.total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="handleQuery"
/>
</el-card>
</div>
<user-score-detail ref="userScoreDetail" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, RefreshLeft, Download, Refresh, View } from '@element-plus/icons-vue'
import { useDeptStore } from '@/store/modules/dept'
import Pagination from '@/components/Pagination/index.vue'
import UserScoreDetail from './userScoreDetail.vue'
import { userScoreApi } from '@/api/submit/userScore'
import type { UserScoreListReqVO, UserScoreListRespVO } from '@/api/submit/userScore/types'
// Store
const deptStore = useDeptStore()
//
const deptOptions = computed(() => deptStore.getDeptList)
//
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
deptName: '',
orgId: '',
userName: '',
beginMonth: undefined,
endMonth: undefined
})
//
const monthRange = ref<string[] | null>(null)
//
const tableListData = ref<UserScoreListRespVO[]>([])
const listLoading = ref(false)
const syncLoading = ref(false)
const exportLoading = ref(false) //
//
const pageData = reactive({
total: 0
})
//
const userScoreDetail = ref()
//
onMounted(() => {
handleQuery()
})
//
const syncUser = async () => {
syncLoading.value = true
try {
await userScoreApi.syncUserScore()
ElMessage.success('用户数据同步成功')
handleQuery()
} catch (error) {
ElMessage.error('同步失败,请稍后重试')
} finally {
syncLoading.value = false
}
}
//
const handleQuery = async () => {
//
if (monthRange.value && monthRange.value.length === 2) {
queryParams.beginMonth = monthRange.value[0]
queryParams.endMonth = monthRange.value[1]
} else {
queryParams.beginMonth = undefined
queryParams.endMonth = undefined
}
tableListData.value = []
listLoading.value = true
try {
//
const params: UserScoreListReqVO = {
userName: queryParams.userName,
deptName: queryParams.deptName,
orgId: queryParams.orgId,
beginMonth: queryParams.beginMonth,
endMonth: queryParams.endMonth,
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize
}
const res = await userScoreApi.getUserScoreStatic(params)
// ranking
const rankedList = res.list.map((item: any, index: number) => ({
...item,
ranking: index + 1
}))
tableListData.value = rankedList
pageData.total = res.total
} catch (error) {
ElMessage.error('查询失败,请稍后重试')
} finally {
listLoading.value = false
}
}
//
const resetQuery = () => {
queryParams.deptName = ''
queryParams.orgId = ''
queryParams.userName = ''
queryParams.beginMonth = undefined
queryParams.endMonth = undefined
monthRange.value = null
queryParams.pageNo = 1
queryParams.pageSize = 10
handleQuery()
}
//
const getFilenameFromResponse = (response: any) => {
// content-disposition
const disposition = response.headers?.['content-disposition'] || response.headers?.['Content-Disposition']
if (!disposition) return null
// "filename=xxx.xlsx" "filename*=UTF-8''xxx.xlsx"
const match = disposition.match(/filename=([^;]+)/i) || disposition.match(/filename\*=UTF-8''([^;]+)/i)
if (match && match[1]) {
return decodeURIComponent(match[1].replace(/["']/g, '')) //
}
return null
}
//
const handleExport = async () => {
exportLoading.value = true
try {
//
const exportParams: UserScoreListReqVO = {
userName: queryParams.userName,
deptName: queryParams.deptName,
orgId: queryParams.orgId,
beginMonth: queryParams.beginMonth,
endMonth: queryParams.endMonth
}
// responseType: 'blob'
const response = await userScoreApi.exportUserScoreStatic(exportParams, {
responseType: 'blob'
})
//
if (response instanceof Blob) {
// Blob URL
const blob = new Blob([response], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
})
const url = window.URL.createObjectURL(blob)
//
const link = document.createElement('a')
link.href = url
// 使
const filename = getFilenameFromResponse(response) || `用户积分排名.xlsx`
link.download = filename
//
document.body.appendChild(link)
link.click()
//
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} else {
ElMessage.success('导出请求已发送')
}
} catch (error) {
console.error('导出失败:', error)
ElMessage.error('导出失败')
} finally {
exportLoading.value = false
}
}
//
const view = (row: UserScoreListRespVO) => {
userScoreDetail.value.open({
monthRange: monthRange.value || [],
row
})
}
</script>
<style scoped>
.app-container {
min-height: 800px;
background: #fff;
}
.filter-container {
margin-bottom: 20px;
}
.table-header {
display: flex;
justify-content: flex-start;
margin-bottom: 20px;
}
.table-container {
margin-top: 20px;
}
.table-opera-icon {
margin-right: 4px;
}
.table-opera-item {
cursor: pointer;
color: #409eff;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4px;
}
.table-opera-item:hover {
color: #66b1ff;
}
.manage-container {
min-height: 800px;
background: #fff;
}
.manage-container .wrap {
padding: 10px 10px;
width: calc(100% - 30px);
}
.manage-container .wrap .top {
background: #fff;
padding: 30px 10px 0;
}
.manage-container .wrap .table-content {
background: #fff;
margin-top: 20px;
padding: 10px 20px;
padding-right: 10px;
}
.manage-container .line {
height: 10px;
background: #f0f2f5;
padding-right: 10px;
}
.manage-container .operas-btns {
display: flex;
}
</style>

View File

@ -0,0 +1,306 @@
<template>
<el-drawer title="数据详情" v-model="dialogVisible" size="38%" :before-close="handleClose">
<div class="drawer-content">
<div class="group-warp">
<el-divider content-position="left"><span class="group-name">基础信息</span></el-divider>
<div class="group-container basic">
<el-descriptions
:column="2"
size="default"
border>
<el-descriptions-item :span="1" label="排名">
{{ search.row.ranking }}
</el-descriptions-item>
<el-descriptions-item :span="1" label="积分">
{{ search.row.score }}
</el-descriptions-item>
<el-descriptions-item label="姓名">
{{ search.row.userName }}
</el-descriptions-item>
<el-descriptions-item label="部门">
{{ search.row.deptName }}
</el-descriptions-item>
<el-descriptions-item label="月份范围">
{{ monthRangeStr }}
</el-descriptions-item>
</el-descriptions>
</div>
</div>
<div class="group-warp">
<el-divider content-position="left"><span class="group-name">月度明细</span></el-divider>
<div class="group-container detail">
<div class="table-content">
<div class="b-white">
<el-button type="primary" size="small" @click="exportExcel" :loading="exportLoading">
<el-icon class="btn-icon">
<Download />
</el-icon>
导出
</el-button>
</div>
<el-table border stripe size="default" ref="table" class="mt16" v-loading="listLoading" :data="tableListData">
<template #empty>
<el-empty description="暂无数据" />
</template>
<el-table-column show-overflow-tooltip label="用户名" prop="userName" />
<el-table-column show-overflow-tooltip label="年度" prop="year" />
<el-table-column show-overflow-tooltip label="月度" prop="month" />
<el-table-column show-overflow-tooltip label="积分" prop="score" />
</el-table>
<Pagination
v-if="pageData.total > 0"
class="mt16 tr"
v-model:pageNo="queryParams.pageNo"
v-model:pageSize="queryParams.pageSize"
:total="pageData.total"
@query="pageList"
/>
</div>
</div>
</div>
</div>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Download } from '@element-plus/icons-vue'
import Pagination from '@/components/Pagination/index.vue'
import { userScoreApi } from '@/api/submit/userScore'
//
import type { UserScoreListReqVO, UserScoreListRespVO } from '@/api/submit/userScore/type'
//
const search = reactive({
row: {} as Partial<UserScoreListRespVO & { userId: string }>, // userId
monthRange: [] as string[]
})
//
const exportLoading = ref(false)
//
const dialogVisible = ref(false)
//
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
userId: '',
beginMonth: undefined as string | undefined,
endMonth: undefined as string | undefined
})
//
const tableListData = ref<UserScoreListRespVO[]>([])
const listLoading = ref(false)
//
const pageData = reactive({
total: 0
})
//
const monthRangeStr = computed(() => {
if (search.monthRange && search.monthRange.length === 2) {
return search.monthRange.join(' 至 ')
}
return '暂无'
})
//
const getFilenameFromResponse = (response: any) => {
//
const disposition = response.headers?.['content-disposition'] || response.headers?.['Content-Disposition']
if (!disposition) return null
// filename=xxx.xlsx filename*=UTF-8''xxx.xlsx
const match = disposition.match(/filename=([^;]+)/i) || disposition.match(/filename\*=UTF-8''([^;]+)/i)
if (match && match[1]) {
//
return decodeURIComponent(match[1].replace(/["']/g, ''))
}
return null
}
//
const pageList = async () => {
tableListData.value = []
listLoading.value = true
try {
const params: UserScoreListReqVO = {
userId: queryParams.userId,
beginMonth: queryParams.beginMonth,
endMonth: queryParams.endMonth,
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize
}
const res = await userScoreApi.getUserScoreList(params)
if (res.code === 200 && res.data) { // res.data
tableListData.value = res.data.list || []
pageData.total = res.data.total || 0
} else {
ElMessage.warning('暂无相关明细数据')
}
} catch (error) {
console.error('月度明细查询失败:', error)
ElMessage.error('查询失败,请稍后重试')
} finally {
listLoading.value = false
}
}
// Excel
const exportExcel = async () => {
exportLoading.value = true
try {
//
const params: UserScoreListReqVO = {
userId: queryParams.userId,
beginMonth: queryParams.beginMonth,
endMonth: queryParams.endMonth
}
// blob
const response = await userScoreApi.exportUserScoreList(params, {
responseType: 'blob'
})
//
if (response instanceof Blob) {
// ExcelBlob
const blob = new Blob([response], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
})
const url = window.URL.createObjectURL(blob)
//
const link = document.createElement('a')
link.href = url
//
const username = search.row.userName || '未知用户'
const filename = getFilenameFromResponse(response) || `${username}_积分月度明细.xlsx`
link.download = filename
//
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} else {
ElMessage.success('导出请求已发送')
}
} catch (error) {
console.error('导出失败:', error)
ElMessage.error('导出失败,请稍后重试')
} finally {
exportLoading.value = false
}
}
//
const open = (data: { row: Partial<UserScoreListRespVO & { userId: string }>, monthRange: string[] }) => {
dialogVisible.value = true
search.row = data.row || {}
search.monthRange = data.monthRange || []
//
queryParams.userId = data.row?.userId || ''
queryParams.beginMonth = data.monthRange?.[0]
queryParams.endMonth = data.monthRange?.[1]
queryParams.pageNo = 1 //
//
pageList()
}
//
const handleClose = () => {
dialogVisible.value = false
//
search.row = {}
search.monthRange = []
//
tableListData.value = []
pageData.total = 0
//
queryParams.userId = ''
queryParams.beginMonth = undefined
queryParams.endMonth = undefined
queryParams.pageNo = 1
}
// open
defineExpose({
open
})
</script>
<style scoped>
.drawer-content {
padding: 0px 40px;
}
.drawer-content .group-name {
color: #A8ABB2;
font-size: 14px;
}
.drawer-content .group-warp {
padding-bottom: 10px;
margin-bottom: 10px;
border-bottom: 1px solid #f5f5f5;
}
.drawer-content .group-warp:last-child {
padding-bottom: 0px;
margin-bottom: 0px;
border-bottom: none;
}
.drawer-content .group-container {
padding: 10px 15px;
background-color: #fafafa;
border-radius: 8px;
}
.table-content {
position: relative;
}
.b-white {
display: flex;
justify-content: flex-end;
margin-bottom: 16px;
}
.btn-icon {
margin-right: 4px;
}
.mt16 {
margin-top: 16px;
}
.tr {
text-align: right;
}
/* 优化描述列表样式 */
.el-descriptions {
font-size: 14px;
}
.el-descriptions-item__label {
font-weight: 500;
color: #666;
}
</style>