feat(settings): add offboarding record list and trace detail

This commit is contained in:
Your Name 2026-02-07 19:38:46 +08:00
parent eae8cbb8e8
commit f041543f02
9 changed files with 570 additions and 12 deletions

View File

@ -1,20 +1,25 @@
package com.designhub.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.designhub.dto.UserOffboardingRecordQuery;
import com.designhub.common.Result;
import com.designhub.dto.UserOffboardingRequest;
import com.designhub.entity.User;
import com.designhub.service.UserOffboardingService;
import com.designhub.service.UserService;
import com.designhub.util.JwtUtil;
import com.designhub.vo.UserOffboardingRecordDetailVO;
import com.designhub.vo.UserOffboardingRecordVO;
import com.designhub.vo.UserOffboardingExecuteVO;
import com.designhub.vo.UserOffboardingPreviewVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@ -196,4 +201,43 @@ public class UserController {
Long operatorId = jwtUtil.getUserIdFromToken(token);
return Result.success(userOffboardingService.execute(request, operatorId));
}
@Operation(summary = "分页查询离职交接记录")
@GetMapping("/offboarding/records")
public Result<Page<UserOffboardingRecordVO>> getOffboardingRecords(
@RequestHeader("Authorization") String authHeader,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Long fromUserId,
@RequestParam(required = false) Long toUserId,
@RequestParam(required = false) Long operatorId,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime
) {
String token = jwtUtil.extractTokenFromHeader(authHeader);
Long currentUserId = jwtUtil.getUserIdFromToken(token);
UserOffboardingRecordQuery query = new UserOffboardingRecordQuery();
query.setPageNum(pageNum);
query.setPageSize(pageSize);
query.setFromUserId(fromUserId);
query.setToUserId(toUserId);
query.setOperatorId(operatorId);
query.setKeyword(keyword);
query.setStartTime(startTime);
query.setEndTime(endTime);
Page<UserOffboardingRecordVO> page = userOffboardingService.getRecordPage(query, currentUserId);
return Result.success(page);
}
@Operation(summary = "获取离职交接记录详情")
@GetMapping("/offboarding/records/{id}")
public Result<UserOffboardingRecordDetailVO> getOffboardingRecordDetail(
@RequestHeader("Authorization") String authHeader,
@PathVariable Long id
) {
String token = jwtUtil.extractTokenFromHeader(authHeader);
Long currentUserId = jwtUtil.getUserIdFromToken(token);
return Result.success(userOffboardingService.getRecordDetail(id, currentUserId));
}
}

View File

@ -0,0 +1,28 @@
package com.designhub.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 离职交接记录查询参数
*/
@Data
public class UserOffboardingRecordQuery {
private Integer pageNum = 1;
private Integer pageSize = 10;
private Long fromUserId;
private Long toUserId;
private Long operatorId;
private String keyword;
private LocalDateTime startTime;
private LocalDateTime endTime;
}

View File

