Merge remote-tracking branch 'origin/feature/20260713/task-module' into feature/20260716/wms-module

This commit is contained in:
2026-07-27 17:56:42 +08:00
13 changed files with 349 additions and 26 deletions

View File

@@ -7,6 +7,8 @@ import cn.code.nl.framework.common.pojo.PageResult;
import cn.code.nl.framework.common.util.object.BeanUtils;
import cn.code.nl.framework.excel.core.util.ExcelUtils;
import cn.code.nl.framework.translate.core.TranslateUtils;
import cn.code.nl.module.system.api.logger.dto.OperateLogPageReqDTO;
import cn.code.nl.module.system.api.logger.dto.OperateLogRespDTO;
import cn.code.nl.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO;
import cn.code.nl.module.system.controller.admin.logger.vo.operatelog.OperateLogRespVO;
import cn.code.nl.module.system.dal.dataobject.logger.OperateLogDO;
@@ -58,6 +60,15 @@ public class OperateLogController {
return success(BeanUtils.toBean(pageResult, OperateLogRespVO.class));
}
@GetMapping("/page2")
@Operation(summary = "查看操作日志分页列表2")
@PreAuthorize("@ss.hasPermission('system:operate-log:query')")
@TransMethodResult
public CommonResult<PageResult<OperateLogRespDTO>> getOperateLogPage(OperateLogPageReqDTO pageReqDTO) {
PageResult<OperateLogDO> operateLogPage = operateLogService.getOperateLogPage(pageReqDTO);
return success(BeanUtils.toBean(operateLogPage, OperateLogRespDTO.class));
}
@Operation(summary = "导出操作日志")
@GetMapping("/export-excel")
@PreAuthorize("@ss.hasPermission('system:operate-log:export')")

View File

