feat(日志管理): 开发日志管理功能。
fix(项目任务): 1、任务完成后需要依然能够修改工作日志,但是只能修改工作内容和上传附件。2、任务完成后,协办人的工作日志不应该能删除、所有任务里的成员不能新增工作日志,前端不显示新增、删除按钮。3、团队成员的面板,在成员排序时,让有下属的成员提前。4、在任务弹出框有个快速用执行的信息填充的icon。
This commit is contained in:
@@ -209,9 +209,30 @@ export function setupElegantRouter() {
|
|||||||
order: 1,
|
order: 1,
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
},
|
},
|
||||||
|
'infra_log-management': {
|
||||||
|
icon: 'mdi:text-box-search-outline',
|
||||||
|
order: 2,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
'infra_log-management_login-log': {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
},
|
||||||
|
'infra_log-management_operate-log': {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
},
|
||||||
|
'infra_log-management_api-access-log': {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
},
|
||||||
|
'infra_log-management_api-error-log': {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
},
|
||||||
'infra_rd-code': {
|
'infra_rd-code': {
|
||||||
icon: 'mdi:identifier',
|
icon: 'mdi:identifier',
|
||||||
order: 2,
|
order: 3,
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"generatedAt": "2026-06-05T03:08:01.803Z",
|
"generatedAt": "2026-06-25T05:22:43.905Z",
|
||||||
"description": "Frontend visible page resource whitelist for backend route/menu configuration.",
|
"description": "Frontend visible page resource whitelist for backend route/menu configuration.",
|
||||||
"rules": {
|
"rules": {
|
||||||
"directoryComponent": "layout.base",
|
"directoryComponent": "layout.base",
|
||||||
"pageComponentPattern": "view.<routeName>",
|
"pageComponentPattern": "view.<routeName>",
|
||||||
"singlePageComponentPattern": "layout.<layoutName>$view.<routeName>"
|
"singlePageComponentPattern": "layout.<layoutName>$view.<routeName>"
|
||||||
},
|
},
|
||||||
"total": 22,
|
"total": 23,
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"name": "product_list",
|
"name": "product_list",
|
||||||
@@ -338,6 +338,39 @@
|
|||||||
"pageType": "leaf",
|
"pageType": "leaf",
|
||||||
"source": "generated"
|
"source": "generated"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "personal-center_my-application",
|
||||||
|
"path": "/personal-center/my-application",
|
||||||
|
"component": "view.personal-center_my-application",
|
||||||
|
"title": "我的申请",
|
||||||
|
"routeTitle": "personal-center_my-application",
|
||||||
|
"i18nKey": "route.personal-center_my-application",
|
||||||
|
"icon": "mdi:file-document-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 4,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "我的申请",
|
||||||
|
"i18nKey": "route.personal-center_my-application",
|
||||||
|
"icon": "mdi:file-document-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 4,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "personal-center",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "personal-center_my-performance",
|
"name": "personal-center_my-performance",
|
||||||
"path": "/personal-center/my-performance",
|
"path": "/personal-center/my-performance",
|
||||||
@@ -372,15 +405,15 @@
|
|||||||
"source": "generated"
|
"source": "generated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "personal-center_my-application",
|
"name": "personal-center_overtime-application",
|
||||||
"path": "/personal-center/my-application",
|
"path": "/personal-center/overtime-application",
|
||||||
"component": "view.personal-center_my-application",
|
"component": "view.personal-center_overtime-application",
|
||||||
"title": "我的申请",
|
"title": "加班申请",
|
||||||
"routeTitle": "personal-center_my-application",
|
"routeTitle": "personal-center_overtime-application",
|
||||||
"i18nKey": "route.personal-center_my-application",
|
"i18nKey": "route.personal-center_overtime-application",
|
||||||
"icon": "mdi:file-document-outline",
|
"icon": "mdi:clock-plus-outline",
|
||||||
"localIcon": null,
|
"localIcon": null,
|
||||||
"order": 5,
|
"order": 6,
|
||||||
"hideInMenu": false,
|
"hideInMenu": false,
|
||||||
"keepAlive": true,
|
"keepAlive": true,
|
||||||
"activeMenu": null,
|
"activeMenu": null,
|
||||||
@@ -389,11 +422,11 @@
|
|||||||
"redirect": null,
|
"redirect": null,
|
||||||
"props": null,
|
"props": null,
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "我的申请",
|
"title": "加班申请",
|
||||||
"i18nKey": "route.personal-center_my-application",
|
"i18nKey": "route.personal-center_overtime-application",
|
||||||
"icon": "mdi:file-document-outline",
|
"icon": "mdi:clock-plus-outline",
|
||||||
"localIcon": null,
|
"localIcon": null,
|
||||||
"order": 5,
|
"order": 6,
|
||||||
"keepAlive": true,
|
"keepAlive": true,
|
||||||
"hideInMenu": false,
|
"hideInMenu": false,
|
||||||
"activeMenu": null,
|
"activeMenu": null,
|
||||||
@@ -437,39 +470,6 @@
|
|||||||
"pageType": "leaf",
|
"pageType": "leaf",
|
||||||
"source": "generated"
|
"source": "generated"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "personal-center_overtime-application",
|
|
||||||
"path": "/personal-center/overtime-application",
|
|
||||||
"component": "view.personal-center_overtime-application",
|
|
||||||
"title": "加班申请",
|
|
||||||
"routeTitle": "personal-center_overtime-application",
|
|
||||||
"i18nKey": "route.personal-center_overtime-application",
|
|
||||||
"icon": "mdi:clock-plus-outline",
|
|
||||||
"localIcon": null,
|
|
||||||
"order": 6,
|
|
||||||
"hideInMenu": false,
|
|
||||||
"keepAlive": true,
|
|
||||||
"activeMenu": null,
|
|
||||||
"multiTab": false,
|
|
||||||
"fixedIndexInTab": null,
|
|
||||||
"redirect": null,
|
|
||||||
"props": null,
|
|
||||||
"meta": {
|
|
||||||
"title": "加班申请",
|
|
||||||
"i18nKey": "route.personal-center_overtime-application",
|
|
||||||
"icon": "mdi:clock-plus-outline",
|
|
||||||
"localIcon": null,
|
|
||||||
"order": 6,
|
|
||||||
"keepAlive": true,
|
|
||||||
"hideInMenu": false,
|
|
||||||
"activeMenu": null,
|
|
||||||
"multiTab": false,
|
|
||||||
"fixedIndexInTab": null
|
|
||||||
},
|
|
||||||
"parentName": "personal-center",
|
|
||||||
"pageType": "leaf",
|
|
||||||
"source": "generated"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "system_user",
|
"name": "system_user",
|
||||||
"path": "/system/user",
|
"path": "/system/user",
|
||||||
@@ -701,6 +701,39 @@
|
|||||||
"pageType": "leaf",
|
"pageType": "leaf",
|
||||||
"source": "generated"
|
"source": "generated"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "infra_log-management",
|
||||||
|
"path": "/infra/log-management",
|
||||||
|
"component": "view.infra_log-management",
|
||||||
|
"title": "日志管理",
|
||||||
|
"routeTitle": "infra_log-management",
|
||||||
|
"i18nKey": "route.infra_log-management",
|
||||||
|
"icon": "mdi:text-box-search-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 2,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"keepAlive": true,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null,
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "日志管理",
|
||||||
|
"i18nKey": "route.infra_log-management",
|
||||||
|
"icon": "mdi:text-box-search-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 2,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": false,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"parentName": "infra",
|
||||||
|
"pageType": "leaf",
|
||||||
|
"source": "generated"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "infra_rd-code",
|
"name": "infra_rd-code",
|
||||||
"path": "/infra/rd-code",
|
"path": "/infra/rd-code",
|
||||||
@@ -710,7 +743,7 @@
|
|||||||
"i18nKey": "route.infra_rd-code",
|
"i18nKey": "route.infra_rd-code",
|
||||||
"icon": "mdi:identifier",
|
"icon": "mdi:identifier",
|
||||||
"localIcon": null,
|
"localIcon": null,
|
||||||
"order": 2,
|
"order": 3,
|
||||||
"hideInMenu": false,
|
"hideInMenu": false,
|
||||||
"keepAlive": true,
|
"keepAlive": true,
|
||||||
"activeMenu": null,
|
"activeMenu": null,
|
||||||
@@ -723,7 +756,7 @@
|
|||||||
"i18nKey": "route.infra_rd-code",
|
"i18nKey": "route.infra_rd-code",
|
||||||
"icon": "mdi:identifier",
|
"icon": "mdi:identifier",
|
||||||
"localIcon": null,
|
"localIcon": null,
|
||||||
"order": 2,
|
"order": 3,
|
||||||
"keepAlive": true,
|
"keepAlive": true,
|
||||||
"hideInMenu": false,
|
"hideInMenu": false,
|
||||||
"activeMenu": null,
|
"activeMenu": null,
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ function handleConfirm() {
|
|||||||
footer-class="business-form-dialog__footer"
|
footer-class="business-form-dialog__footer"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
>
|
>
|
||||||
|
<template #header="{ close, titleId, titleClass }">
|
||||||
|
<slot name="title" :close="close" :title="props.title" :title-id="titleId" :title-class="titleClass">
|
||||||
|
<span :id="titleId" :class="titleClass">{{ props.title }}</span>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
<ElScrollbar
|
<ElScrollbar
|
||||||
v-if="props.scrollbar"
|
v-if="props.scrollbar"
|
||||||
:max-height="props.maxBodyHeight"
|
:max-height="props.maxBodyHeight"
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ defineProps<Props>();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="business-form-section">
|
<section class="business-form-section">
|
||||||
<h4 class="business-form-section__title">{{ title }}</h4>
|
<h4 class="business-form-section__title">
|
||||||
|
<slot name="title">{{ title }}</slot>
|
||||||
|
</h4>
|
||||||
<slot />
|
<slot />
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
defineOptions({ name: 'SubordinateSelector' });
|
defineOptions({ name: 'SubordinateSelector' });
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -17,6 +19,41 @@ const selectedUserId = defineModel<string | null>('selectedUserId', {
|
|||||||
default: null
|
default: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function sortSubordinateNodes(
|
||||||
|
nodes: Api.SystemManage.MySubordinateTreeNode[] | null | undefined
|
||||||
|
): Api.SystemManage.MySubordinateTreeNode[] | null {
|
||||||
|
if (!nodes?.length) return null;
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
.map((node, index) => ({
|
||||||
|
...node,
|
||||||
|
children: sortSubordinateNodes(node.children),
|
||||||
|
originalIndex: index
|
||||||
|
}))
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftHasChildren = (left.children?.length ?? 0) > 0 ? 1 : 0;
|
||||||
|
const rightHasChildren = (right.children?.length ?? 0) > 0 ? 1 : 0;
|
||||||
|
|
||||||
|
if (leftHasChildren !== rightHasChildren) {
|
||||||
|
return rightHasChildren - leftHasChildren;
|
||||||
|
}
|
||||||
|
|
||||||
|
return left.originalIndex - right.originalIndex;
|
||||||
|
})
|
||||||
|
.map(({ originalIndex: _ignored, ...node }) => node);
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeData = computed<Api.SystemManage.MySubordinateTreeNode[] | null>(() => {
|
||||||
|
if (!props.data) return null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...props.data,
|
||||||
|
children: sortSubordinateNodes(props.data.children)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
function handleNodeClick(node: Api.SystemManage.MySubordinateTreeNode) {
|
function handleNodeClick(node: Api.SystemManage.MySubordinateTreeNode) {
|
||||||
selectedUserId.value = node.userId;
|
selectedUserId.value = node.userId;
|
||||||
}
|
}
|
||||||
@@ -40,7 +77,7 @@ function renderNodeLabel(node: Api.SystemManage.MySubordinateTreeNode) {
|
|||||||
<ElEmpty v-if="!props.data" :image-size="88" :description="props.emptyText" />
|
<ElEmpty v-if="!props.data" :image-size="88" :description="props.emptyText" />
|
||||||
<ElTree
|
<ElTree
|
||||||
v-else
|
v-else
|
||||||
:data="[props.data]"
|
:data="treeData || []"
|
||||||
node-key="userId"
|
node-key="userId"
|
||||||
:current-node-key="selectedUserId || undefined"
|
:current-node-key="selectedUserId || undefined"
|
||||||
:props="{ label: 'userNickname', children: 'children' }"
|
:props="{ label: 'userNickname', children: 'children' }"
|
||||||
|
|||||||
@@ -128,3 +128,51 @@ export const RDMS_OVERTIME_DURATION_DICT_CODE = 'rdms_overtime_duration';
|
|||||||
* 显示名与颜色(hex)均走字典,前端按 level 取色不硬编码。
|
* 显示名与颜色(hex)均走字典,前端按 level 取色不硬编码。
|
||||||
*/
|
*/
|
||||||
export const NOTIFY_MESSAGE_LEVEL_DICT_CODE = 'notify_message_level';
|
export const NOTIFY_MESSAGE_LEVEL_DICT_CODE = 'notify_message_level';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统用户类型字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:系统日志中的 userType
|
||||||
|
* 来源口径:后端 DictTypeConstants.USER_TYPE = user_type
|
||||||
|
*/
|
||||||
|
export const SYSTEM_USER_TYPE_DICT_CODE = 'user_type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统登录日志类型字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:登录日志中的 logType
|
||||||
|
* 来源口径:后端 DictTypeConstants.LOGIN_TYPE = system_login_type
|
||||||
|
*/
|
||||||
|
export const SYSTEM_LOGIN_TYPE_DICT_CODE = 'system_login_type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统登录结果字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:登录日志中的 result
|
||||||
|
* 来源口径:后端 DictTypeConstants.LOGIN_RESULT = system_login_result
|
||||||
|
*/
|
||||||
|
export const SYSTEM_LOGIN_RESULT_DICT_CODE = 'system_login_result';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础设施操作分类字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:API 访问日志中的 operateType
|
||||||
|
* 来源口径:后端 DictTypeConstants.OPERATE_TYPE = infra_operate_type
|
||||||
|
*/
|
||||||
|
export const INFRA_OPERATE_TYPE_DICT_CODE = 'infra_operate_type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 错误日志处理状态字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:API 错误日志中的 processStatus
|
||||||
|
* 来源口径:后端 DictTypeConstants.API_ERROR_LOG_PROCESS_STATUS = infra_api_error_log_process_status
|
||||||
|
*/
|
||||||
|
export const INFRA_API_ERROR_LOG_PROCESS_STATUS_DICT_CODE = 'infra_api_error_log_process_status';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统请求方式字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:操作日志中的 requestMethod
|
||||||
|
* 来源口径:用户明确指定请求方式下拉来自运行时字典 system_request_method
|
||||||
|
*/
|
||||||
|
export const SYSTEM_REQUEST_METHOD_DICT_CODE = 'system_request_method';
|
||||||
|
|||||||
@@ -179,6 +179,11 @@ const local: App.I18n.Schema = {
|
|||||||
'personal-center_pending-approval': 'Pending Approval',
|
'personal-center_pending-approval': 'Pending Approval',
|
||||||
infra: 'Infra',
|
infra: 'Infra',
|
||||||
'infra_state-machine': 'State Machine',
|
'infra_state-machine': 'State Machine',
|
||||||
|
'infra_log-management': 'Log Management',
|
||||||
|
'infra_log-management_login-log': 'Login Log',
|
||||||
|
'infra_log-management_operate-log': 'Operate Log',
|
||||||
|
'infra_log-management_api-access-log': 'API Access Log',
|
||||||
|
'infra_log-management_api-error-log': 'API Error Log',
|
||||||
'infra_rd-code': 'R&D Code',
|
'infra_rd-code': 'R&D Code',
|
||||||
product: 'Product',
|
product: 'Product',
|
||||||
product_list: 'Product List',
|
product_list: 'Product List',
|
||||||
|
|||||||
@@ -179,6 +179,11 @@ const local: App.I18n.Schema = {
|
|||||||
'personal-center_pending-approval': '待我审批',
|
'personal-center_pending-approval': '待我审批',
|
||||||
infra: '基础设施',
|
infra: '基础设施',
|
||||||
'infra_state-machine': '状态机管理',
|
'infra_state-machine': '状态机管理',
|
||||||
|
'infra_log-management': '日志管理',
|
||||||
|
'infra_log-management_login-log': '登录日志',
|
||||||
|
'infra_log-management_operate-log': '操作日志',
|
||||||
|
'infra_log-management_api-access-log': 'API访问日志',
|
||||||
|
'infra_log-management_api-error-log': 'API错误日志',
|
||||||
'infra_rd-code': '研发令号',
|
'infra_rd-code': '研发令号',
|
||||||
product: '产品管理',
|
product: '产品管理',
|
||||||
product_list: '产品列表',
|
product_list: '产品列表',
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
|||||||
500: () => import("@/views/_builtin/500/index.vue"),
|
500: () => import("@/views/_builtin/500/index.vue"),
|
||||||
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
|
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
|
||||||
login: () => import("@/views/_builtin/login/index.vue"),
|
login: () => import("@/views/_builtin/login/index.vue"),
|
||||||
|
"infra_log-management_api-access-log": () => import("@/views/infra/log-management/api-access-log/index.vue"),
|
||||||
|
"infra_log-management_api-error-log": () => import("@/views/infra/log-management/api-error-log/index.vue"),
|
||||||
|
"infra_log-management": () => import("@/views/infra/log-management/index.vue"),
|
||||||
|
"infra_log-management_login-log": () => import("@/views/infra/log-management/login-log/index.vue"),
|
||||||
|
"infra_log-management_operate-log": () => import("@/views/infra/log-management/operate-log/index.vue"),
|
||||||
"infra_rd-code": () => import("@/views/infra/rd-code/index.vue"),
|
"infra_rd-code": () => import("@/views/infra/rd-code/index.vue"),
|
||||||
"infra_state-machine": () => import("@/views/infra/state-machine/index.vue"),
|
"infra_state-machine": () => import("@/views/infra/state-machine/index.vue"),
|
||||||
"metrics_member-efficiency": () => import("@/views/metrics/member-efficiency/index.vue"),
|
"metrics_member-efficiency": () => import("@/views/metrics/member-efficiency/index.vue"),
|
||||||
|
|||||||
@@ -63,6 +63,64 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
order: 20
|
order: 20
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
name: 'infra_log-management',
|
||||||
|
path: '/infra/log-management',
|
||||||
|
component: 'view.infra_log-management',
|
||||||
|
meta: {
|
||||||
|
title: 'infra_log-management',
|
||||||
|
i18nKey: 'route.infra_log-management',
|
||||||
|
icon: 'mdi:text-box-search-outline',
|
||||||
|
order: 2,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'infra_log-management_api-access-log',
|
||||||
|
path: '/infra/log-management/api-access-log',
|
||||||
|
component: 'view.infra_log-management_api-access-log',
|
||||||
|
meta: {
|
||||||
|
title: 'infra_log-management_api-access-log',
|
||||||
|
i18nKey: 'route.infra_log-management_api-access-log',
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'infra_log-management_api-error-log',
|
||||||
|
path: '/infra/log-management/api-error-log',
|
||||||
|
component: 'view.infra_log-management_api-error-log',
|
||||||
|
meta: {
|
||||||
|
title: 'infra_log-management_api-error-log',
|
||||||
|
i18nKey: 'route.infra_log-management_api-error-log',
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'infra_log-management_login-log',
|
||||||
|
path: '/infra/log-management/login-log',
|
||||||
|
component: 'view.infra_log-management_login-log',
|
||||||
|
meta: {
|
||||||
|
title: 'infra_log-management_login-log',
|
||||||
|
i18nKey: 'route.infra_log-management_login-log',
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'infra_log-management_operate-log',
|
||||||
|
path: '/infra/log-management/operate-log',
|
||||||
|
component: 'view.infra_log-management_operate-log',
|
||||||
|
meta: {
|
||||||
|
title: 'infra_log-management_operate-log',
|
||||||
|
i18nKey: 'route.infra_log-management_operate-log',
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'infra_log-management'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'infra_rd-code',
|
name: 'infra_rd-code',
|
||||||
path: '/infra/rd-code',
|
path: '/infra/rd-code',
|
||||||
@@ -71,7 +129,7 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
title: 'infra_rd-code',
|
title: 'infra_rd-code',
|
||||||
i18nKey: 'route.infra_rd-code',
|
i18nKey: 'route.infra_rd-code',
|
||||||
icon: 'mdi:identifier',
|
icon: 'mdi:identifier',
|
||||||
order: 2,
|
order: 3,
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -172,6 +172,11 @@ const routeMap: RouteMap = {
|
|||||||
"500": "/500",
|
"500": "/500",
|
||||||
"iframe-page": "/iframe-page/:url",
|
"iframe-page": "/iframe-page/:url",
|
||||||
"infra": "/infra",
|
"infra": "/infra",
|
||||||
|
"infra_log-management": "/infra/log-management",
|
||||||
|
"infra_log-management_api-access-log": "/infra/log-management/api-access-log",
|
||||||
|
"infra_log-management_api-error-log": "/infra/log-management/api-error-log",
|
||||||
|
"infra_log-management_login-log": "/infra/log-management/login-log",
|
||||||
|
"infra_log-management_operate-log": "/infra/log-management/operate-log",
|
||||||
"infra_rd-code": "/infra/rd-code",
|
"infra_rd-code": "/infra/rd-code",
|
||||||
"infra_state-machine": "/infra/state-machine",
|
"infra_state-machine": "/infra/state-machine",
|
||||||
"login": "/login/:module(pwd-login|reset-pwd)?",
|
"login": "/login/:module(pwd-login|reset-pwd)?",
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ export function createDocumentTitleGuard(router: Router) {
|
|||||||
|
|
||||||
const documentTitle = i18nKey ? $t(i18nKey) : title;
|
const documentTitle = i18nKey ? $t(i18nKey) : title;
|
||||||
|
|
||||||
useTitle(documentTitle);
|
useTitle(`研发管理系统 - ${documentTitle}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ export * from './project-group';
|
|||||||
export * from './project-shared';
|
export * from './project-shared';
|
||||||
export * from './route';
|
export * from './route';
|
||||||
export * from './system-manage';
|
export * from './system-manage';
|
||||||
|
export * from './system-log';
|
||||||
export * from './work-report';
|
export * from './work-report';
|
||||||
|
|||||||
260
src/service/api/system-log.ts
Normal file
260
src/service/api/system-log.ts
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
import { request } from '../request';
|
||||||
|
import {
|
||||||
|
type ServiceRequestResult,
|
||||||
|
mapServiceResult,
|
||||||
|
normalizeNullableStringId,
|
||||||
|
normalizeStringId,
|
||||||
|
safeJsonRequestConfig
|
||||||
|
} from './shared';
|
||||||
|
|
||||||
|
const LOGIN_LOG_PREFIX = `${SYSTEM_SERVICE_PREFIX}/login-log`;
|
||||||
|
const OPERATE_LOG_PREFIX = `${SYSTEM_SERVICE_PREFIX}/operate-log`;
|
||||||
|
const API_ACCESS_LOG_PREFIX = `${SYSTEM_SERVICE_PREFIX}/api-access-log`;
|
||||||
|
const API_ERROR_LOG_PREFIX = `${SYSTEM_SERVICE_PREFIX}/api-error-log`;
|
||||||
|
|
||||||
|
type StringIdResponse = string | number;
|
||||||
|
|
||||||
|
type LoginLogResponse = Omit<Api.SystemLog.Login.Log, 'id' | 'userId'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
userId?: StringIdResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OperateLogResponse = Omit<Api.SystemLog.Operate.Log, 'id' | 'userId' | 'bizId'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
userId: StringIdResponse;
|
||||||
|
bizId?: StringIdResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApiAccessLogResponse = Omit<Api.SystemLog.ApiAccess.Log, 'id' | 'userId'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
userId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApiErrorLogResponse = Omit<Api.SystemLog.ApiError.Log, 'id' | 'userId' | 'processUserId'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
userId: StringIdResponse;
|
||||||
|
processUserId?: StringIdResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LoginLogPageResponse = Api.SystemLog.Common.PageResult<LoginLogResponse>;
|
||||||
|
type OperateLogPageResponse = Api.SystemLog.Common.PageResult<OperateLogResponse>;
|
||||||
|
type ApiAccessLogPageResponse = Api.SystemLog.Common.PageResult<ApiAccessLogResponse>;
|
||||||
|
type ApiErrorLogPageResponse = Api.SystemLog.Common.PageResult<ApiErrorLogResponse>;
|
||||||
|
|
||||||
|
function appendValue(query: URLSearchParams, key: string, value: unknown) {
|
||||||
|
if (value === null || value === undefined || value === '') return;
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach(item => appendValue(query, key, item));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
query.append(key, String(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQuery(params: Record<string, unknown> = {}) {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
appendValue(query, key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return query.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLoginLog(log: LoginLogResponse): Api.SystemLog.Login.Log {
|
||||||
|
return {
|
||||||
|
...log,
|
||||||
|
id: normalizeStringId(log.id),
|
||||||
|
userId: normalizeNullableStringId(log.userId),
|
||||||
|
traceId: log.traceId ?? null,
|
||||||
|
userType: log.userType ?? null,
|
||||||
|
userAgent: log.userAgent ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOperateLog(log: OperateLogResponse): Api.SystemLog.Operate.Log {
|
||||||
|
return {
|
||||||
|
...log,
|
||||||
|
id: normalizeStringId(log.id),
|
||||||
|
userId: normalizeStringId(log.userId),
|
||||||
|
bizId: normalizeNullableStringId(log.bizId),
|
||||||
|
traceId: log.traceId ?? null,
|
||||||
|
action: log.action ?? null,
|
||||||
|
extra: log.extra ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeApiAccessLog(log: ApiAccessLogResponse): Api.SystemLog.ApiAccess.Log {
|
||||||
|
return {
|
||||||
|
...log,
|
||||||
|
id: normalizeStringId(log.id),
|
||||||
|
userId: normalizeStringId(log.userId),
|
||||||
|
traceId: log.traceId ?? null,
|
||||||
|
requestParams: log.requestParams ?? null,
|
||||||
|
responseBody: log.responseBody ?? null,
|
||||||
|
operateModule: log.operateModule ?? null,
|
||||||
|
operateName: log.operateName ?? null,
|
||||||
|
operateType: log.operateType ?? null,
|
||||||
|
resultMsg: log.resultMsg ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeApiErrorLog(log: ApiErrorLogResponse): Api.SystemLog.ApiError.Log {
|
||||||
|
return {
|
||||||
|
...log,
|
||||||
|
id: normalizeStringId(log.id),
|
||||||
|
userId: normalizeStringId(log.userId),
|
||||||
|
traceId: log.traceId ?? null,
|
||||||
|
requestParams: log.requestParams ?? null,
|
||||||
|
exceptionRootCauseMessage: log.exceptionRootCauseMessage ?? null,
|
||||||
|
exceptionStackTrace: log.exceptionStackTrace ?? null,
|
||||||
|
exceptionClassName: log.exceptionClassName ?? null,
|
||||||
|
exceptionFileName: log.exceptionFileName ?? null,
|
||||||
|
exceptionMethodName: log.exceptionMethodName ?? null,
|
||||||
|
exceptionLineNumber: log.exceptionLineNumber ?? null,
|
||||||
|
processTime: log.processTime ?? null,
|
||||||
|
processUserId: normalizeNullableStringId(log.processUserId)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetLoginLogPage(params?: Api.SystemLog.Login.SearchParams) {
|
||||||
|
const query = buildQuery((params ?? {}) as Record<string, unknown>);
|
||||||
|
const result = await request<LoginLogPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${LOGIN_LOG_PREFIX}/page?${query}` : `${LOGIN_LOG_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<LoginLogPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeLoginLog)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetLoginLog(id: string) {
|
||||||
|
const result = await request<LoginLogResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${LOGIN_LOG_PREFIX}/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<LoginLogResponse>, normalizeLoginLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportLoginLog(params: Api.SystemLog.Login.SearchParams = {}) {
|
||||||
|
const query = buildQuery(params as Record<string, unknown>);
|
||||||
|
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${LOGIN_LOG_PREFIX}/export-excel?${query}` : `${LOGIN_LOG_PREFIX}/export-excel`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetOperateLogPage(params?: Api.SystemLog.Operate.SearchParams) {
|
||||||
|
const query = buildQuery((params ?? {}) as Record<string, unknown>);
|
||||||
|
const result = await request<OperateLogPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${OPERATE_LOG_PREFIX}/page?${query}` : `${OPERATE_LOG_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<OperateLogPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeOperateLog)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetOperateLog(id: string) {
|
||||||
|
const result = await request<OperateLogResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OPERATE_LOG_PREFIX}/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<OperateLogResponse>, normalizeOperateLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportOperateLog(params: Api.SystemLog.Operate.SearchParams = {}) {
|
||||||
|
const query = buildQuery(params as Record<string, unknown>);
|
||||||
|
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${OPERATE_LOG_PREFIX}/export-excel?${query}` : `${OPERATE_LOG_PREFIX}/export-excel`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetApiAccessLogPage(params?: Api.SystemLog.ApiAccess.SearchParams) {
|
||||||
|
const query = buildQuery((params ?? {}) as Record<string, unknown>);
|
||||||
|
const result = await request<ApiAccessLogPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${API_ACCESS_LOG_PREFIX}/page?${query}` : `${API_ACCESS_LOG_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ApiAccessLogPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeApiAccessLog)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetApiAccessLog(id: string) {
|
||||||
|
const result = await request<ApiAccessLogResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${API_ACCESS_LOG_PREFIX}/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ApiAccessLogResponse>, normalizeApiAccessLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportApiAccessLog(params: Api.SystemLog.ApiAccess.SearchParams = {}) {
|
||||||
|
const query = buildQuery(params as Record<string, unknown>);
|
||||||
|
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${API_ACCESS_LOG_PREFIX}/export-excel?${query}` : `${API_ACCESS_LOG_PREFIX}/export-excel`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetApiErrorLogPage(params?: Api.SystemLog.ApiError.SearchParams) {
|
||||||
|
const query = buildQuery((params ?? {}) as Record<string, unknown>);
|
||||||
|
const result = await request<ApiErrorLogPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${API_ERROR_LOG_PREFIX}/page?${query}` : `${API_ERROR_LOG_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ApiErrorLogPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeApiErrorLog)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetApiErrorLog(id: string) {
|
||||||
|
const result = await request<ApiErrorLogResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${API_ERROR_LOG_PREFIX}/get`,
|
||||||
|
method: 'get',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ApiErrorLogResponse>, normalizeApiErrorLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportApiErrorLog(params: Api.SystemLog.ApiError.SearchParams = {}) {
|
||||||
|
const query = buildQuery(params as Record<string, unknown>);
|
||||||
|
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${API_ERROR_LOG_PREFIX}/export-excel?${query}` : `${API_ERROR_LOG_PREFIX}/export-excel`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ export const useAppStore = defineStore(SetupStoreId.App, () => {
|
|||||||
|
|
||||||
const documentTitle = i18nKey ? $t(i18nKey) : title;
|
const documentTitle = i18nKey ? $t(i18nKey) : title;
|
||||||
|
|
||||||
useTitle(documentTitle);
|
useTitle(`研发管理系统 - ${documentTitle}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
|||||||
151
src/typings/api/system-log.d.ts
vendored
Normal file
151
src/typings/api/system-log.d.ts
vendored
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
declare namespace Api {
|
||||||
|
/**
|
||||||
|
* namespace SystemLog
|
||||||
|
*
|
||||||
|
* backend api module: "system/*-log"
|
||||||
|
*/
|
||||||
|
namespace SystemLog {
|
||||||
|
namespace Common {
|
||||||
|
interface PageParams {
|
||||||
|
pageNo: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageResult<T = any> {
|
||||||
|
total: number;
|
||||||
|
list: T[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Login {
|
||||||
|
interface Log {
|
||||||
|
id: string;
|
||||||
|
logType: number;
|
||||||
|
userId?: string | null;
|
||||||
|
userType?: number | null;
|
||||||
|
traceId?: string | null;
|
||||||
|
username: string;
|
||||||
|
result: number;
|
||||||
|
userIp: string;
|
||||||
|
userAgent?: string | null;
|
||||||
|
createTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchParams = CommonType.RecordNullable<
|
||||||
|
Common.PageParams & {
|
||||||
|
userIp: string;
|
||||||
|
username: string;
|
||||||
|
status: boolean;
|
||||||
|
createTime: string[];
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Operate {
|
||||||
|
interface Log {
|
||||||
|
id: string;
|
||||||
|
traceId?: string | null;
|
||||||
|
userId: string;
|
||||||
|
userName: string;
|
||||||
|
userType: number;
|
||||||
|
type: string;
|
||||||
|
subType: string;
|
||||||
|
bizId?: string | null;
|
||||||
|
action?: string | null;
|
||||||
|
extra?: string | null;
|
||||||
|
requestMethod: string;
|
||||||
|
requestUrl: string;
|
||||||
|
userIp: string;
|
||||||
|
userAgent: string;
|
||||||
|
createTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchParams = CommonType.RecordNullable<
|
||||||
|
Common.PageParams & {
|
||||||
|
userId: string;
|
||||||
|
type: string;
|
||||||
|
requestMethod: string;
|
||||||
|
subType: string;
|
||||||
|
action: string;
|
||||||
|
createTime: string[];
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ApiAccess {
|
||||||
|
interface Log {
|
||||||
|
id: string;
|
||||||
|
traceId?: string | null;
|
||||||
|
userId: string;
|
||||||
|
userType: number;
|
||||||
|
applicationName: string;
|
||||||
|
requestMethod: string;
|
||||||
|
requestUrl: string;
|
||||||
|
requestParams?: string | null;
|
||||||
|
responseBody?: string | null;
|
||||||
|
userIp: string;
|
||||||
|
userAgent: string;
|
||||||
|
operateModule?: string | null;
|
||||||
|
operateName?: string | null;
|
||||||
|
operateType?: number | null;
|
||||||
|
beginTime: string;
|
||||||
|
endTime: string;
|
||||||
|
duration: number;
|
||||||
|
resultCode: number;
|
||||||
|
resultMsg?: string | null;
|
||||||
|
createTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchParams = CommonType.RecordNullable<
|
||||||
|
Common.PageParams & {
|
||||||
|
userId: string;
|
||||||
|
userType: number;
|
||||||
|
applicationName: string;
|
||||||
|
requestUrl: string;
|
||||||
|
beginTime: string[];
|
||||||
|
duration: number;
|
||||||
|
resultCode: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ApiError {
|
||||||
|
interface Log {
|
||||||
|
id: string;
|
||||||
|
traceId?: string | null;
|
||||||
|
userId: string;
|
||||||
|
userType: number;
|
||||||
|
applicationName: string;
|
||||||
|
requestMethod: string;
|
||||||
|
requestUrl: string;
|
||||||
|
requestParams?: string | null;
|
||||||
|
userIp: string;
|
||||||
|
userAgent: string;
|
||||||
|
exceptionTime: string;
|
||||||
|
exceptionName: string;
|
||||||
|
exceptionMessage: string;
|
||||||
|
exceptionRootCauseMessage?: string | null;
|
||||||
|
exceptionStackTrace?: string | null;
|
||||||
|
exceptionClassName?: string | null;
|
||||||
|
exceptionFileName?: string | null;
|
||||||
|
exceptionMethodName?: string | null;
|
||||||
|
exceptionLineNumber?: number | null;
|
||||||
|
processStatus: number;
|
||||||
|
processTime?: string | null;
|
||||||
|
processUserId?: string | null;
|
||||||
|
createTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchParams = CommonType.RecordNullable<
|
||||||
|
Common.PageParams & {
|
||||||
|
userId: string;
|
||||||
|
userType: number;
|
||||||
|
applicationName: string;
|
||||||
|
requestUrl: string;
|
||||||
|
exceptionTime: string[];
|
||||||
|
processStatus: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/typings/elegant-router.d.ts
vendored
10
src/typings/elegant-router.d.ts
vendored
@@ -26,6 +26,11 @@ declare module "@elegant-router/types" {
|
|||||||
"500": "/500";
|
"500": "/500";
|
||||||
"iframe-page": "/iframe-page/:url";
|
"iframe-page": "/iframe-page/:url";
|
||||||
"infra": "/infra";
|
"infra": "/infra";
|
||||||
|
"infra_log-management": "/infra/log-management";
|
||||||
|
"infra_log-management_api-access-log": "/infra/log-management/api-access-log";
|
||||||
|
"infra_log-management_api-error-log": "/infra/log-management/api-error-log";
|
||||||
|
"infra_log-management_login-log": "/infra/log-management/login-log";
|
||||||
|
"infra_log-management_operate-log": "/infra/log-management/operate-log";
|
||||||
"infra_rd-code": "/infra/rd-code";
|
"infra_rd-code": "/infra/rd-code";
|
||||||
"infra_state-machine": "/infra/state-machine";
|
"infra_state-machine": "/infra/state-machine";
|
||||||
"login": "/login/:module(pwd-login|reset-pwd)?";
|
"login": "/login/:module(pwd-login|reset-pwd)?";
|
||||||
@@ -138,6 +143,11 @@ declare module "@elegant-router/types" {
|
|||||||
| "500"
|
| "500"
|
||||||
| "iframe-page"
|
| "iframe-page"
|
||||||
| "login"
|
| "login"
|
||||||
|
| "infra_log-management_api-access-log"
|
||||||
|
| "infra_log-management_api-error-log"
|
||||||
|
| "infra_log-management"
|
||||||
|
| "infra_log-management_login-log"
|
||||||
|
| "infra_log-management_operate-log"
|
||||||
| "infra_rd-code"
|
| "infra_rd-code"
|
||||||
| "infra_state-machine"
|
| "infra_state-machine"
|
||||||
| "metrics_member-efficiency"
|
| "metrics_member-efficiency"
|
||||||
|
|||||||
238
src/views/infra/log-management/api-access-log/index.vue
Normal file
238
src/views/infra/log-management/api-access-log/index.vue
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import { INFRA_OPERATE_TYPE_DICT_CODE } from '@/constants/dict';
|
||||||
|
import { fetchExportApiAccessLog, fetchGetApiAccessLog, fetchGetApiAccessLogPage } from '@/service/api';
|
||||||
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||||
|
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||||
|
import DictText from '@/components/custom/dict-text.vue';
|
||||||
|
import LogDetailDialog from '../shared/log-detail-dialog.vue';
|
||||||
|
import {
|
||||||
|
type LogDetailSection,
|
||||||
|
LogPermission,
|
||||||
|
downloadBlob,
|
||||||
|
formatDateTime,
|
||||||
|
formatDuration,
|
||||||
|
getLogExportFileName
|
||||||
|
} from '../shared';
|
||||||
|
import ApiAccessLogSearch from './modules/search.vue';
|
||||||
|
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ApiAccessLogTab' });
|
||||||
|
|
||||||
|
type ApiAccessLogPageResponse = Awaited<ReturnType<typeof fetchGetApiAccessLogPage>>;
|
||||||
|
|
||||||
|
function createSearchParams(): Api.SystemLog.ApiAccess.SearchParams {
|
||||||
|
return {
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
userId: undefined,
|
||||||
|
userType: undefined,
|
||||||
|
applicationName: undefined,
|
||||||
|
requestUrl: undefined,
|
||||||
|
beginTime: undefined,
|
||||||
|
duration: undefined,
|
||||||
|
resultCode: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformPageResult(response: ApiAccessLogPageResponse, pageNo: number, pageSize: number) {
|
||||||
|
if (!response.error && response.data) {
|
||||||
|
return {
|
||||||
|
data: response.data.list,
|
||||||
|
pageNum: pageNo,
|
||||||
|
pageSize,
|
||||||
|
total: response.data.total
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize,
|
||||||
|
total: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
const searchParams = reactive(createSearchParams());
|
||||||
|
const detailVisible = ref(false);
|
||||||
|
const currentRow = ref<Api.SystemLog.ApiAccess.Log | null>(null);
|
||||||
|
const exporting = ref(false);
|
||||||
|
const canExport = computed(() => hasAuth(LogPermission.ApiAccessExport));
|
||||||
|
|
||||||
|
const detailSections: LogDetailSection[] = [
|
||||||
|
{
|
||||||
|
title: '请求信息',
|
||||||
|
fields: [
|
||||||
|
{ label: '日志编号', key: 'id' },
|
||||||
|
{ label: '链路追踪编号', key: 'traceId' },
|
||||||
|
{ label: '应用名', key: 'applicationName' },
|
||||||
|
{ label: '请求方式', key: 'requestMethod' },
|
||||||
|
{ label: '请求地址', key: 'requestUrl', span: 2 },
|
||||||
|
{ label: '开始时间', key: 'beginTime', type: 'datetime' },
|
||||||
|
{ label: '结束时间', key: 'endTime', type: 'datetime' },
|
||||||
|
{ label: '执行时长', formatter: detail => formatDuration(detail.duration) },
|
||||||
|
{ label: '结果码', key: 'resultCode' },
|
||||||
|
{ label: '结果提示', key: 'resultMsg', span: 2 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '业务上下文',
|
||||||
|
fields: [
|
||||||
|
{ label: '用户编号', key: 'userId' },
|
||||||
|
{ label: '操作模块', key: 'operateModule' },
|
||||||
|
{ label: '操作名', key: 'operateName' },
|
||||||
|
{ label: '操作分类', key: 'operateType', type: 'dict', dictCode: INFRA_OPERATE_TYPE_DICT_CODE },
|
||||||
|
{ label: '用户IP', key: 'userIp' },
|
||||||
|
{ label: '浏览器UA', key: 'userAgent', type: 'multiline' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '报文内容',
|
||||||
|
fields: [
|
||||||
|
{ label: '请求参数', key: 'requestParams', type: 'multiline' },
|
||||||
|
{ label: '响应结果', key: 'responseBody', type: 'multiline' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
||||||
|
ApiAccessLogPageResponse,
|
||||||
|
Api.SystemLog.ApiAccess.Log
|
||||||
|
>({
|
||||||
|
paginationProps: {
|
||||||
|
currentPage: searchParams.pageNo,
|
||||||
|
pageSize: searchParams.pageSize
|
||||||
|
},
|
||||||
|
api: () => fetchGetApiAccessLogPage(searchParams),
|
||||||
|
transform: response => transformPageResult(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||||
|
onPaginationParamsChange: params => {
|
||||||
|
searchParams.pageNo = params.currentPage ?? 1;
|
||||||
|
searchParams.pageSize = params.pageSize ?? 10;
|
||||||
|
},
|
||||||
|
columns: () => [
|
||||||
|
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||||
|
{ prop: 'applicationName', label: '应用名', minWidth: 140, showOverflowTooltip: true },
|
||||||
|
{ prop: 'requestMethod', label: '请求方式', width: 100, align: 'center' },
|
||||||
|
{ prop: 'requestUrl', label: '请求地址', minWidth: 220, showOverflowTooltip: true },
|
||||||
|
{ prop: 'operateModule', label: '操作模块', minWidth: 140, showOverflowTooltip: true },
|
||||||
|
{ prop: 'operateName', label: '操作名', minWidth: 140, showOverflowTooltip: true },
|
||||||
|
{
|
||||||
|
prop: 'operateType',
|
||||||
|
label: '操作分类',
|
||||||
|
minWidth: 120,
|
||||||
|
formatter: row => <DictText dictCode={INFRA_OPERATE_TYPE_DICT_CODE} value={row.operateType} />
|
||||||
|
},
|
||||||
|
{ prop: 'resultCode', label: '结果码', width: 100, align: 'center' },
|
||||||
|
{ prop: 'duration', label: '执行时长', width: 120, formatter: row => formatDuration(row.duration) },
|
||||||
|
{ prop: 'beginTime', label: '请求时间', minWidth: 180, formatter: row => formatDateTime(row.beginTime) },
|
||||||
|
{
|
||||||
|
prop: 'operate',
|
||||||
|
label: '操作',
|
||||||
|
width: 90,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
function getRowActions(row: Api.SystemLog.ApiAccess.Log): BusinessTableAction[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'view',
|
||||||
|
label: '查看',
|
||||||
|
buttonType: 'primary',
|
||||||
|
icon: IconMdiEyeOutline,
|
||||||
|
onClick: () => openDetail(row)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(row: Api.SystemLog.ApiAccess.Log) {
|
||||||
|
currentRow.value = row;
|
||||||
|
detailVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadTable(page = searchParams.pageNo) {
|
||||||
|
await getDataByPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSearchParams() {
|
||||||
|
Object.assign(searchParams, createSearchParams());
|
||||||
|
reloadTable(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
reloadTable(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExport() {
|
||||||
|
exporting.value = true;
|
||||||
|
const { error, data: blob } = await fetchExportApiAccessLog(searchParams);
|
||||||
|
exporting.value = false;
|
||||||
|
|
||||||
|
if (error || !blob) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadBlob(blob, getLogExportFileName('API访问日志'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||||
|
<ApiAccessLogSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
||||||
|
|
||||||
|
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between gap-12px">
|
||||||
|
<div class="flex items-center gap-10px">
|
||||||
|
<p>API访问日志列表</p>
|
||||||
|
<ElTag effect="plain">{{ mobilePagination.total || data.length }}</ElTag>
|
||||||
|
</div>
|
||||||
|
<TableHeaderOperation
|
||||||
|
v-model:columns="columnChecks"
|
||||||
|
:disabled-delete="true"
|
||||||
|
:loading="loading"
|
||||||
|
@refresh="getData"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElButton v-if="canExport" plain type="primary" :loading="exporting" @click="handleExport">
|
||||||
|
<template #icon>
|
||||||
|
<icon-mdi-download class="text-icon" />
|
||||||
|
</template>
|
||||||
|
导出
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</TableHeaderOperation>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<ElTable v-loading="loading" height="100%" border row-key="id" :data="data">
|
||||||
|
<ElTableColumn v-for="col in columns" :key="String(col.prop)" v-bind="col" />
|
||||||
|
</ElTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-20px flex justify-end">
|
||||||
|
<ElPagination
|
||||||
|
v-if="mobilePagination.total"
|
||||||
|
layout="total,prev,pager,next,sizes"
|
||||||
|
v-bind="mobilePagination"
|
||||||
|
@current-change="mobilePagination['current-change']"
|
||||||
|
@size-change="mobilePagination['size-change']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
|
||||||
|
<LogDetailDialog
|
||||||
|
v-model:visible="detailVisible"
|
||||||
|
title="API访问日志详情"
|
||||||
|
:row-data="currentRow"
|
||||||
|
:sections="detailSections"
|
||||||
|
:fetch-detail="fetchGetApiAccessLog"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { SYSTEM_USER_TYPE_DICT_CODE } from '@/constants/dict';
|
||||||
|
import TableSearchFields, { type SearchField } from '@/components/custom/table-search-fields.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ApiAccessLogSearch' });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
reset: [];
|
||||||
|
search: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const model = defineModel<Api.SystemLog.ApiAccess.SearchParams>('model', { required: true });
|
||||||
|
|
||||||
|
const fields = computed<SearchField[]>(() => [
|
||||||
|
{
|
||||||
|
key: 'applicationName',
|
||||||
|
label: '应用名',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '请输入应用名'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'requestUrl',
|
||||||
|
label: '请求地址',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '请输入请求地址'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'resultCode',
|
||||||
|
label: '结果码',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '请输入结果码',
|
||||||
|
transformValue: value => {
|
||||||
|
const text = String(value ?? '').trim();
|
||||||
|
if (!text) return undefined;
|
||||||
|
const resultCode = Number(text);
|
||||||
|
return Number.isFinite(resultCode) ? resultCode : undefined;
|
||||||
|
},
|
||||||
|
resolveValue: value => (value === null || value === undefined ? '' : String(value))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'duration',
|
||||||
|
label: '最小时长(ms)',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '请输入执行时长下限',
|
||||||
|
transformValue: value => {
|
||||||
|
const text = String(value ?? '').trim();
|
||||||
|
if (!text) return undefined;
|
||||||
|
const duration = Number(text);
|
||||||
|
return Number.isFinite(duration) ? duration : undefined;
|
||||||
|
},
|
||||||
|
resolveValue: value => (value === null || value === undefined ? '' : String(value))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'userId',
|
||||||
|
label: '用户编号',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '请输入用户编号'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'userType',
|
||||||
|
label: '用户类型',
|
||||||
|
type: 'dict',
|
||||||
|
placeholder: '请选择用户类型',
|
||||||
|
dictCode: SYSTEM_USER_TYPE_DICT_CODE,
|
||||||
|
filterable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'beginTime',
|
||||||
|
label: '请求时间',
|
||||||
|
type: 'dateRange',
|
||||||
|
placeholder: '请选择请求时间',
|
||||||
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
format: 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TableSearchFields v-model="model" :fields="fields" :columns="4" @reset="emit('reset')" @search="emit('search')" />
|
||||||
|
</template>
|
||||||
230
src/views/infra/log-management/api-error-log/index.vue
Normal file
230
src/views/infra/log-management/api-error-log/index.vue
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import { INFRA_API_ERROR_LOG_PROCESS_STATUS_DICT_CODE } from '@/constants/dict';
|
||||||
|
import { fetchExportApiErrorLog, fetchGetApiErrorLog, fetchGetApiErrorLogPage } from '@/service/api';
|
||||||
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||||
|
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||||
|
import DictText from '@/components/custom/dict-text.vue';
|
||||||
|
import LogDetailDialog from '../shared/log-detail-dialog.vue';
|
||||||
|
import { type LogDetailSection, LogPermission, downloadBlob, formatDateTime, getLogExportFileName } from '../shared';
|
||||||
|
import ApiErrorLogSearch from './modules/search.vue';
|
||||||
|
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ApiErrorLogTab' });
|
||||||
|
|
||||||
|
type ApiErrorLogPageResponse = Awaited<ReturnType<typeof fetchGetApiErrorLogPage>>;
|
||||||
|
|
||||||
|
function createSearchParams(): Api.SystemLog.ApiError.SearchParams {
|
||||||
|
return {
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
userId: undefined,
|
||||||
|
userType: undefined,
|
||||||
|
applicationName: undefined,
|
||||||
|
requestUrl: undefined,
|
||||||
|
exceptionTime: undefined,
|
||||||
|
processStatus: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformPageResult(response: ApiErrorLogPageResponse, pageNo: number, pageSize: number) {
|
||||||
|
if (!response.error && response.data) {
|
||||||
|
return {
|
||||||
|
data: response.data.list,
|
||||||
|
pageNum: pageNo,
|
||||||
|
pageSize,
|
||||||
|
total: response.data.total
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize,
|
||||||
|
total: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
const searchParams = reactive(createSearchParams());
|
||||||
|
const detailVisible = ref(false);
|
||||||
|
const currentRow = ref<Api.SystemLog.ApiError.Log | null>(null);
|
||||||
|
const exporting = ref(false);
|
||||||
|
const canExport = computed(() => hasAuth(LogPermission.ApiErrorExport));
|
||||||
|
|
||||||
|
const detailSections: LogDetailSection[] = [
|
||||||
|
{
|
||||||
|
title: '异常信息',
|
||||||
|
fields: [
|
||||||
|
{ label: '编号', key: 'id' },
|
||||||
|
{ label: '链路追踪编号', key: 'traceId' },
|
||||||
|
{ label: '应用名', key: 'applicationName' },
|
||||||
|
{ label: '请求方式', key: 'requestMethod' },
|
||||||
|
{ label: '请求地址', key: 'requestUrl', span: 2 },
|
||||||
|
{ label: '异常时间', key: 'exceptionTime', type: 'datetime' },
|
||||||
|
{ label: '异常名', key: 'exceptionName' },
|
||||||
|
{ label: '异常消息', key: 'exceptionMessage', type: 'multiline' },
|
||||||
|
{ label: '根因消息', key: 'exceptionRootCauseMessage', type: 'multiline' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '上下文',
|
||||||
|
fields: [
|
||||||
|
{ label: '用户编号', key: 'userId' },
|
||||||
|
{ label: '用户IP', key: 'userIp' },
|
||||||
|
{ label: '浏览器UA', key: 'userAgent', type: 'multiline' },
|
||||||
|
{ label: '请求参数', key: 'requestParams', type: 'multiline' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '堆栈与处理',
|
||||||
|
fields: [
|
||||||
|
{ label: '异常类名', key: 'exceptionClassName' },
|
||||||
|
{ label: '异常文件', key: 'exceptionFileName' },
|
||||||
|
{ label: '异常方法', key: 'exceptionMethodName' },
|
||||||
|
{ label: '异常行号', key: 'exceptionLineNumber' },
|
||||||
|
{ label: '处理状态', key: 'processStatus', type: 'dict', dictCode: INFRA_API_ERROR_LOG_PROCESS_STATUS_DICT_CODE },
|
||||||
|
{ label: '处理时间', key: 'processTime', type: 'datetime' },
|
||||||
|
{ label: '处理用户编号', key: 'processUserId', span: 2 },
|
||||||
|
{ label: '异常栈轨迹', key: 'exceptionStackTrace', type: 'multiline' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
||||||
|
ApiErrorLogPageResponse,
|
||||||
|
Api.SystemLog.ApiError.Log
|
||||||
|
>({
|
||||||
|
paginationProps: {
|
||||||
|
currentPage: searchParams.pageNo,
|
||||||
|
pageSize: searchParams.pageSize
|
||||||
|
},
|
||||||
|
api: () => fetchGetApiErrorLogPage(searchParams),
|
||||||
|
transform: response => transformPageResult(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||||
|
onPaginationParamsChange: params => {
|
||||||
|
searchParams.pageNo = params.currentPage ?? 1;
|
||||||
|
searchParams.pageSize = params.pageSize ?? 10;
|
||||||
|
},
|
||||||
|
columns: () => [
|
||||||
|
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||||
|
{ prop: 'applicationName', label: '应用名', minWidth: 140, showOverflowTooltip: true },
|
||||||
|
{ prop: 'requestMethod', label: '请求方式', width: 100, align: 'center' },
|
||||||
|
{ prop: 'requestUrl', label: '请求地址', minWidth: 220, showOverflowTooltip: true },
|
||||||
|
{ prop: 'exceptionName', label: '异常名', minWidth: 180, showOverflowTooltip: true },
|
||||||
|
{
|
||||||
|
prop: 'processStatus',
|
||||||
|
label: '处理状态',
|
||||||
|
minWidth: 120,
|
||||||
|
formatter: row => <DictText dictCode={INFRA_API_ERROR_LOG_PROCESS_STATUS_DICT_CODE} value={row.processStatus} />
|
||||||
|
},
|
||||||
|
{ prop: 'exceptionTime', label: '异常时间', minWidth: 180, formatter: row => formatDateTime(row.exceptionTime) },
|
||||||
|
{
|
||||||
|
prop: 'operate',
|
||||||
|
label: '操作',
|
||||||
|
width: 90,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
function getRowActions(row: Api.SystemLog.ApiError.Log): BusinessTableAction[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'view',
|
||||||
|
label: '查看',
|
||||||
|
buttonType: 'primary',
|
||||||
|
icon: IconMdiEyeOutline,
|
||||||
|
onClick: () => openDetail(row)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(row: Api.SystemLog.ApiError.Log) {
|
||||||
|
currentRow.value = row;
|
||||||
|
detailVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadTable(page = searchParams.pageNo) {
|
||||||
|
await getDataByPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSearchParams() {
|
||||||
|
Object.assign(searchParams, createSearchParams());
|
||||||
|
reloadTable(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
reloadTable(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExport() {
|
||||||
|
exporting.value = true;
|
||||||
|
const { error, data: blob } = await fetchExportApiErrorLog(searchParams);
|
||||||
|
exporting.value = false;
|
||||||
|
|
||||||
|
if (error || !blob) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadBlob(blob, getLogExportFileName('API错误日志'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||||
|
<ApiErrorLogSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
||||||
|
|
||||||
|
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between gap-12px">
|
||||||
|
<div class="flex items-center gap-10px">
|
||||||
|
<p>API错误日志列表</p>
|
||||||
|
<ElTag effect="plain">{{ mobilePagination.total || data.length }}</ElTag>
|
||||||
|
</div>
|
||||||
|
<TableHeaderOperation
|
||||||
|
v-model:columns="columnChecks"
|
||||||
|
:disabled-delete="true"
|
||||||
|
:loading="loading"
|
||||||
|
@refresh="getData"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElButton v-if="canExport" plain type="primary" :loading="exporting" @click="handleExport">
|
||||||
|
<template #icon>
|
||||||
|
<icon-mdi-download class="text-icon" />
|
||||||
|
</template>
|
||||||
|
导出
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</TableHeaderOperation>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<ElTable v-loading="loading" height="100%" border row-key="id" :data="data">
|
||||||
|
<ElTableColumn v-for="col in columns" :key="String(col.prop)" v-bind="col" />
|
||||||
|
</ElTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-20px flex justify-end">
|
||||||
|
<ElPagination
|
||||||
|
v-if="mobilePagination.total"
|
||||||
|
layout="total,prev,pager,next,sizes"
|
||||||
|
v-bind="mobilePagination"
|
||||||
|
@current-change="mobilePagination['current-change']"
|
||||||
|
@size-change="mobilePagination['size-change']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
|
||||||
|
<LogDetailDialog
|
||||||
|
v-model:visible="detailVisible"
|
||||||
|
title="API错误日志详情"
|
||||||
|
:row-data="currentRow"
|
||||||
|
:sections="detailSections"
|
||||||
|
:fetch-detail="fetchGetApiErrorLog"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { INFRA_API_ERROR_LOG_PROCESS_STATUS_DICT_CODE, SYSTEM_USER_TYPE_DICT_CODE } from '@/constants/dict';
|
||||||
|
import TableSearchFields, { type SearchField } from '@/components/custom/table-search-fields.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ApiErrorLogSearch' });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
reset: [];
|
||||||
|
search: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const model = defineModel<Api.SystemLog.ApiError.SearchParams>('model', { required: true });
|
||||||
|
|
||||||
|
const fields = computed<SearchField[]>(() => [
|
||||||
|
{
|
||||||
|
key: 'applicationName',
|
||||||
|
label: '应用名',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '请输入应用名'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'requestUrl',
|
||||||
|
label: '请求地址',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '请输入请求地址'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'processStatus',
|
||||||
|
label: '处理状态',
|
||||||
|
type: 'dict',
|
||||||
|
placeholder: '请选择处理状态',
|
||||||
|
dictCode: INFRA_API_ERROR_LOG_PROCESS_STATUS_DICT_CODE,
|
||||||
|
filterable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'userId',
|
||||||
|
label: '用户编号',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '请输入用户编号'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'userType',
|
||||||
|
label: '用户类型',
|
||||||
|
type: 'dict',
|
||||||
|
placeholder: '请选择用户类型',
|
||||||
|
dictCode: SYSTEM_USER_TYPE_DICT_CODE,
|
||||||
|
filterable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'exceptionTime',
|
||||||
|
label: '异常时间',
|
||||||
|
type: 'dateRange',
|
||||||
|
placeholder: '请选择异常时间',
|
||||||
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
format: 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TableSearchFields v-model="model" :fields="fields" :columns="4" @reset="emit('reset')" @search="emit('search')" />
|
||||||
|
</template>
|
||||||
177
src/views/infra/log-management/index.vue
Normal file
177
src/views/infra/log-management/index.vue
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, markRaw, ref, watch } from 'vue';
|
||||||
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
|
import LoginLogTab from './login-log/index.vue';
|
||||||
|
import OperateLogTab from './operate-log/index.vue';
|
||||||
|
import ApiAccessLogTab from './api-access-log/index.vue';
|
||||||
|
import ApiErrorLogTab from './api-error-log/index.vue';
|
||||||
|
import { LOG_TABS, type LogTabKey } from './shared';
|
||||||
|
|
||||||
|
defineOptions({ name: 'LogManagement' });
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
const activeTab = ref<LogTabKey>('login-log');
|
||||||
|
|
||||||
|
const visibleTabs = computed(() => LOG_TABS.filter(tab => hasAuth(tab.queryPermission)));
|
||||||
|
const activeTabMeta = computed(() => visibleTabs.value.find(tab => tab.name === activeTab.value) || null);
|
||||||
|
|
||||||
|
const scopeOptions = computed(() =>
|
||||||
|
visibleTabs.value.map(tab => ({
|
||||||
|
label: tab.label,
|
||||||
|
value: tab.name
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const componentMap: Record<LogTabKey, object> = {
|
||||||
|
'login-log': markRaw(LoginLogTab),
|
||||||
|
'operate-log': markRaw(OperateLogTab),
|
||||||
|
'api-access-log': markRaw(ApiAccessLogTab),
|
||||||
|
'api-error-log': markRaw(ApiErrorLogTab)
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
visibleTabs,
|
||||||
|
tabs => {
|
||||||
|
if (!tabs.length) return;
|
||||||
|
if (!tabs.some(tab => tab.name === activeTab.value)) {
|
||||||
|
activeTab.value = tabs[0].name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="log-management-page">
|
||||||
|
<ElCard class="log-management-page__context" body-class="log-management-page__context-body">
|
||||||
|
<div v-if="visibleTabs.length" class="log-management-page__context-layout">
|
||||||
|
<div class="log-management-page__context-controls">
|
||||||
|
<ElSegmented v-model="activeTab" :options="scopeOptions" class="log-management-page__segmented" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="log-management-page__context-info">
|
||||||
|
<div class="log-management-page__context-main">
|
||||||
|
<div class="log-management-page__context-item">
|
||||||
|
<span class="log-management-page__context-label">当前日志</span>
|
||||||
|
<strong class="log-management-page__context-value">{{ activeTabMeta?.label || '--' }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="log-management-page__context-desc">
|
||||||
|
{{ activeTabMeta?.description || '当前查看系统日志数据。' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElEmpty v-else description="暂无可查看的日志权限" />
|
||||||
|
</ElCard>
|
||||||
|
|
||||||
|
<div v-if="activeTabMeta" class="log-management-page__content">
|
||||||
|
<KeepAlive>
|
||||||
|
<component :is="componentMap[activeTab]" />
|
||||||
|
</KeepAlive>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.log-management-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-management-page__context {
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
background: var(--el-fill-color-blank);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.log-management-page__context-body) {
|
||||||
|
padding: 16px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-management-page__context-layout {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-management-page__context-controls {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.log-management-page__segmented) {
|
||||||
|
padding: 6px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.log-management-page__segmented .el-segmented__item) {
|
||||||
|
min-width: 128px;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 0 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-management-page__context-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
border-left: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-management-page__context-main {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-management-page__context-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-management-page__context-label {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-management-page__context-value {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-management-page__context-desc {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-management-page__content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 1200px) {
|
||||||
|
.log-management-page__context-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-management-page__context-info {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-left: none;
|
||||||
|
border-top: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
235
src/views/infra/log-management/login-log/index.vue
Normal file
235
src/views/infra/log-management/login-log/index.vue
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import { ElTag } from 'element-plus';
|
||||||
|
import { SYSTEM_LOGIN_RESULT_DICT_CODE, SYSTEM_LOGIN_TYPE_DICT_CODE } from '@/constants/dict';
|
||||||
|
import { fetchExportLoginLog, fetchGetLoginLog, fetchGetLoginLogPage } from '@/service/api';
|
||||||
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||||
|
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||||
|
import DictText from '@/components/custom/dict-text.vue';
|
||||||
|
import LogDetailDialog from '../shared/log-detail-dialog.vue';
|
||||||
|
import { type LogDetailSection, LogPermission, downloadBlob, formatDateTime, getLogExportFileName } from '../shared';
|
||||||
|
import LoginLogSearch from './modules/search.vue';
|
||||||
|
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||||
|
|
||||||
|
defineOptions({ name: 'LoginLogTab' });
|
||||||
|
|
||||||
|
type LoginLogPageResponse = Awaited<ReturnType<typeof fetchGetLoginLogPage>>;
|
||||||
|
|
||||||
|
function createSearchParams(): Api.SystemLog.Login.SearchParams {
|
||||||
|
return {
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
userIp: undefined,
|
||||||
|
username: undefined,
|
||||||
|
status: undefined,
|
||||||
|
createTime: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformPageResult(response: LoginLogPageResponse, pageNo: number, pageSize: number) {
|
||||||
|
if (!response.error && response.data) {
|
||||||
|
return {
|
||||||
|
data: response.data.list,
|
||||||
|
pageNum: pageNo,
|
||||||
|
pageSize,
|
||||||
|
total: response.data.total
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize,
|
||||||
|
total: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
const searchParams = reactive(createSearchParams());
|
||||||
|
const detailVisible = ref(false);
|
||||||
|
const currentRow = ref<Api.SystemLog.Login.Log | null>(null);
|
||||||
|
const exporting = ref(false);
|
||||||
|
const canExport = computed(() => hasAuth(LogPermission.LoginExport));
|
||||||
|
|
||||||
|
const detailSections: LogDetailSection[] = [
|
||||||
|
{
|
||||||
|
title: '基础信息',
|
||||||
|
fields: [
|
||||||
|
{ label: '日志编号', key: 'id' },
|
||||||
|
{ label: '日志类型', key: 'logType', type: 'dict', dictCode: SYSTEM_LOGIN_TYPE_DICT_CODE },
|
||||||
|
{ label: '用户编号', key: 'userId' },
|
||||||
|
{ label: '账号', key: 'username' },
|
||||||
|
{ label: '登录结果', key: 'result', type: 'dict', dictCode: SYSTEM_LOGIN_RESULT_DICT_CODE },
|
||||||
|
{ label: '登录时间', key: 'createTime', type: 'datetime' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '访问上下文',
|
||||||
|
fields: [
|
||||||
|
{ label: '链路追踪编号', key: 'traceId' },
|
||||||
|
{ label: '登录IP', key: 'userIp' },
|
||||||
|
{ label: '浏览器UA', key: 'userAgent', type: 'multiline' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
||||||
|
LoginLogPageResponse,
|
||||||
|
Api.SystemLog.Login.Log
|
||||||
|
>({
|
||||||
|
paginationProps: {
|
||||||
|
currentPage: searchParams.pageNo,
|
||||||
|
pageSize: searchParams.pageSize
|
||||||
|
},
|
||||||
|
api: () => fetchGetLoginLogPage(searchParams),
|
||||||
|
transform: response => transformPageResult(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||||
|
onPaginationParamsChange: params => {
|
||||||
|
searchParams.pageNo = params.currentPage ?? 1;
|
||||||
|
searchParams.pageSize = params.pageSize ?? 10;
|
||||||
|
},
|
||||||
|
columns: () => [
|
||||||
|
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||||
|
{
|
||||||
|
prop: 'username',
|
||||||
|
label: '账号',
|
||||||
|
minWidth: 140,
|
||||||
|
showOverflowTooltip: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'logType',
|
||||||
|
label: '日志类型',
|
||||||
|
minWidth: 120,
|
||||||
|
formatter: row => <DictText dictCode={SYSTEM_LOGIN_TYPE_DICT_CODE} value={row.logType} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'result',
|
||||||
|
label: '登录结果',
|
||||||
|
minWidth: 80,
|
||||||
|
formatter: row => (
|
||||||
|
<ElTag type={row.result === 1 ? 'success' : 'danger'}>
|
||||||
|
<DictText dictCode={SYSTEM_LOGIN_RESULT_DICT_CODE} value={row.result} />
|
||||||
|
</ElTag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'userIp',
|
||||||
|
label: '登录IP',
|
||||||
|
minWidth: 140,
|
||||||
|
showOverflowTooltip: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'createTime',
|
||||||
|
label: '登录时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: row => formatDateTime(row.createTime)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'operate',
|
||||||
|
label: '操作',
|
||||||
|
width: 90,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
function getRowActions(row: Api.SystemLog.Login.Log): BusinessTableAction[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'view',
|
||||||
|
label: '查看',
|
||||||
|
buttonType: 'primary',
|
||||||
|
icon: IconMdiEyeOutline,
|
||||||
|
onClick: () => openDetail(row)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(row: Api.SystemLog.Login.Log) {
|
||||||
|
currentRow.value = row;
|
||||||
|
detailVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadTable(page = searchParams.pageNo) {
|
||||||
|
await getDataByPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSearchParams() {
|
||||||
|
Object.assign(searchParams, createSearchParams());
|
||||||
|
reloadTable(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
reloadTable(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExport() {
|
||||||
|
exporting.value = true;
|
||||||
|
const { error, data: blob } = await fetchExportLoginLog(searchParams);
|
||||||
|
exporting.value = false;
|
||||||
|
|
||||||
|
if (error || !blob) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadBlob(blob, getLogExportFileName('登录日志'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||||
|
<LoginLogSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
||||||
|
|
||||||
|
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between gap-12px">
|
||||||
|
<div class="flex items-center gap-10px">
|
||||||
|
<p>登录日志列表</p>
|
||||||
|
<ElTag effect="plain">{{ mobilePagination.total || data.length }}</ElTag>
|
||||||
|
</div>
|
||||||
|
<TableHeaderOperation
|
||||||
|
v-model:columns="columnChecks"
|
||||||
|
:disabled-delete="true"
|
||||||
|
:loading="loading"
|
||||||
|
@refresh="getData"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElButton v-if="canExport" plain type="primary" :loading="exporting" @click="handleExport">
|
||||||
|
<template #icon>
|
||||||
|
<icon-mdi-download class="text-icon" />
|
||||||
|
</template>
|
||||||
|
导出
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</TableHeaderOperation>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<ElTable v-loading="loading" height="100%" border row-key="id" :data="data">
|
||||||
|
<ElTableColumn v-for="col in columns" :key="String(col.prop)" v-bind="col" />
|
||||||
|
</ElTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-20px flex justify-end">
|
||||||
|
<ElPagination
|
||||||
|
v-if="mobilePagination.total"
|
||||||
|
layout="total,prev,pager,next,sizes"
|
||||||
|
v-bind="mobilePagination"
|
||||||
|
@current-change="mobilePagination['current-change']"
|
||||||
|
@size-change="mobilePagination['size-change']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
|
||||||
|
<LogDetailDialog
|
||||||
|
v-model:visible="detailVisible"
|
||||||
|
title="登录日志详情"
|
||||||
|
:row-data="currentRow"
|
||||||
|
:sections="detailSections"
|
||||||
|
:fetch-detail="fetchGetLoginLog"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
60
src/views/infra/log-management/login-log/modules/search.vue
Normal file
60
src/views/infra/log-management/login-log/modules/search.vue
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import TableSearchFields, { type SearchField } from '@/components/custom/table-search-fields.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'LoginLogSearch' });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
reset: [];
|
||||||
|
search: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const model = defineModel<Api.SystemLog.Login.SearchParams>('model', { required: true });
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: '成功', value: 1 },
|
||||||
|
{ label: '失败', value: 0 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const fields = computed<SearchField[]>(() => [
|
||||||
|
{
|
||||||
|
key: 'username',
|
||||||
|
label: '账号',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '请输入用户账号'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'userIp',
|
||||||
|
label: '登录IP',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '请输入登录 IP'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: '登录结果',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: '请选择登录结果',
|
||||||
|
options: statusOptions,
|
||||||
|
transformValue: value => {
|
||||||
|
if (value === undefined || value === null || value === '') return undefined;
|
||||||
|
return Number(value) === 1;
|
||||||
|
},
|
||||||
|
resolveValue: value => {
|
||||||
|
if (value === undefined || value === null) return undefined;
|
||||||
|
return value ? 1 : 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'createTime',
|
||||||
|
label: '登录时间',
|
||||||
|
type: 'dateRange',
|
||||||
|
placeholder: '请选择登录时间',
|
||||||
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
format: 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TableSearchFields v-model="model" :fields="fields" :columns="4" @reset="emit('reset')" @search="emit('search')" />
|
||||||
|
</template>
|
||||||
244
src/views/infra/log-management/operate-log/index.vue
Normal file
244
src/views/infra/log-management/operate-log/index.vue
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import {
|
||||||
|
fetchExportOperateLog,
|
||||||
|
fetchGetOperateLog,
|
||||||
|
fetchGetOperateLogPage,
|
||||||
|
fetchGetUserSimpleList
|
||||||
|
} from '@/service/api';
|
||||||
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||||
|
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||||
|
import LogDetailDialog from '../shared/log-detail-dialog.vue';
|
||||||
|
import { type LogDetailSection, LogPermission, downloadBlob, formatDateTime, getLogExportFileName } from '../shared';
|
||||||
|
import OperateLogSearch from './modules/search.vue';
|
||||||
|
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||||
|
|
||||||
|
defineOptions({ name: 'OperateLogTab' });
|
||||||
|
|
||||||
|
type OperateLogPageResponse = Awaited<ReturnType<typeof fetchGetOperateLogPage>>;
|
||||||
|
|
||||||
|
function createSearchParams(): Api.SystemLog.Operate.SearchParams {
|
||||||
|
return {
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
userId: undefined,
|
||||||
|
type: undefined,
|
||||||
|
requestMethod: undefined,
|
||||||
|
subType: undefined,
|
||||||
|
action: undefined,
|
||||||
|
createTime: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformPageResult(response: OperateLogPageResponse, pageNo: number, pageSize: number) {
|
||||||
|
if (!response.error && response.data) {
|
||||||
|
return {
|
||||||
|
data: response.data.list,
|
||||||
|
pageNum: pageNo,
|
||||||
|
pageSize,
|
||||||
|
total: response.data.total
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize,
|
||||||
|
total: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
const searchParams = reactive(createSearchParams());
|
||||||
|
const detailVisible = ref(false);
|
||||||
|
const currentRow = ref<Api.SystemLog.Operate.Log | null>(null);
|
||||||
|
const exporting = ref(false);
|
||||||
|
const userOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||||
|
const canExport = computed(() => hasAuth(LogPermission.OperateExport));
|
||||||
|
|
||||||
|
const detailSections: LogDetailSection[] = [
|
||||||
|
{
|
||||||
|
title: '操作信息',
|
||||||
|
fields: [
|
||||||
|
{ label: '日志编号', key: 'id' },
|
||||||
|
{ label: '操作人', key: 'userName' },
|
||||||
|
{ label: '用户编号', key: 'userId' },
|
||||||
|
{ label: '模块类型', key: 'type' },
|
||||||
|
{ label: '操作名', key: 'subType' },
|
||||||
|
{ label: '业务编号', key: 'bizId' },
|
||||||
|
{ label: '请求方式', key: 'requestMethod' },
|
||||||
|
{ label: '请求地址', key: 'requestUrl', span: 2 },
|
||||||
|
{ label: '操作时间', key: 'createTime', type: 'datetime' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '上下文',
|
||||||
|
fields: [
|
||||||
|
{ label: '链路追踪编号', key: 'traceId' },
|
||||||
|
{ label: '用户IP', key: 'userIp' },
|
||||||
|
{ label: '浏览器UA', key: 'userAgent', type: 'multiline' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '详细内容',
|
||||||
|
fields: [
|
||||||
|
{ label: '操作明细', key: 'action', type: 'multiline' },
|
||||||
|
{ label: '扩展字段', key: 'extra', type: 'multiline' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
||||||
|
OperateLogPageResponse,
|
||||||
|
Api.SystemLog.Operate.Log
|
||||||
|
>({
|
||||||
|
paginationProps: {
|
||||||
|
currentPage: searchParams.pageNo,
|
||||||
|
pageSize: searchParams.pageSize
|
||||||
|
},
|
||||||
|
api: () => fetchGetOperateLogPage(searchParams),
|
||||||
|
transform: response => transformPageResult(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||||
|
onPaginationParamsChange: params => {
|
||||||
|
searchParams.pageNo = params.currentPage ?? 1;
|
||||||
|
searchParams.pageSize = params.pageSize ?? 10;
|
||||||
|
},
|
||||||
|
columns: () => [
|
||||||
|
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||||
|
{ prop: 'userName', label: '操作人', minWidth: 120, showOverflowTooltip: true },
|
||||||
|
{ prop: 'type', label: '模块类型', minWidth: 140, showOverflowTooltip: true },
|
||||||
|
{ prop: 'subType', label: '操作名', minWidth: 140, showOverflowTooltip: true },
|
||||||
|
// { prop: 'bizId', label: '业务编号', minWidth: 120, showOverflowTooltip: true },
|
||||||
|
{ prop: 'requestMethod', label: '请求方式', width: 150, align: 'center' },
|
||||||
|
{ prop: 'requestUrl', label: '请求地址', minWidth: 220, showOverflowTooltip: true },
|
||||||
|
{ prop: 'createTime', label: '操作时间', minWidth: 180, formatter: row => formatDateTime(row.createTime) },
|
||||||
|
{
|
||||||
|
prop: 'operate',
|
||||||
|
label: '操作',
|
||||||
|
width: 90,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
function getRowActions(row: Api.SystemLog.Operate.Log): BusinessTableAction[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'view',
|
||||||
|
label: '查看',
|
||||||
|
buttonType: 'primary',
|
||||||
|
icon: IconMdiEyeOutline,
|
||||||
|
onClick: () => openDetail(row)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(row: Api.SystemLog.Operate.Log) {
|
||||||
|
currentRow.value = row;
|
||||||
|
detailVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadTable(page = searchParams.pageNo) {
|
||||||
|
await getDataByPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSearchParams() {
|
||||||
|
Object.assign(searchParams, createSearchParams());
|
||||||
|
reloadTable(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
reloadTable(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUserOptions() {
|
||||||
|
const { error, data: userList } = await fetchGetUserSimpleList();
|
||||||
|
|
||||||
|
if (error || !userList) {
|
||||||
|
userOptions.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
userOptions.value = userList.map((item: Api.SystemManage.UserSimple) => ({
|
||||||
|
label: item.username ? `${item.nickname}(${item.username})` : item.nickname,
|
||||||
|
value: item.id
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
loadUserOptions();
|
||||||
|
|
||||||
|
async function handleExport() {
|
||||||
|
exporting.value = true;
|
||||||
|
const { error, data: blob } = await fetchExportOperateLog(searchParams);
|
||||||
|
exporting.value = false;
|
||||||
|
|
||||||
|
if (error || !blob) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadBlob(blob, getLogExportFileName('操作日志'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||||
|
<OperateLogSearch
|
||||||
|
v-model:model="searchParams"
|
||||||
|
:user-options="userOptions"
|
||||||
|
@reset="resetSearchParams"
|
||||||
|
@search="handleSearch"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between gap-12px">
|
||||||
|
<div class="flex items-center gap-10px">
|
||||||
|
<p>操作日志列表</p>
|
||||||
|
<ElTag effect="plain">{{ mobilePagination.total || data.length }}</ElTag>
|
||||||
|
</div>
|
||||||
|
<TableHeaderOperation
|
||||||
|
v-model:columns="columnChecks"
|
||||||
|
:disabled-delete="true"
|
||||||
|
:loading="loading"
|
||||||
|
@refresh="getData"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElButton v-if="canExport" plain type="primary" :loading="exporting" @click="handleExport">
|
||||||
|
<template #icon>
|
||||||
|
<icon-mdi-download class="text-icon" />
|
||||||
|
</template>
|
||||||
|
导出
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</TableHeaderOperation>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<ElTable v-loading="loading" height="100%" border row-key="id" :data="data">
|
||||||
|
<ElTableColumn v-for="col in columns" :key="String(col.prop)" v-bind="col" />
|
||||||
|
</ElTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-20px flex justify-end">
|
||||||
|
<ElPagination
|
||||||
|
v-if="mobilePagination.total"
|
||||||
|
layout="total,prev,pager,next,sizes"
|
||||||
|
v-bind="mobilePagination"
|
||||||
|
@current-change="mobilePagination['current-change']"
|
||||||
|
@size-change="mobilePagination['size-change']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
|
||||||
|
<LogDetailDialog
|
||||||
|
v-model:visible="detailVisible"
|
||||||
|
title="操作日志详情"
|
||||||
|
:row-data="currentRow"
|
||||||
|
:sections="detailSections"
|
||||||
|
:fetch-detail="fetchGetOperateLog"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { SYSTEM_REQUEST_METHOD_DICT_CODE } from '@/constants/dict';
|
||||||
|
import TableSearchFields, { type SearchField } from '@/components/custom/table-search-fields.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'OperateLogSearch' });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
reset: [];
|
||||||
|
search: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const model = defineModel<Api.SystemLog.Operate.SearchParams>('model', { required: true });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
userOptions: Array<{ label: string; value: string }>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const fields = computed<SearchField[]>(() => [
|
||||||
|
{
|
||||||
|
key: 'userId',
|
||||||
|
label: '操作人',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: '请选择操作人',
|
||||||
|
options: props.userOptions,
|
||||||
|
filterable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'type',
|
||||||
|
label: '模块类型',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '请输入操作模块类型'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'requestMethod',
|
||||||
|
label: '请求方式',
|
||||||
|
type: 'dict',
|
||||||
|
placeholder: '请选择请求方式',
|
||||||
|
dictCode: SYSTEM_REQUEST_METHOD_DICT_CODE,
|
||||||
|
filterable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'subType',
|
||||||
|
label: '操作名',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '请输入操作名'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'createTime',
|
||||||
|
label: '操作时间',
|
||||||
|
type: 'dateRange',
|
||||||
|
placeholder: '请选择操作时间',
|
||||||
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
format: 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TableSearchFields v-model="model" :fields="fields" :columns="4" @reset="emit('reset')" @search="emit('search')" />
|
||||||
|
</template>
|
||||||
123
src/views/infra/log-management/shared.ts
Normal file
123
src/views/infra/log-management/shared.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
export type LogTabKey = 'login-log' | 'operate-log' | 'api-access-log' | 'api-error-log';
|
||||||
|
|
||||||
|
export interface LogTabOption {
|
||||||
|
name: LogTabKey;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
queryPermission: string;
|
||||||
|
exportPermission: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogDetailField {
|
||||||
|
label: string;
|
||||||
|
key?: string;
|
||||||
|
span?: number;
|
||||||
|
type?: 'text' | 'datetime' | 'dict' | 'multiline';
|
||||||
|
dictCode?: string;
|
||||||
|
formatter?: (detail: Record<string, unknown>) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogDetailSection {
|
||||||
|
title: string;
|
||||||
|
fields: LogDetailField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogPermission = {
|
||||||
|
LoginQuery: 'system:login-log:query',
|
||||||
|
LoginExport: 'system:login-log:export',
|
||||||
|
OperateQuery: 'system:operate-log:query',
|
||||||
|
OperateExport: 'system:operate-log:export',
|
||||||
|
ApiAccessQuery: 'system:api-access-log:query',
|
||||||
|
ApiAccessExport: 'system:api-access-log:export',
|
||||||
|
ApiErrorQuery: 'system:api-error-log:query',
|
||||||
|
ApiErrorExport: 'system:api-error-log:export'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const LOG_TABS: LogTabOption[] = [
|
||||||
|
{
|
||||||
|
name: 'login-log',
|
||||||
|
label: '登录日志',
|
||||||
|
description: '查看系统登录行为、登录结果与登录时间。',
|
||||||
|
queryPermission: LogPermission.LoginQuery,
|
||||||
|
exportPermission: LogPermission.LoginExport
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'operate-log',
|
||||||
|
label: '操作日志',
|
||||||
|
description: '查看系统操作轨迹、请求地址与业务编号。',
|
||||||
|
queryPermission: LogPermission.OperateQuery,
|
||||||
|
exportPermission: LogPermission.OperateExport
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'api-access-log',
|
||||||
|
label: 'API访问日志',
|
||||||
|
description: '查看接口访问结果、执行时长与请求链路。',
|
||||||
|
queryPermission: LogPermission.ApiAccessQuery,
|
||||||
|
exportPermission: LogPermission.ApiAccessExport
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'api-error-log',
|
||||||
|
label: 'API错误日志',
|
||||||
|
description: '查看接口异常、处理状态与错误上下文。',
|
||||||
|
queryPermission: LogPermission.ApiErrorQuery,
|
||||||
|
exportPermission: LogPermission.ApiErrorExport
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function formatDateTime(value?: string | null) {
|
||||||
|
if (!value) return '--';
|
||||||
|
|
||||||
|
const target = dayjs(value);
|
||||||
|
return target.isValid() ? target.format('YYYY-MM-DD HH:mm:ss') : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatText(value: unknown) {
|
||||||
|
if (value === null || value === undefined || value === '') return '--';
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDuration(value: unknown) {
|
||||||
|
if (value === null || value === undefined || value === '') return '--';
|
||||||
|
|
||||||
|
const duration = Number(value);
|
||||||
|
return Number.isFinite(duration) ? `${duration} ms` : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMultilineText(value: unknown) {
|
||||||
|
if (value === null || value === undefined || value === '') return '--';
|
||||||
|
|
||||||
|
const text = String(value);
|
||||||
|
const normalized = text.trim();
|
||||||
|
|
||||||
|
if (!normalized) return '--';
|
||||||
|
|
||||||
|
if (
|
||||||
|
(normalized.startsWith('{') && normalized.endsWith('}')) ||
|
||||||
|
(normalized.startsWith('[') && normalized.endsWith(']'))
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(normalized), null, 2);
|
||||||
|
} catch {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadBlob(blob: Blob, fileName: string) {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
|
||||||
|
link.href = url;
|
||||||
|
link.download = fileName;
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLogExportFileName(label: string) {
|
||||||
|
return `${label}_${dayjs().format('YYYY-MM-DD')}.xls`;
|
||||||
|
}
|
||||||
137
src/views/infra/log-management/shared/log-detail-dialog.vue
Normal file
137
src/views/infra/log-management/shared/log-detail-dialog.vue
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||||
|
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||||
|
import DictText from '@/components/custom/dict-text.vue';
|
||||||
|
import { type LogDetailField, type LogDetailSection, formatDateTime, formatMultilineText, formatText } from '../shared';
|
||||||
|
|
||||||
|
defineOptions({ name: 'LogDetailDialog' });
|
||||||
|
|
||||||
|
type DetailRecord = { id: string };
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
title: string;
|
||||||
|
rowData?: { id: string } | null;
|
||||||
|
sections: LogDetailSection[];
|
||||||
|
fetchDetail: (id: string) => Promise<{
|
||||||
|
error: unknown;
|
||||||
|
data: DetailRecord | null;
|
||||||
|
}>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const detail = ref<DetailRecord | null>(null);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [visible.value, props.rowData?.id] as const,
|
||||||
|
([isVisible]) => {
|
||||||
|
if (!isVisible) return;
|
||||||
|
loadDetail();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function loadDetail() {
|
||||||
|
if (!props.rowData?.id) {
|
||||||
|
detail.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
const result = await props.fetchDetail(props.rowData.id);
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
detail.value = !result.error && result.data ? result.data : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFieldValue(field: LogDetailField) {
|
||||||
|
if (!detail.value || !field.key) return undefined;
|
||||||
|
return (detail.value as Record<string, unknown>)[field.key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDictFieldValue(field: LogDetailField): string | number | null | undefined {
|
||||||
|
const value = getFieldValue(field);
|
||||||
|
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof value === 'string' || typeof value === 'number' ? value : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFieldText(field: LogDetailField) {
|
||||||
|
if (field.formatter && detail.value) {
|
||||||
|
return field.formatter(detail.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = getFieldValue(field);
|
||||||
|
|
||||||
|
if (field.type === 'datetime') {
|
||||||
|
return formatDateTime(value as string | null | undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === 'multiline') {
|
||||||
|
return formatMultilineText(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatText(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFieldSpan(field: LogDetailField) {
|
||||||
|
return field.span || (field.type === 'multiline' ? 2 : 1);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BusinessFormDialog v-model="visible" :title="title" preset="lg" :loading="loading" :show-footer="false">
|
||||||
|
<div v-if="detail" class="log-detail-dialog">
|
||||||
|
<BusinessFormSection v-for="section in sections" :key="section.title" :title="section.title">
|
||||||
|
<ElDescriptions class="log-detail-dialog__descriptions" :column="2" border size="small">
|
||||||
|
<ElDescriptionsItem
|
||||||
|
v-for="field in section.fields"
|
||||||
|
:key="`${section.title}-${field.label}`"
|
||||||
|
:label="field.label"
|
||||||
|
label-class-name="log-detail-dialog__label"
|
||||||
|
:span="getFieldSpan(field)"
|
||||||
|
>
|
||||||
|
<DictText v-if="field.type === 'dict'" :dict-code="field.dictCode!" :value="getDictFieldValue(field)" />
|
||||||
|
<div v-else-if="field.type === 'multiline'" class="log-detail-dialog__multiline">
|
||||||
|
{{ getFieldText(field) }}
|
||||||
|
</div>
|
||||||
|
<span v-else>{{ getFieldText(field) }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
</BusinessFormSection>
|
||||||
|
</div>
|
||||||
|
<ElEmpty v-else description="暂无日志详情" />
|
||||||
|
</BusinessFormDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.log-detail-dialog {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.log-detail-dialog__descriptions .el-descriptions__cell) {
|
||||||
|
line-height: 1.7;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.log-detail-dialog__label) {
|
||||||
|
width: 120px;
|
||||||
|
min-width: 120px;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-detail-dialog__multiline {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -98,7 +98,7 @@ watch(visible, isVisible => {
|
|||||||
<ElForm ref="formRef" :model="form" :rules="rules" label-position="top">
|
<ElForm ref="formRef" :model="form" :rules="rules" label-position="top">
|
||||||
<ElDescriptions :column="1" border>
|
<ElDescriptions :column="1" border>
|
||||||
<ElDescriptionsItem label="绩效月份">{{ props.rowData?.periodMonth || '--' }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="绩效月份">{{ props.rowData?.periodMonth || '--' }}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="下属">{{ props.rowData?.employeeName || '--' }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="被考核人">{{ props.rowData?.employeeName || '--' }}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="实际得分">{{ props.rowData?.actualScoreTotal ?? '--' }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="实际得分">{{ props.rowData?.actualScoreTotal ?? '--' }}</ElDescriptionsItem>
|
||||||
</ElDescriptions>
|
</ElDescriptions>
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function useTaskCompletionCascade(options: UseTaskCompletionCascadeOption
|
|||||||
const { task } = payload;
|
const { task } = payload;
|
||||||
const completeAction = options.resolveCompleteAction(task);
|
const completeAction = options.resolveCompleteAction(task);
|
||||||
if (!completeAction) {
|
if (!completeAction) {
|
||||||
window.$message?.warning('当前任务暂无可用完成动作');
|
// window.$message?.warning('当前任务暂无可用完成动作');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import BusinessRichTextEditor from '@/components/custom/business-rich-text-edito
|
|||||||
import BusinessUserSelect from '@/components/custom/business-user-select.vue';
|
import BusinessUserSelect from '@/components/custom/business-user-select.vue';
|
||||||
import DictSelect from '@/components/custom/dict-select.vue';
|
import DictSelect from '@/components/custom/dict-select.vue';
|
||||||
import { SHOW_TASK_PARENT_FIELD } from '../shared';
|
import { SHOW_TASK_PARENT_FIELD } from '../shared';
|
||||||
|
import IconMdiAutoFix from '~icons/mdi/auto-fix';
|
||||||
|
|
||||||
defineOptions({ name: 'ProjectExecutionTaskOperateDialog' });
|
defineOptions({ name: 'ProjectExecutionTaskOperateDialog' });
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ export interface PlannedEndShortcutOffset {
|
|||||||
interface Props {
|
interface Props {
|
||||||
mode: OperateMode;
|
mode: OperateMode;
|
||||||
rowData: Api.Project.ProjectTask | null;
|
rowData: Api.Project.ProjectTask | null;
|
||||||
|
executionData?: Api.Project.ProjectExecution | null;
|
||||||
/** 创建模式下的父任务预填;编辑/查看模式忽略 */
|
/** 创建模式下的父任务预填;编辑/查看模式忽略 */
|
||||||
defaultParentTaskId?: string | null;
|
defaultParentTaskId?: string | null;
|
||||||
userOptions: Api.SystemManage.UserSimple[];
|
userOptions: Api.SystemManage.UserSimple[];
|
||||||
@@ -40,6 +42,7 @@ interface Emits {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
executionData: null,
|
||||||
defaultParentTaskId: null,
|
defaultParentTaskId: null,
|
||||||
canManageAssignee: false,
|
canManageAssignee: false,
|
||||||
plannedEndShortcuts: () => [
|
plannedEndShortcuts: () => [
|
||||||
@@ -95,6 +98,8 @@ const dialogTitle = computed(() => {
|
|||||||
return props.rowData?.taskTitle ? `编辑任务:${props.rowData.taskTitle}` : '编辑任务';
|
return props.rowData?.taskTitle ? `编辑任务:${props.rowData.taskTitle}` : '编辑任务';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canFillFromExecution = computed(() => props.mode === 'create' && Boolean(props.executionData));
|
||||||
|
|
||||||
const selectableParentTasks = computed(() => props.taskOptions.filter(item => item.id !== props.rowData?.id));
|
const selectableParentTasks = computed(() => props.taskOptions.filter(item => item.id !== props.rowData?.id));
|
||||||
|
|
||||||
/** 编辑态 + 任务已开始(statusCode 离开 pending)→ 负责人锁定不可切换 */
|
/** 编辑态 + 任务已开始(statusCode 离开 pending)→ 负责人锁定不可切换 */
|
||||||
@@ -108,11 +113,17 @@ const ownerLocked = computed(() => {
|
|||||||
* owner 可能已不在执行协办人池里,用任务自带 ownerNickname 兜底回显,避免锁定态显示成裸 userId。
|
* owner 可能已不在执行协办人池里,用任务自带 ownerNickname 兜底回显,避免锁定态显示成裸 userId。
|
||||||
*/
|
*/
|
||||||
const ownerSelectOptions = computed<Api.SystemManage.UserSimple[]>(() => {
|
const ownerSelectOptions = computed<Api.SystemManage.UserSimple[]>(() => {
|
||||||
const ownerId = props.rowData?.ownerId;
|
const currentOwnerId = props.mode === 'create' ? model.ownerId : (props.rowData?.ownerId ?? null);
|
||||||
if (props.mode === 'create' || !ownerId || props.userOptions.some(item => item.id === ownerId)) {
|
if (!currentOwnerId || props.userOptions.some(item => item.id === currentOwnerId)) {
|
||||||
return props.userOptions;
|
return props.userOptions;
|
||||||
}
|
}
|
||||||
return [...props.userOptions, { id: ownerId, nickname: props.rowData?.ownerNickname || ownerId }];
|
|
||||||
|
const fallbackNickname =
|
||||||
|
props.mode === 'create'
|
||||||
|
? props.executionData?.ownerNickname || currentOwnerId
|
||||||
|
: props.rowData?.ownerNickname || currentOwnerId;
|
||||||
|
|
||||||
|
return [...props.userOptions, { id: currentOwnerId, nickname: fallbackNickname }];
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 编辑态无协办人管理权限时只读回显(create 恒可交互) */
|
/** 编辑态无协办人管理权限时只读回显(create 恒可交互) */
|
||||||
@@ -129,7 +140,13 @@ const assigneeSelectOptions = computed(() => {
|
|||||||
const options = props.userOptions.map(item => ({ id: item.id, nickname: item.nickname }));
|
const options = props.userOptions.map(item => ({ id: item.id, nickname: item.nickname }));
|
||||||
const known = new Set(options.map(item => item.id));
|
const known = new Set(options.map(item => item.id));
|
||||||
|
|
||||||
if (props.mode !== 'create' && props.rowData) {
|
if (props.mode === 'create') {
|
||||||
|
const ownerId = model.ownerId;
|
||||||
|
if (ownerId && !known.has(ownerId)) {
|
||||||
|
options.push({ id: ownerId, nickname: props.executionData?.ownerNickname || ownerId });
|
||||||
|
known.add(ownerId);
|
||||||
|
}
|
||||||
|
} else if (props.rowData) {
|
||||||
props.rowData.assignees?.forEach(assignee => {
|
props.rowData.assignees?.forEach(assignee => {
|
||||||
if (assignee.userId && !known.has(assignee.userId)) {
|
if (assignee.userId && !known.has(assignee.userId)) {
|
||||||
options.push({ id: assignee.userId, nickname: assignee.nickname || assignee.userId });
|
options.push({ id: assignee.userId, nickname: assignee.nickname || assignee.userId });
|
||||||
@@ -311,6 +328,23 @@ function handleAssigneeChange(value: string[]) {
|
|||||||
model.assigneeUserIds = cleaned;
|
model.assigneeUserIds = cleaned;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fillFromExecution() {
|
||||||
|
const execution = props.executionData;
|
||||||
|
if (!execution) return;
|
||||||
|
|
||||||
|
model.taskTitle = execution.executionName || '';
|
||||||
|
model.ownerId = execution.ownerId || null;
|
||||||
|
model.plannedStartDate = execution.plannedStartDate || null;
|
||||||
|
model.plannedEndDate = execution.plannedEndDate || null;
|
||||||
|
model.priority = execution.priority || '3';
|
||||||
|
model.taskDesc = execution.executionDesc || null;
|
||||||
|
model.assigneeUserIds = props.userOptions.map(item => item.id);
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
formRef.value?.clearValidate(['taskTitle', 'ownerId', 'plannedStartDate', 'plannedEndDate', 'priority']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function applyBasicFieldsFromRow(row: Api.Project.ProjectTask | null) {
|
function applyBasicFieldsFromRow(row: Api.Project.ProjectTask | null) {
|
||||||
model.taskTitle = row?.taskTitle || '';
|
model.taskTitle = row?.taskTitle || '';
|
||||||
model.type = row?.type || '';
|
model.type = row?.type || '';
|
||||||
@@ -386,6 +420,19 @@ defineExpose({
|
|||||||
<div class="task-operate-dialog__grid">
|
<div class="task-operate-dialog__grid">
|
||||||
<div ref="leftColRef" class="task-operate-dialog__col-left">
|
<div ref="leftColRef" class="task-operate-dialog__col-left">
|
||||||
<BusinessFormSection title="任务信息">
|
<BusinessFormSection title="任务信息">
|
||||||
|
<template v-if="canFillFromExecution" #title>
|
||||||
|
<span class="task-operate-dialog__section-title">
|
||||||
|
<span>任务信息</span>
|
||||||
|
<ElTooltip content="一键带出当前执行信息" placement="top">
|
||||||
|
<ElButton text type="primary" class="task-operate-dialog__title-action" @click="fillFromExecution">
|
||||||
|
<template #icon>
|
||||||
|
<IconMdiAutoFix class="text-16px" />
|
||||||
|
</template>
|
||||||
|
</ElButton>
|
||||||
|
</ElTooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<ElFormItem label="任务名称" prop="taskTitle">
|
<ElFormItem label="任务名称" prop="taskTitle">
|
||||||
<ElInput v-model="model.taskTitle" maxlength="200" placeholder="请输入任务名称" />
|
<ElInput v-model="model.taskTitle" maxlength="200" placeholder="请输入任务名称" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -526,6 +573,23 @@ defineExpose({
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.task-operate-dialog__section-title {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-operate-dialog__title-action {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-operate-dialog__title-action:hover {
|
||||||
|
background-color: var(--el-color-primary-light-9);
|
||||||
|
}
|
||||||
|
|
||||||
.task-operate-dialog__grid {
|
.task-operate-dialog__grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 360px 1fr;
|
grid-template-columns: 360px 1fr;
|
||||||
|
|||||||
@@ -37,9 +37,7 @@ const isActiveAssignee = computed(() =>
|
|||||||
const canSubmitWorklog = computed(() => {
|
const canSubmitWorklog = computed(() => {
|
||||||
if (!props.task || !currentUserId.value) return false;
|
if (!props.task || !currentUserId.value) return false;
|
||||||
if (!isOwner.value && !isActiveAssignee.value) return false;
|
if (!isOwner.value && !isActiveAssignee.value) return false;
|
||||||
return (
|
return props.task.statusCode === 'pending' || props.task.statusCode === 'active';
|
||||||
props.task.statusCode === 'pending' || props.task.statusCode === 'active' || props.task.statusCode === 'completed'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const records = ref<Api.Project.TaskWorklog[]>([]);
|
const records = ref<Api.Project.TaskWorklog[]>([]);
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ const isOwner = computed(() => Boolean(props.taskOwnerId && props.taskOwnerId ==
|
|||||||
const isView = computed(() => props.mode === 'view');
|
const isView = computed(() => props.mode === 'view');
|
||||||
// 任务 completed 后填工时不回写进度(§4.8.4 备注),此时进度字段只读,避免误导用户以为能改任务进度
|
// 任务 completed 后填工时不回写进度(§4.8.4 备注),此时进度字段只读,避免误导用户以为能改任务进度
|
||||||
const isProgressReadonly = computed(() => isView.value || props.taskStatusCode === 'completed');
|
const isProgressReadonly = computed(() => isView.value || props.taskStatusCode === 'completed');
|
||||||
|
// 任务 completed 时编辑模式:除工作内容和附件外,其他字段全部禁用
|
||||||
|
const isTaskCompleted = computed(() => props.taskStatusCode === 'completed');
|
||||||
|
const isFieldDisabled = computed(() => isView.value || isTaskCompleted.value);
|
||||||
|
|
||||||
const { formRef, validate } = useForm();
|
const { formRef, validate } = useForm();
|
||||||
const { createRequiredRule } = useFormRules();
|
const { createRequiredRule } = useFormRules();
|
||||||
@@ -373,7 +376,7 @@ defineExpose({
|
|||||||
<ElRow :gutter="16">
|
<ElRow :gutter="16">
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem label="填报粒度" prop="granularity">
|
<ElFormItem label="填报粒度" prop="granularity">
|
||||||
<ElSegmented v-model="model.granularity" :options="granularityOptions" :disabled="isView" />
|
<ElSegmented v-model="model.granularity" :options="granularityOptions" :disabled="isFieldDisabled" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
@@ -384,8 +387,8 @@ defineExpose({
|
|||||||
type="date"
|
type="date"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
placeholder="选择工作日期"
|
placeholder="选择工作日期"
|
||||||
:shortcuts="isView ? undefined : workDateShortcuts"
|
:shortcuts="isFieldDisabled ? undefined : workDateShortcuts"
|
||||||
:disabled="isView"
|
:disabled="isFieldDisabled"
|
||||||
class="task-worklog-form-dialog__date-picker"
|
class="task-worklog-form-dialog__date-picker"
|
||||||
/>
|
/>
|
||||||
<ElTooltip v-else :content="weekRangeTooltip" :disabled="!weekRangeTooltip" placement="top">
|
<ElTooltip v-else :content="weekRangeTooltip" :disabled="!weekRangeTooltip" placement="top">
|
||||||
@@ -395,8 +398,8 @@ defineExpose({
|
|||||||
type="week"
|
type="week"
|
||||||
format="YYYY[年第]ww[周]"
|
format="YYYY[年第]ww[周]"
|
||||||
placeholder="选择工作周次"
|
placeholder="选择工作周次"
|
||||||
:shortcuts="isView ? undefined : weekDateShortcuts"
|
:shortcuts="isFieldDisabled ? undefined : weekDateShortcuts"
|
||||||
:disabled="isView"
|
:disabled="isFieldDisabled"
|
||||||
class="task-worklog-form-dialog__date-picker"
|
class="task-worklog-form-dialog__date-picker"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -411,7 +414,7 @@ defineExpose({
|
|||||||
:step="0.5"
|
:step="0.5"
|
||||||
:precision="1"
|
:precision="1"
|
||||||
:placeholder="durationPlaceholder"
|
:placeholder="durationPlaceholder"
|
||||||
:disabled="isView"
|
:disabled="isFieldDisabled"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
@@ -437,7 +440,7 @@ defineExpose({
|
|||||||
v-model="model.difficulty"
|
v-model="model.difficulty"
|
||||||
:dict-code="RDMS_WORKLOG_DIFFICULTY_DICT_CODE"
|
:dict-code="RDMS_WORKLOG_DIFFICULTY_DICT_CODE"
|
||||||
placeholder="请选择完成难度"
|
placeholder="请选择完成难度"
|
||||||
:disabled="isView"
|
:disabled="isFieldDisabled"
|
||||||
show-remark
|
show-remark
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ function canEditRow(row: Api.Project.TaskWorklog) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function canDeleteRow(row: Api.Project.TaskWorklog) {
|
function canDeleteRow(row: Api.Project.TaskWorklog) {
|
||||||
return Boolean(isWorklogMutableStatus.value && currentUserId.value && row.userId === currentUserId.value);
|
return Boolean(props.taskStatusCode === 'active' && currentUserId.value && row.userId === currentUserId.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatHours(hours: number | null | undefined) {
|
function formatHours(hours: number | null | undefined) {
|
||||||
|
|||||||
@@ -959,6 +959,7 @@ defineExpose({
|
|||||||
v-model:visible="operateVisible"
|
v-model:visible="operateVisible"
|
||||||
:mode="operateMode"
|
:mode="operateMode"
|
||||||
:row-data="currentTask"
|
:row-data="currentTask"
|
||||||
|
:execution-data="props.execution"
|
||||||
:default-parent-task-id="presetParentTaskId"
|
:default-parent-task-id="presetParentTaskId"
|
||||||
:user-options="executionAssigneeOptions"
|
:user-options="executionAssigneeOptions"
|
||||||
:task-options="taskOptions"
|
:task-options="taskOptions"
|
||||||
|
|||||||
Reference in New Issue
Block a user