@ -1,9 +1,65 @@
package com.designhub.mapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.designhub.dto.UserOffboardingRecordQuery;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.designhub.entity.TaskTransferLog;
import com.designhub.vo.UserOffboardingRecordDetailVO;
import com.designhub.vo.UserOffboardingRecordVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface TaskTransferLogMapper extends BaseMapper<TaskTransferLog> {
@Select("<script>" +
"SELECT l.id, l.task_id, t.task_no, t.title AS task_title, t.status AS task_status, " +
" l.from_user_id, COALESCE(fu.real_name, fu.username) AS from_user_name, " +
" l.to_user_id, COALESCE(tu.real_name, tu.username) AS to_user_name, " +
" l.operator_id, COALESCE(ou.real_name, ou.username) AS operator_name, " +
" l.reason, l.remark, l.old_deadline, l.new_deadline, l.old_priority, l.new_priority, " +
" l.delay_days, l.penalty_score, l.create_time " +
"FROM task_transfer_log l " +
"LEFT JOIN task t ON t.id = l.task_id " +
"LEFT JOIN sys_user fu ON fu.id = l.from_user_id " +
"LEFT JOIN sys_user tu ON tu.id = l.to_user_id " +
"LEFT JOIN sys_user ou ON ou.id = l.operator_id " +
"WHERE l.transfer_type = 'transfer' " +
" AND l.remark = '离职交接' " +
"<if test='query.fromUserId != null'> AND l.from_user_id = #{query.fromUserId}</if> " +
"<if test='query.toUserId != null'> AND l.to_user_id = #{query.toUserId}</if> " +
"<if test='query.operatorId != null'> AND l.operator_id = #{query.operatorId}</if> " +
"<if test='query.startTime != null'> AND l.create_time &gt;= #{query.startTime}</if> " +
"<if test='query.endTime != null'> AND l.create_time &lt;= #{query.endTime}</if> " +
"<if test='query.keyword != null and query.keyword != \"\"'> " +
"AND (t.task_no LIKE CONCAT('%', #{query.keyword}, '%') " +
" OR t.title LIKE CONCAT('%', #{query.keyword}, '%') " +
" OR l.reason LIKE CONCAT('%', #{query.keyword}, '%') " +
" OR COALESCE(fu.real_name, fu.username, '') LIKE CONCAT('%', #{query.keyword}, '%') " +
" OR COALESCE(tu.real_name, tu.username, '') LIKE CONCAT('%', #{query.keyword}, '%') " +
" OR COALESCE(ou.real_name, ou.username, '') LIKE CONCAT('%', #{query.keyword}, '%')) " +
"</if> " +
"ORDER BY l.create_time DESC, l.id DESC" +
"</script>")
IPage<UserOffboardingRecordVO> selectOffboardingRecordPage(
Page<UserOffboardingRecordVO> page,
@Param("query") UserOffboardingRecordQuery query
);
@Select("SELECT l.id, l.transfer_type, l.task_id, t.task_no, t.title AS task_title, t.status AS task_status, " +
" t.spu_info AS task_spu_info, l.from_user_id, COALESCE(fu.real_name, fu.username) AS from_user_name, " +
" l.to_user_id, COALESCE(tu.real_name, tu.username) AS to_user_name, " +
" l.operator_id, COALESCE(ou.real_name, ou.username) AS operator_name, " +
" l.reason, l.remark, l.old_deadline, l.new_deadline, l.old_priority, l.new_priority, " +
" l.delay_days, l.penalty_score, l.create_time " +
"FROM task_transfer_log l " +
"LEFT JOIN task t ON t.id = l.task_id " +
"LEFT JOIN sys_user fu ON fu.id = l.from_user_id " +
"LEFT JOIN sys_user tu ON tu.id = l.to_user_id " +
"LEFT JOIN sys_user ou ON ou.id = l.operator_id " +
"WHERE l.id = #{id} AND l.transfer_type = 'transfer' AND l.remark = '离职交接' " +
"LIMIT 1")
UserOffboardingRecordDetailVO selectOffboardingRecordDetail(@Param("id") Long id);
}

View File