@@ -39,6 +39,10 @@ public interface TransportTaskApi {
@Operation(summary = "根据 taskId 下发搬运任务")
CommonResult<Boolean> issueTransportTask(@RequestParam("taskId") Long taskId);
@PostMapping(PREFIX + "/issue-rollback")
@Operation(summary = "根据 taskId 下发搬运任务可回滚")
CommonResult<Boolean> issueTransportTaskRollBack(@RequestParam("taskId") Long taskId);
@GetMapping(PREFIX + "/getTaskById")
@Operation(summary = "根据 taskId 查询任务")
CommonResult<TaskInfoDTO> getTaskById(@RequestParam("taskId") Long taskId);

View File

@@ -17,5 +17,13 @@ public interface ErrorCodeConstants {
ErrorCode TRANSPORT_TASK_OPERATION_NOT_SUPPORTED = new ErrorCode(5006, "不支持的任务操作类型");
ErrorCode TRANSPORT_TASK_ALREADY_FINAL = new ErrorCode(5007, "任务已处于终态,不允许操作");
ErrorCode TRANSPORT_TASK_AUTO_ISSUE_NOT_ALLOW_MANUAL = new ErrorCode(5008, "自动任务不允许手动下发");
ErrorCode TRANSPORT_TASK_OPERATING = new ErrorCode(5009, "任务标识为:{}的任务正在操作中!");
ErrorCode TRANSPORT_TASK_ISSUE_FAILED = new ErrorCode(5010, "任务下发失败,失败原因:{}");
ErrorCode TRANSPORT_TASK_ACS_SERVER_ADDRESS_NOT_CONFIGURED = new ErrorCode(5011, "未配置 ACS 服务地址:{}");
ErrorCode TRANSPORT_TASK_ACS_OPERATE_CHECK_FAILED = new ErrorCode(5012, "ACS操作校验失败");
ErrorCode TRANSPORT_TASK_PRODUCT_AREA_EMPTY = new ErrorCode(5013, "{}生产区域为空,无法获取 ACS 服务地址");
ErrorCode TRANSPORT_TASK_ACS_OPERATE_CHECK_RESULT_EMPTY = new ErrorCode(5014, "ACS 操作校验返回为空");
ErrorCode TRANSPORT_TASK_ACS_OPERATE_NOT_ALLOW = new ErrorCode(5015, "ACS 不允许执行该操作,原因:{}");
ErrorCode TRANSPORT_TASK_OPERATION_FAILED = new ErrorCode(5016, "任务操作失败,失败原因:{}");
}

View File

@@ -44,6 +44,12 @@ public class TransportTaskApiImpl implements TransportTaskApi {
return success(true);
}
@Override
public CommonResult<Boolean> issueTransportTaskRollBack(Long taskId) {
transportTaskService.issueTransportTaskRollBack(taskId);
return success(true);
}
@Override
public CommonResult<TaskInfoDTO> getTaskById(Long taskId) {
return success(transportTaskService.getTaskInfoById(taskId));

View File

@@ -1,6 +1,5 @@
package cn.code.nl.module.task.manage;
import cn.code.nl.framework.common.exception.ServiceException;
import cn.code.nl.framework.common.pojo.CommonResult;
import cn.code.nl.framework.execute.biz.api.TaskCommonApi;
import cn.code.nl.framework.execute.biz.dto.TaskStatusCallApiReqDTO;
@@ -22,6 +21,8 @@ import java.util.EnumMap;
import java.util.Map;
import java.util.function.BiFunction;
import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_OPERATING;
import static cn.code.nl.module.task.framework.common.util.TaskUtil.isAllowedFrom;
/**
@@ -81,7 +82,7 @@ public class TransportTaskBusinessOperationManager {
}
}
} else {
throw new ServiceException(5007, "任务标识为:" + task.getTaskId() + "的任务正在操作中!");
throw exception(TRANSPORT_TASK_OPERATING, task.getTaskId());
}
}

View File

@@ -24,7 +24,11 @@ import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.code.nl.module.task.enums.AcsApiConstants.ACS_TASK_API;
import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_ACS_SERVER_ADDRESS_NOT_CONFIGURED;
import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_ISSUE_FAILED;
import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_PRODUCT_AREA_EMPTY;
/**
* 搬运任务下发管理器
@@ -57,6 +61,29 @@ public class TransportTaskIssueManager {
taskMap.forEach(this::issueProductAreaTasks);
}
/**
* 下发单个任务到 ACS失败时直接抛出异常
*
* @param task 待下发任务
*/
public void issueTask(TransportTaskDO task) {
if (StrUtil.isBlank(task.getProductArea())) {
throw exception(TRANSPORT_TASK_PRODUCT_AREA_EMPTY, "任务下发失败,失败原因:");
}
List<AcsTaskDTO> acsTasks = List.of(AcsTaskUtil.buildAcsTaskDTO(task));
String requestJson = JsonUtils.toJsonString(acsTasks);
try {
String serverAddress = getAcsServerAddress(task.getProductArea());
AcsIssueResultRespDTO result = AcsUtil.post(serverAddress, ACS_TASK_API, acsTasks, AcsIssueResultRespDTO.class);
handleSingleIssueResult(task, result);
} catch (ServiceException ex) {
throw ex;
} catch (Exception ex) {
log.error("下发单个任务失败productArea={}tasks={}", task.getProductArea(), requestJson, ex);
throw exception(TRANSPORT_TASK_ISSUE_FAILED, ex.getMessage());
}
}
/**
* 下发单个生产区域的任务
*
@@ -87,7 +114,7 @@ public class TransportTaskIssueManager {
CommonResult<String> result = configApi.getConfigValueByKey(configKey);
String serverAddress = result.getCheckedData();
if (StrUtil.isBlank(serverAddress)) {
throw new ServiceException(500, "未配置 ACS 服务地址:" + configKey);
throw exception(TRANSPORT_TASK_ACS_SERVER_ADDRESS_NOT_CONFIGURED, configKey);
}
return serverAddress;
}
@@ -125,6 +152,27 @@ public class TransportTaskIssueManager {
}
}
/**
* 处理单个任务的 ACS 下发响应
*
* @param task 本次下发任务
* @param result ACS 响应
*/
private void handleSingleIssueResult(TransportTaskDO task, AcsIssueResultRespDTO result) {
if (result == null) {
throw exception(TRANSPORT_TASK_ISSUE_FAILED, "ACS 返回为空");
}
String resultJson = JsonUtils.toJsonString(result);
if (Boolean.TRUE.equals(result.getSuccess())) {
transportTaskMapper.updateIssueResult(task.getTaskId(), TransportTaskStatusEnum.ISSUED.getCode(), resultJson, null);
return;
}
String errorMessage = CollUtil.isNotEmpty(result.getFailedTasks())
? result.getFailedTasks().get(0).getErrorMessage()
: result.getMsg();
throw exception(TRANSPORT_TASK_ISSUE_FAILED, StrUtil.blankToDefault(errorMessage, "ACS 下发失败"));
}
/**
* 更新任务为下发失败
*

View File

@@ -14,7 +14,13 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.code.nl.module.task.enums.AcsApiConstants.ACS_OPERATE_CHECK_API;
import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_ACS_OPERATE_CHECK_FAILED;
import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_ACS_OPERATE_CHECK_RESULT_EMPTY;
import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_ACS_OPERATE_NOT_ALLOW;
import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_ACS_SERVER_ADDRESS_NOT_CONFIGURED;
import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_PRODUCT_AREA_EMPTY;
/**
* PC 端任务操作 ACS 校验管理器
@@ -50,7 +56,7 @@ public class TransportTaskOperateCheckManager {
throw ex;
} catch (Exception ex) {
log.error("ACS 操作校验请求失败reqDTO={}", JsonUtils.toJsonString(reqDTO), ex);
throw new ServiceException(500, "ACS操作校验失败");
throw exception(TRANSPORT_TASK_ACS_OPERATE_CHECK_FAILED);
}
handleCheckResult(reqDTO, result);
}
@@ -71,13 +77,13 @@ public class TransportTaskOperateCheckManager {
*/
private String getAcsServerAddress(String productArea) {
if (StrUtil.isBlank(productArea)) {
throw new ServiceException(500, "生产区域为空,无法获取 ACS 服务地址");
throw exception(TRANSPORT_TASK_PRODUCT_AREA_EMPTY, "");
}
String configKey = productArea + ACS_SERVER_ADDRESS_CONFIG_SUFFIX;
CommonResult<String> result = configApi.getConfigValueByKey(configKey);
String serverAddress = result.getCheckedData();
if (StrUtil.isBlank(serverAddress)) {
throw new ServiceException(500, "未配置 ACS 服务地址:" + configKey);
throw exception(TRANSPORT_TASK_ACS_SERVER_ADDRESS_NOT_CONFIGURED, configKey);
}
return serverAddress;
}
@@ -99,7 +105,7 @@ public class TransportTaskOperateCheckManager {
*/
private void handleCheckResult(AcsOperateCheckReqDTO reqDTO, AcsOperateCheckRespDTO result) {
if (result == null) {
throw new ServiceException(500, "ACS 操作校验返回为空");
throw exception(TRANSPORT_TASK_ACS_OPERATE_CHECK_RESULT_EMPTY);
}
Boolean enableOperate = getEnableOperate(result);
if (Boolean.TRUE.equals(enableOperate)) {
@@ -108,7 +114,7 @@ public class TransportTaskOperateCheckManager {
String message = getMessage(result);
log.warn("ACS 拒绝 PC 端任务操作reqDTO={}result={}",
JsonUtils.toJsonString(reqDTO), JsonUtils.toJsonString(result));
throw new ServiceException(500, message);
throw exception(TRANSPORT_TASK_ACS_OPERATE_NOT_ALLOW, message);
}
/**

View File

@@ -28,6 +28,8 @@ import java.util.function.BiConsumer;
import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_AUTO_ISSUE_NOT_ALLOW_MANUAL;
import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_OPERATING;
import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_OPERATION_FAILED;
import static cn.code.nl.module.task.enums.ErrorCodeConstants.TRANSPORT_TASK_STATUS_NOT_ALLOW;
import static cn.code.nl.module.task.framework.common.util.TaskUtil.isAllowedFrom;
@@ -86,14 +88,14 @@ public class TransportTaskOperationManager {
throw serviceException;
}
log.error("[messageResend][执行异常][lockKey={}]", task.getTaskId(), ex);
throw new ServiceException(500, ex.getMessage());
throw exception(TRANSPORT_TASK_OPERATION_FAILED, ex.getMessage());
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} else {
throw new ServiceException(5007, "任务标识为:" + task.getTaskId() + "的任务正在操作中!");
throw exception(TRANSPORT_TASK_OPERATING, task.getTaskId());
}
}

View File

@@ -82,6 +82,12 @@ public interface TransportTaskService {
* @param taskId 任务ID
*/
void issueTransportTask(Long taskId);
/**
* 根据任务ID下发搬运任务到 ACS 报错回滚
*
* @param taskId 任务ID
*/
void issueTransportTaskRollBack(Long taskId);
/**
* 根据 taskId 查询任务全量信息RPC 用,自动过滤逻辑删除,查不到返回 null

View File

@@ -184,6 +184,15 @@ public class TransportTaskServiceImpl implements TransportTaskService {
transportTaskOperationManager.dispatchOperation(task, TaskOperationTypeEnum.ISSUE, reqDTO);
}
@Override
public void issueTransportTaskRollBack(Long taskId) {
TransportTaskDO task = transportTaskMapper.selectById(taskId);
if (task == null) {
throw exception(TRANSPORT_TASK_NOT_EXISTS);
}
transportTaskIssueManager.issueTask(task);
}
@Override
public TaskInfoDTO getTaskInfoById(Long taskId) {
TransportTaskDO task = transportTaskMapper.selectById(taskId);

View File

@@ -33,6 +33,14 @@ export function getOperateLogPage(params: PageParam) {
);
}
/** 查询操作日志列表2 */
export function getOperateLogPage2(params: PageParam) {
return requestClient.get<PageResult<SystemOperateLogApi.OperateLog>>(
'/system/operate-log/page2',
{ params },
);
}
/** 导出操作日志 */
export function exportOperateLog(params: any) {
return requestClient.download('/system/operate-log/export-excel', { params });

View File

@@ -5,6 +5,7 @@ import { requestClient } from '#/api/request';
export namespace TaskTransportTaskApi {
/** 搬运任务信息 */
export interface TransportTask {
id?: number | string;
taskId?: string;
taskCode?: string; // 任务编码
taskName: string; // 任务名称
@@ -52,7 +53,7 @@ export function getTransportTaskPage(params: PageParam) {
}
/** 查询搬运任务详情 */
export function getTransportTask(id: number) {
export function getTransportTask(id: number | string) {
return requestClient.get<TaskTransportTaskApi.TransportTask>(
`/task/transport-task/get?id=${id}`,
);
@@ -69,12 +70,12 @@ export function updateTransportTask(data: TaskTransportTaskApi.TransportTask) {
}
/** 删除搬运任务 */
export function deleteTransportTask(id: number) {
export function deleteTransportTask(id: number | string) {
return requestClient.delete(`/task/transport-task/delete?id=${id}`);
}
/** 批量删除搬运任务 */
export function deleteTransportTaskList(ids: number[]) {
export function deleteTransportTaskList(ids: Array<number | string>) {
return requestClient.delete(
`/task/transport-task/delete-list?ids=${ids.join(',')}`,
);

View File

@@ -1,15 +1,28 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemOperateLogApi } from '#/api/system/operate-log';
import type { TaskTransportTaskApi } from '#/api/task/transporttask';
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { confirm, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
import { downloadFileFromBlobPart, formatDateTime, isEmpty } from '@vben/utils';
import { message } from 'antdv-next';
import {
Button,
Descriptions,
DescriptionsItem,
Drawer,
Empty,
message,
Modal,
Spin,
Timeline,
TimelineItem,
} from 'antdv-next';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getOperateLogPage2 } from '#/api/system/operate-log';
import {
deleteTransportTask,
deleteTransportTaskList,
@@ -22,13 +35,15 @@ import { $t } from '#/locales';
import { loadAllLookupData, useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const OPERATE_LOG_PAGE_SIZE = 20;
const ready = ref(false);
onMounted(async () => {
await loadAllLookupData();
ready.value = true;
});
const [FormModal, formModalApi] = useVbenModal({
const [FormModal] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
@@ -38,11 +53,6 @@ function handleRefresh() {
gridApi.query();
}
/** 创建搬运任务 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** PC 端操作搬运任务(完成/取消/强制完成) */
async function handleOperate(
row: TaskTransportTaskApi.TransportTask,
@@ -95,7 +105,7 @@ async function handleDeleteBatch() {
}
}
const checkedIds = ref<number[]>([]);
const checkedIds = ref<Array<number | string>>([]);
function handleRowCheckboxChange({
records,
}: {
@@ -110,6 +120,66 @@ async function handleExport() {
downloadFileFromBlobPart({ fileName: '搬运任务.xls', source: data });
}
const operateLogDrawerOpen = ref(false);
const operateLogLoading = ref(false);
const operateLogPageNo = ref(1);
const operateLogTotal = ref(0);
const currentOperateLogTask = ref<TaskTransportTaskApi.TransportTask>();
const operateLogList = ref<SystemOperateLogApi.OperateLog[]>([]);
const selectedOperateLog = ref<SystemOperateLogApi.OperateLog>();
const operateLogDrawerTitle = computed(() => {
const taskCode = currentOperateLogTask.value?.taskCode;
return taskCode ? `操作记录 - ${taskCode}` : '操作记录';
});
const operateLogHasMore = computed(
() => operateLogList.value.length < operateLogTotal.value,
);
async function loadOperateLogs(reset = false) {
if (operateLogLoading.value) {
return;
}
const taskId = currentOperateLogTask.value?.taskId;
if (!taskId) {
return;
}
if (reset) {
operateLogPageNo.value = 1;
operateLogTotal.value = 0;
operateLogList.value = [];
}
operateLogLoading.value = true;
try {
const data = await getOperateLogPage2({
pageNo: operateLogPageNo.value,
pageSize: OPERATE_LOG_PAGE_SIZE,
type: '任务信息',
bizId: taskId,
});
operateLogList.value = [...operateLogList.value, ...(data.list || [])];
operateLogTotal.value = data.total || 0;
operateLogPageNo.value += 1;
} finally {
operateLogLoading.value = false;
}
}
function handleOperateLog(row: TaskTransportTaskApi.TransportTask) {
currentOperateLogTask.value = row;
selectedOperateLog.value = undefined;
operateLogDrawerOpen.value = true;
loadOperateLogs(true);
}
function handleOperateLogDrawerClose() {
selectedOperateLog.value = undefined;
currentOperateLogTask.value = undefined;
}
function handleOperateLogDetail(log: SystemOperateLogApi.OperateLog) {
selectedOperateLog.value = log;
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
@@ -188,7 +258,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
type: 'link',
icon: ACTION_ICON.CIRCLE_CHECK,
auth: ['task:transport-task:finish'],
disabled: ['79', '85', '89', '99'].includes(row.taskStatus),
disabled: ['79', '85', '89', '99'].includes(row.taskStatus || ''),
onClick: handleOperate.bind(null, row, 'FINISHED', '完成任务'),
},
{
@@ -196,7 +266,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
type: 'link',
icon: ACTION_ICON.CIRCLE_X,
auth: ['task:transport-task:cancel'],
disabled: ['75', '79', '89', '99'].includes(row.taskStatus),
disabled: ['75', '79', '89', '99'].includes(row.taskStatus || ''),
onClick: handleOperate.bind(null, row, 'CANCELLED', '取消任务'),
},
{
@@ -204,9 +274,16 @@ const [Grid, gridApi] = useVbenVxeGrid({
type: 'link',
icon: ACTION_ICON.CANCEL,
auth: ['task:transport-task:cancel'],
disabled: ['79', '89', '99'].includes(row.taskStatus),
disabled: ['79', '89', '99'].includes(row.taskStatus || ''),
onClick: handleOperate.bind(null, row, 'FORCE-FINISH', '强制完成任务'),
},
{
label: '操作记录',
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['system:operate-log:query'],
onClick: handleOperateLog.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
@@ -222,5 +299,141 @@ const [Grid, gridApi] = useVbenVxeGrid({
/>
</template>
</Grid>
<Drawer
v-model:open="operateLogDrawerOpen"
:title="operateLogDrawerTitle"
width="560"
placement="right"
destroy-on-close
@close="handleOperateLogDrawerClose"
>
<Spin :spinning="operateLogLoading && isEmpty(operateLogList)">
<Empty v-if="!operateLogLoading && isEmpty(operateLogList)" description="暂无操作记录" />
<Timeline v-else class="operate-log-timeline">
<TimelineItem v-for="log in operateLogList" :key="log.id">
<div class="operate-log-timeline-row" @click="handleOperateLogDetail(log)">
<span class="operate-log-time">
{{ formatDateTime(log.createTime) }}
</span>
<span class="operate-log-action">{{ log.action || '-' }}</span>
</div>
</TimelineItem>
</Timeline>
</Spin>
<div v-if="!isEmpty(operateLogList)" class="operate-log-load-more">
<Button
v-if="operateLogHasMore"
:loading="operateLogLoading"
block
@click="loadOperateLogs()"
>
加载更多
</Button>
<span v-else class="operate-log-finished">已加载全部记录</span>
</div>
</Drawer>
<Modal
:open="!!selectedOperateLog"
title="操作日志详情"
width="760px"
:footer="null"
@cancel="selectedOperateLog = undefined"
>
<Descriptions
v-if="selectedOperateLog"
bordered
size="small"
:column="1"
class="operate-log-descriptions"
>
<DescriptionsItem label="链路追踪编号">
{{ selectedOperateLog.traceId || '-' }}
</DescriptionsItem>
<DescriptionsItem label="用户名称">
{{ selectedOperateLog.userName || '-' }}
</DescriptionsItem>
<DescriptionsItem label="操作模块类型">
{{ selectedOperateLog.type || '-' }}
</DescriptionsItem>
<DescriptionsItem label="操作模块业务编号">
{{ selectedOperateLog.bizId || '-' }}
</DescriptionsItem>
<DescriptionsItem label="操作内容">
{{ selectedOperateLog.action || '-' }}
</DescriptionsItem>
<DescriptionsItem label="拓展字段">
<pre>{{ selectedOperateLog.extra || '-' }}</pre>
</DescriptionsItem>
<DescriptionsItem label="请求方法名">
{{ selectedOperateLog.requestMethod || '-' }}
</DescriptionsItem>
<DescriptionsItem label="请求地址">
{{ selectedOperateLog.requestUrl || '-' }}
</DescriptionsItem>
<DescriptionsItem label="用户 IP">
{{ selectedOperateLog.userIp || '-' }}
</DescriptionsItem>
<DescriptionsItem label="浏览器 UserAgent">
{{ selectedOperateLog.userAgent || '-' }}
</DescriptionsItem>
<DescriptionsItem label="创建时间">
{{ formatDateTime(selectedOperateLog.createTime) }}
</DescriptionsItem>
</Descriptions>
</Modal>
</Page>
</template>
<style scoped>
.operate-log-timeline {
padding: 8px 4px 0;
}
.operate-log-timeline-row {
display: grid;
grid-template-columns: 156px minmax(0, 1fr);
gap: 12px;
padding: 8px 10px;
margin: -8px 0 8px;
cursor: pointer;
border-radius: 6px;
transition: background-color 0.2s ease;
}
.operate-log-timeline-row:hover {
background-color: hsl(var(--accent));
}
.operate-log-time {
font-size: 13px;
line-height: 22px;
color: hsl(var(--muted-foreground));
white-space: nowrap;
}
.operate-log-action {
min-width: 0;
line-height: 22px;
word-break: break-word;
}
.operate-log-load-more {
padding: 8px 4px 0;
text-align: center;
}
.operate-log-finished {
font-size: 13px;
color: hsl(var(--muted-foreground));
}
.operate-log-descriptions :deep(pre) {
max-height: 180px;
padding: 0;
margin: 0;
overflow: auto;
font-family: inherit;
white-space: pre-wrap;
word-break: break-word;
}
</style>