@ -2,6 +2,8 @@ package com.designhub.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.designhub.dto.UserOffboardingRecordQuery;
import com.designhub.dto.UserOffboardingRequest;
import com.designhub.entity.Task;
import com.designhub.entity.TaskItem;
@ -12,6 +14,8 @@ import com.designhub.mapper.TaskItemMapper;
import com.designhub.mapper.TaskMapper;
import com.designhub.mapper.TaskTransferLogMapper;
import com.designhub.mapper.UserMapper;
import com.designhub.vo.UserOffboardingRecordDetailVO;
import com.designhub.vo.UserOffboardingRecordVO;
import com.designhub.vo.UserOffboardingExecuteVO;
import com.designhub.vo.UserOffboardingPreviewVO;
import lombok.Builder;
@ -54,16 +58,7 @@ public class UserOffboardingService {
@Transactional(rollbackFor = Exception.class)
public UserOffboardingExecuteVO execute(UserOffboardingRequest request, Long operatorId) {
if (operatorId == null) {
throw new BusinessException("操作人不能为空");
}
User operator = userMapper.selectById(operatorId);
if (operator == null) {
throw new BusinessException("操作人不存在");
}
if (operator.getIsAdmin() == null || operator.getIsAdmin() != 1) {
throw new BusinessException("仅管理员可执行离职交接");
}
assertAdminOperator(operatorId, "仅管理员可执行离职交接");
OffboardingContext context = buildContext(request);
LocalDateTime now = LocalDateTime.now();
@ -96,6 +91,61 @@ public class UserOffboardingService {
.build();
}
public Page<UserOffboardingRecordVO> getRecordPage(UserOffboardingRecordQuery query, Long operatorId) {
assertAdminOperator(operatorId, "仅管理员可查看离职交接记录");
UserOffboardingRecordQuery safeQuery = normalizeRecordQuery(query);
Page<UserOffboardingRecordVO> page = new Page<>(safeQuery.getPageNum(), safeQuery.getPageSize());
taskTransferLogMapper.selectOffboardingRecordPage(page, safeQuery);
return page;
}
public UserOffboardingRecordDetailVO getRecordDetail(Long id, Long operatorId) {
assertAdminOperator(operatorId, "仅管理员可查看离职交接记录");
if (id == null) {
throw new BusinessException("记录ID不能为空");
}
UserOffboardingRecordDetailVO detail = taskTransferLogMapper.selectOffboardingRecordDetail(id);
if (detail == null) {
throw new BusinessException("离职交接记录不存在");
}
return detail;
}
private User assertAdminOperator(Long operatorId, String forbiddenMessage) {
if (operatorId == null) {
throw new BusinessException("操作人不能为空");
}
User operator = userMapper.selectById(operatorId);
if (operator == null) {
throw new BusinessException("操作人不存在");
}
if (operator.getIsAdmin() == null || operator.getIsAdmin() != 1) {
throw new BusinessException(forbiddenMessage);
}
return operator;
}
private UserOffboardingRecordQuery normalizeRecordQuery(UserOffboardingRecordQuery query) {
UserOffboardingRecordQuery safeQuery = query == null ? new UserOffboardingRecordQuery() : query;
if (safeQuery.getPageNum() == null || safeQuery.getPageNum() < 1) {
safeQuery.setPageNum(1);
}
if (safeQuery.getPageSize() == null || safeQuery.getPageSize() < 1) {
safeQuery.setPageSize(10);
}
if (safeQuery.getPageSize() > 100) {
safeQuery.setPageSize(100);
}
if (StringUtils.hasText(safeQuery.getKeyword())) {
safeQuery.setKeyword(safeQuery.getKeyword().trim());
}
if (safeQuery.getStartTime() != null && safeQuery.getEndTime() != null
&& safeQuery.getStartTime().isAfter(safeQuery.getEndTime())) {
throw new BusinessException("开始时间不能晚于结束时间");
}
return safeQuery;
}
private OffboardingContext buildContext(UserOffboardingRequest request) {
UserOffboardingRequest safeRequest = normalizeAndValidate(request);
User fromUser = userMapper.selectById(safeRequest.getFromUserId());

View File

@ -0,0 +1,39 @@
package com.designhub.vo;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 离职交接记录详情
*/
@Data
public class UserOffboardingRecordDetailVO {
private Long id;
private String transferType;
private Long taskId;
private String taskNo;
private String taskTitle;
private String taskStatus;
private String taskSpuInfo;
private Long fromUserId;
private String fromUserName;
private Long toUserId;
private String toUserName;
private Long operatorId;
private String operatorName;
private String reason;
private String remark;
private LocalDateTime oldDeadline;
private LocalDateTime newDeadline;
private Integer oldPriority;
private Integer newPriority;
private Integer delayDays;
private Integer penaltyScore;
private LocalDateTime createTime;
}

View File

@ -0,0 +1,37 @@
package com.designhub.vo;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 离职交接记录
*/
@Data
public class UserOffboardingRecordVO {
private Long id;
private Long taskId;
private String taskNo;
private String taskTitle;
private String taskStatus;
private Long fromUserId;
private String fromUserName;
private Long toUserId;
private String toUserName;
private Long operatorId;
private String operatorName;
private String reason;
private String remark;
private LocalDateTime oldDeadline;
private LocalDateTime newDeadline;
private Integer oldPriority;
private Integer newPriority;
private Integer delayDays;
private Integer penaltyScore;
private LocalDateTime createTime;
}

View File

@ -242,3 +242,56 @@ export const executeUserOffboarding = async (
const response = await request.post('/users/offboarding/execute', payload);
return response.data;
};
export interface UserOffboardingRecordItem {
id: number;
taskId: number;
taskNo: string;
taskTitle: string;
taskStatus: string;
fromUserId: number;
fromUserName: string;
toUserId: number;
toUserName: string;
operatorId: number;
operatorName: string;
reason: string;
remark: string;
oldDeadline?: string;
newDeadline?: string;
oldPriority?: number;
newPriority?: number;
delayDays?: number;
penaltyScore?: number;
createTime: string;
}
export interface UserOffboardingRecordDetail extends UserOffboardingRecordItem {
transferType: string;
taskSpuInfo?: string;
}
export interface UserOffboardingRecordQuery {
pageNum?: number;
pageSize?: number;
fromUserId?: number;
toUserId?: number;
operatorId?: number;
keyword?: string;
startTime?: string;
endTime?: string;
}
export const getUserOffboardingRecords = async (
query: UserOffboardingRecordQuery
): Promise<PageResult<UserOffboardingRecordItem>> => {
const response = await request.get('/users/offboarding/records', { params: query });
return response.data;
};
export const getUserOffboardingRecordDetail = async (
id: number
): Promise<UserOffboardingRecordDetail> => {
const response = await request.get(`/users/offboarding/records/${id}`);
return response.data;
};

View File

@ -41,6 +41,7 @@ declare module 'vue' {
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
ARate: typeof import('ant-design-vue/es')['Rate']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']

View File

@ -677,6 +677,136 @@
/>
</div>
</div>
<div class="table-card offboarding-records">
<div class="table-header offboarding-records-header">
<h3 class="chart-title">离职交接记录</h3>
<a-space>
<a-button size="small" @click="resetOffboardingRecordFilters">重置</a-button>
<a-button type="primary" size="small" @click="loadOffboardingRecords(true)">
<template #icon><Icon name="search" size="sm" /></template>
查询
</a-button>
</a-space>
</div>
<div class="offboarding-record-filters">
<a-input
v-model:value="offboardingRecordFilters.keyword"
allow-clear
placeholder="任务编号/标题/操作人/原因"
style="width: 240px;"
@press-enter="loadOffboardingRecords(true)"
/>
<a-select
v-model:value="offboardingRecordFilters.fromUserId"
allow-clear
show-search
:filter-option="filterOffboardingUserOption"
placeholder="离职成员"
style="width: 180px;"
>
<a-select-option v-for="item in offboardingUsers" :key="item.id" :value="item.id">
{{ item.realName || item.username }}
</a-select-option>
</a-select>
<a-select
v-model:value="offboardingRecordFilters.toUserId"
allow-clear
show-search
:filter-option="filterOffboardingUserOption"
placeholder="接替成员"
style="width: 180px;"
>
<a-select-option v-for="item in offboardingUsers" :key="item.id" :value="item.id">
{{ item.realName || item.username }}
</a-select-option>
</a-select>
<a-select
v-model:value="offboardingRecordFilters.operatorId"
allow-clear
show-search
:filter-option="filterOffboardingUserOption"
placeholder="操作人"
style="width: 180px;"
>
<a-select-option v-for="item in offboardingUsers" :key="item.id" :value="item.id">
{{ item.realName || item.username }}
</a-select-option>
</a-select>
<a-range-picker
v-model:value="offboardingRecordDateRange"
show-time
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 340px;"
/>
</div>
<a-table
size="small"
row-key="id"
:columns="offboardingRecordColumns"
:data-source="offboardingRecords"
:loading="offboardingRecordLoading"
:pagination="{
current: offboardingRecordPage.current,
pageSize: offboardingRecordPage.pageSize,
total: offboardingRecordPage.total,
showSizeChanger: true,
showTotal: (total: number) => `共 ${total} 条`,
onChange: handleOffboardingRecordPageChange,
onShowSizeChange: handleOffboardingRecordPageChange
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'taskStatus'">
{{ getTaskStatusLabel(record.taskStatus) }}
</template>
<template v-else-if="column.key === 'createTime'">
{{ formatDateTime(record.createTime) }}
</template>
<template v-else-if="column.key === 'actions'">
<a-button type="link" size="small" @click="openOffboardingRecordDetail(record)">
详情
</a-button>
</template>
</template>
</a-table>
</div>
<a-modal
v-model:open="offboardingDetailVisible"
title="离职交接记录详情"
:footer="null"
width="760px"
>
<a-spin :spinning="offboardingDetailLoading">
<a-empty v-if="!offboardingRecordDetail" description="暂无详情" />
<a-descriptions v-else :column="2" bordered size="small">
<a-descriptions-item label="记录ID">{{ offboardingRecordDetail.id }}</a-descriptions-item>
<a-descriptions-item label="时间">{{ formatDateTime(offboardingRecordDetail.createTime) }}</a-descriptions-item>
<a-descriptions-item label="任务编号">{{ offboardingRecordDetail.taskNo || '--' }}</a-descriptions-item>
<a-descriptions-item label="任务状态">{{ getTaskStatusLabel(offboardingRecordDetail.taskStatus) }}</a-descriptions-item>
<a-descriptions-item label="任务标题" :span="2">{{ offboardingRecordDetail.taskTitle || '--' }}</a-descriptions-item>
<a-descriptions-item label="SPU信息" :span="2">{{ offboardingRecordDetail.taskSpuInfo || '--' }}</a-descriptions-item>
<a-descriptions-item label="离职成员">{{ offboardingRecordDetail.fromUserName || '--' }}</a-descriptions-item>
<a-descriptions-item label="接替成员">{{ offboardingRecordDetail.toUserName || '--' }}</a-descriptions-item>
<a-descriptions-item label="操作人">{{ offboardingRecordDetail.operatorName || '--' }}</a-descriptions-item>
<a-descriptions-item label="变更类型">{{ offboardingRecordDetail.transferType || '--' }}</a-descriptions-item>
<a-descriptions-item label="原截止时间">{{ formatDateTime(offboardingRecordDetail.oldDeadline) }}</a-descriptions-item>
<a-descriptions-item label="新截止时间">{{ formatDateTime(offboardingRecordDetail.newDeadline) }}</a-descriptions-item>
<a-descriptions-item label="原优先级">{{ offboardingRecordDetail.oldPriority ?? '--' }}</a-descriptions-item>
<a-descriptions-item label="新优先级">{{ offboardingRecordDetail.newPriority ?? '--' }}</a-descriptions-item>
<a-descriptions-item label="原因" :span="2">
<div class="offboarding-detail-text">{{ offboardingRecordDetail.reason || '--' }}</div>
</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">
<div class="offboarding-detail-text">{{ offboardingRecordDetail.remark || '--' }}</div>
</a-descriptions-item>
</a-descriptions>
</a-spin>
</a-modal>
</div>
</a-tab-pane>
@ -2991,9 +3121,13 @@ import {
setAdmin,
previewUserOffboarding,
executeUserOffboarding,
getUserOffboardingRecords,
getUserOffboardingRecordDetail,
type User,
type UserOffboardingPreview,
type UserOffboardingRequest
type UserOffboardingRequest,
type UserOffboardingRecordItem,
type UserOffboardingRecordDetail
} from '@/api/user'
import {
getAllS3Configs,
@ -3141,6 +3275,23 @@ const offboardingPreviewLoading = ref(false)
const offboardingExecuting = ref(false)
const offboardingPreview = ref<UserOffboardingPreview | null>(null)
const offboardingPreviewDigest = ref('')
const offboardingRecordLoading = ref(false)
const offboardingRecords = ref<UserOffboardingRecordItem[]>([])
const offboardingRecordPage = reactive({
current: 1,
pageSize: 10,
total: 0
})
const offboardingRecordDateRange = ref<string[]>([])
const offboardingRecordFilters = reactive({
keyword: '',
fromUserId: undefined as number | undefined,
toUserId: undefined as number | undefined,
operatorId: undefined as number | undefined
})
const offboardingDetailVisible = ref(false)
const offboardingDetailLoading = ref(false)
const offboardingRecordDetail = ref<UserOffboardingRecordDetail | null>(null)
const offboardingForm = reactive<UserOffboardingRequest>({
fromUserId: undefined,
toUserId: undefined,
@ -3387,6 +3538,17 @@ const offboardingTaskColumns = [
}
]
const offboardingRecordColumns = [
{ title: '时间', dataIndex: 'createTime', key: 'createTime', width: 170 },
{ title: '任务编号', dataIndex: 'taskNo', key: 'taskNo', width: 170 },
{ title: '任务标题', dataIndex: 'taskTitle', key: 'taskTitle', ellipsis: true },
{ title: '离职成员', dataIndex: 'fromUserName', key: 'fromUserName', width: 130 },
{ title: '接替成员', dataIndex: 'toUserName', key: 'toUserName', width: 130 },
{ title: '操作人', dataIndex: 'operatorName', key: 'operatorName', width: 130 },
{ title: '任务状态', dataIndex: 'taskStatus', key: 'taskStatus', width: 110 },
{ title: '操作', key: 'actions', width: 80, fixed: 'right' }
]
// 过滤出当前部门的用户(用于负责人选择)
const departmentUsers = computed(() => {
// 部门负责人应该可以选择任何用户,不限制于该部门的成员
@ -4341,7 +4503,7 @@ const handleExecuteOffboarding = async () => {
`日志${result.transferLogCount || 0}条`
].join(',')
message.success(`交接执行成功:${summary}`)
await Promise.all([loadDepartmentUsers(), loadDepartments()])
await Promise.all([loadDepartmentUsers(), loadDepartments(), loadOffboardingRecords(true)])
} catch (error: any) {
console.error('离职交接执行失败:', error)
message.error(error?.response?.data?.message || error?.message || '执行失败,请联系管理员')
@ -4350,6 +4512,67 @@ const handleExecuteOffboarding = async () => {
}
}
const loadOffboardingRecords = async (resetPage = false) => {
if (resetPage) {
offboardingRecordPage.current = 1
}
offboardingRecordLoading.value = true
try {
const [startTime, endTime] = offboardingRecordDateRange.value || []
const data = await getUserOffboardingRecords({
pageNum: offboardingRecordPage.current,
pageSize: offboardingRecordPage.pageSize,
keyword: offboardingRecordFilters.keyword?.trim() || undefined,
fromUserId: offboardingRecordFilters.fromUserId,
toUserId: offboardingRecordFilters.toUserId,
operatorId: offboardingRecordFilters.operatorId,
startTime: startTime || undefined,
endTime: endTime || undefined
})
offboardingRecords.value = data.records || []
offboardingRecordPage.total = data.total || 0
offboardingRecordPage.current = data.current || offboardingRecordPage.current
offboardingRecordPage.pageSize = data.size || offboardingRecordPage.pageSize
} catch (error: any) {
console.error('加载离职交接记录失败:', error)
message.error(error?.response?.data?.message || error?.message || '加载离职交接记录失败')
} finally {
offboardingRecordLoading.value = false
}
}
const handleOffboardingRecordPageChange = (page: number, pageSize: number) => {
offboardingRecordPage.current = page
offboardingRecordPage.pageSize = pageSize
loadOffboardingRecords()
}
const resetOffboardingRecordFilters = () => {
offboardingRecordFilters.keyword = ''
offboardingRecordFilters.fromUserId = undefined
offboardingRecordFilters.toUserId = undefined
offboardingRecordFilters.operatorId = undefined
offboardingRecordDateRange.value = []
loadOffboardingRecords(true)
}
const openOffboardingRecordDetail = async (record: UserOffboardingRecordItem) => {
if (!record?.id) {
return
}
offboardingDetailVisible.value = true
offboardingRecordDetail.value = null
offboardingDetailLoading.value = true
try {
offboardingRecordDetail.value = await getUserOffboardingRecordDetail(record.id)
} catch (error: any) {
console.error('加载离职交接详情失败:', error)
message.error(error?.response?.data?.message || error?.message || '加载详情失败')
} finally {
offboardingDetailLoading.value = false
}
}
// 部门搜索处理
const onDepartmentSearch = () => {
// 搜索逻辑通过 computed 属性 filteredDepartments 自动处理
@ -6272,6 +6495,7 @@ watch(activeTab, (value) => {
if (!usersLoaded.value) {
loadDepartmentUsers()
}
loadOffboardingRecords()
}
// 切换到部门管理 tab 时,刷新部门数据以获取最新成员数
if (value === 'departments') {
@ -6408,6 +6632,32 @@ watch(activeTab, (value) => {
}
}
.offboarding-records {
margin-top: 16px;
.offboarding-records-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
}
.offboarding-record-filters {
margin-bottom: 12px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.offboarding-detail-text {
white-space: pre-wrap;
word-break: break-word;
color: $text-primary;
line-height: 1.6;
}
.balance-display {
font-size: $font-size-xl;
color: $color-info;