38 changed files with 1990 additions and 2 deletions
@ -0,0 +1,15 @@ |
|||
<script lang="ts" setup> |
|||
import { Page } from '@vben/common-ui'; |
|||
|
|||
import { AuditLogTable } from '@abp/auditing'; |
|||
|
|||
defineOptions({ |
|||
name: 'AuditingAuditLogs', |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<Page> |
|||
<AuditLogTable /> |
|||
</Page> |
|||
</template> |
|||
@ -0,0 +1,37 @@ |
|||
{ |
|||
"name": "@abp/auditing", |
|||
"version": "8.3.2", |
|||
"homepage": "https://github.com/colinin/abp-next-admin", |
|||
"bugs": "https://github.com/colinin/abp-next-admin/issues", |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "git+https://github.com/colinin/abp-next-admin.git", |
|||
"directory": "packages/@abp/auditing" |
|||
}, |
|||
"license": "MIT", |
|||
"type": "module", |
|||
"sideEffects": [ |
|||
"**/*.css" |
|||
], |
|||
"exports": { |
|||
".": { |
|||
"types": "./src/index.ts", |
|||
"default": "./src/index.ts" |
|||
} |
|||
}, |
|||
"dependencies": { |
|||
"@abp/core": "workspace:*", |
|||
"@abp/request": "workspace:*", |
|||
"@abp/ui": "workspace:*", |
|||
"@ant-design/icons-vue": "catalog:", |
|||
"@vben/access": "workspace:*", |
|||
"@vben/common-ui": "workspace:*", |
|||
"@vben/hooks": "workspace:*", |
|||
"@vben/icons": "workspace:*", |
|||
"@vben/layouts": "workspace:*", |
|||
"@vben/locales": "workspace:*", |
|||
"ant-design-vue": "catalog:", |
|||
"vue": "catalog:*", |
|||
"vxe-table": "catalog:" |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
import type { PagedResultDto } from '@abp/core'; |
|||
|
|||
import type { AuditLogDto, AuditLogGetListInput } from '../types/audit-logs'; |
|||
|
|||
import { requestClient } from '@abp/request'; |
|||
|
|||
/** |
|||
* 获取审计日志 |
|||
* @param id 日志id |
|||
*/ |
|||
export function getApi(id: string): Promise<AuditLogDto> { |
|||
return requestClient.get<AuditLogDto>(`/api/auditing/audit-log/${id}`); |
|||
} |
|||
|
|||
/** |
|||
* 获取审计日志分页列表 |
|||
* @param input 参数 |
|||
*/ |
|||
export function getPagedListApi( |
|||
input: AuditLogGetListInput, |
|||
): Promise<PagedResultDto<AuditLogDto>> { |
|||
return requestClient.get<PagedResultDto<AuditLogDto>>( |
|||
'/api/auditing/audit-log', |
|||
{ |
|||
params: input, |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 删除审计日志 |
|||
* @param id 日志id |
|||
*/ |
|||
export function deleteApi(id: string): Promise<void> { |
|||
return requestClient.delete(`/api/auditing/audit-log/${id}`); |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
import type { ListResultDto } from '@abp/core'; |
|||
|
|||
import type { |
|||
EntityChangeGetWithUsernameInput, |
|||
EntityChangeWithUsernameDto, |
|||
} from '../types/entity-changes'; |
|||
|
|||
import { requestClient } from '@abp/request'; |
|||
|
|||
/** |
|||
* 获取包含用户名称的实体变更列表 |
|||
* @param input 参数 |
|||
*/ |
|||
export function getListWithUsernameApi( |
|||
input: EntityChangeGetWithUsernameInput, |
|||
): Promise<ListResultDto<EntityChangeWithUsernameDto>> { |
|||
return requestClient.get<ListResultDto<EntityChangeWithUsernameDto>>( |
|||
'/api/auditing/entity-changes/with-username', |
|||
{ |
|||
params: input, |
|||
}, |
|||
); |
|||
} |
|||
@ -0,0 +1,240 @@ |
|||
<script setup lang="ts"> |
|||
import type { VxeGridProps } from 'vxe-table'; |
|||
|
|||
import type { Action, AuditLogDto } from '../../types/audit-logs'; |
|||
|
|||
import { ref } from 'vue'; |
|||
|
|||
import { useVbenDrawer } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { formatToDateTime } from '@abp/core'; |
|||
import { CodeEditor, MODE, useVbenVxeGrid } from '@abp/ui'; |
|||
import { Descriptions, Tabs, Tag } from 'ant-design-vue'; |
|||
|
|||
import { getApi } from '../../api/audit-logs'; |
|||
import { useAuditlogs } from '../../hooks/useAuditlogs'; |
|||
import EntityChangeTable from '../entity-changes/EntityChangeTable.vue'; |
|||
|
|||
defineOptions({ |
|||
name: 'AuditLogDrawer', |
|||
}); |
|||
|
|||
const TabPane = Tabs.TabPane; |
|||
const DescriptionsItem = Descriptions.Item; |
|||
|
|||
const activedTab = ref('basic'); |
|||
const auditLogModel = ref<AuditLogDto>({} as AuditLogDto); |
|||
|
|||
const { getHttpMethodColor, getHttpStatusCodeColor } = useAuditlogs(); |
|||
const [Drawer, drawerApi] = useVbenDrawer({ |
|||
class: 'w-auto', |
|||
onCancel() { |
|||
drawerApi.close(); |
|||
}, |
|||
onConfirm: async () => {}, |
|||
onOpenChange: async (isOpen: boolean) => { |
|||
if (isOpen) { |
|||
try { |
|||
auditLogModel.value = {} as AuditLogDto; |
|||
drawerApi.setState({ loading: true }); |
|||
const dto = drawerApi.getData<AuditLogDto>(); |
|||
await onGet(dto.id); |
|||
} finally { |
|||
drawerApi.setState({ loading: false }); |
|||
} |
|||
} |
|||
}, |
|||
title: $t('AbpAuditLogging.AuditLog'), |
|||
}); |
|||
/** 调用方法表格配置 */ |
|||
const actionsGridOptions: VxeGridProps<Action> = { |
|||
border: true, |
|||
columns: [ |
|||
{ |
|||
align: 'left', |
|||
field: 'parameters', |
|||
slots: { |
|||
content: 'parameters', |
|||
}, |
|||
type: 'expand', |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'serviceName', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.ServiceName'), |
|||
width: 'auto', |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'methodName', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.MethodName'), |
|||
width: 150, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'executionTime', |
|||
formatter: ({ cellValue }) => { |
|||
return cellValue ? formatToDateTime(cellValue) : cellValue; |
|||
}, |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.ExecutionTime'), |
|||
width: 200, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'executionDuration', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.ExecutionDuration'), |
|||
width: 150, |
|||
}, |
|||
], |
|||
expandConfig: { |
|||
padding: true, |
|||
trigger: 'row', |
|||
}, |
|||
exportConfig: {}, |
|||
keepSource: true, |
|||
pagerConfig: { |
|||
enabled: false, |
|||
}, |
|||
proxyConfig: { |
|||
ajax: { |
|||
query: () => { |
|||
return Promise.resolve(auditLogModel.value.actions); |
|||
}, |
|||
}, |
|||
response: { |
|||
list: ({ data }) => { |
|||
return data; |
|||
}, |
|||
}, |
|||
}, |
|||
toolbarConfig: { |
|||
enabled: false, |
|||
}, |
|||
}; |
|||
/** 调用方法表格 */ |
|||
const [ActionsGrid] = useVbenVxeGrid({ |
|||
gridOptions: actionsGridOptions, |
|||
}); |
|||
/** 查询审计日志 */ |
|||
async function onGet(id: string) { |
|||
const dto = await getApi(id); |
|||
auditLogModel.value = dto; |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Drawer> |
|||
<div style="width: 800px"> |
|||
<Tabs v-model="activedTab"> |
|||
<TabPane key="basic" :tab="$t('AbpAuditLogging.Operation')"> |
|||
<Descriptions :colon="false" :column="2" bordered size="small"> |
|||
<DescriptionsItem :label="$t('AbpAuditLogging.ApplicationName')"> |
|||
{{ auditLogModel.applicationName }} |
|||
</DescriptionsItem> |
|||
<DescriptionsItem :label="$t('AbpAuditLogging.ExecutionTime')"> |
|||
{{ formatToDateTime(auditLogModel.executionTime) }} |
|||
</DescriptionsItem> |
|||
<DescriptionsItem :label="$t('AbpAuditLogging.UserName')"> |
|||
{{ auditLogModel.userName }} |
|||
</DescriptionsItem> |
|||
<DescriptionsItem :label="$t('AbpAuditLogging.HttpMethod')"> |
|||
<Tag :color="getHttpMethodColor(auditLogModel.httpMethod)"> |
|||
{{ auditLogModel.httpMethod }} |
|||
</Tag> |
|||
</DescriptionsItem> |
|||
<DescriptionsItem |
|||
:label="$t('AbpAuditLogging.RequestUrl')" |
|||
:span="2" |
|||
> |
|||
{{ auditLogModel.url }} |
|||
</DescriptionsItem> |
|||
<DescriptionsItem :label="$t('AbpAuditLogging.HttpStatusCode')"> |
|||
<Tag |
|||
:color="getHttpStatusCodeColor(auditLogModel.httpStatusCode)" |
|||
> |
|||
{{ auditLogModel.httpStatusCode }} |
|||
</Tag> |
|||
</DescriptionsItem> |
|||
<DescriptionsItem :label="$t('AbpAuditLogging.ExecutionDuration')"> |
|||
{{ auditLogModel.executionDuration }} |
|||
</DescriptionsItem> |
|||
<DescriptionsItem :label="$t('AbpAuditLogging.ClientId')"> |
|||
{{ auditLogModel.clientId }} |
|||
</DescriptionsItem> |
|||
<DescriptionsItem :label="$t('AbpAuditLogging.ClientIpAddress')"> |
|||
{{ auditLogModel.clientIpAddress }} |
|||
</DescriptionsItem> |
|||
<DescriptionsItem :label="$t('AbpAuditLogging.ClientName')"> |
|||
{{ auditLogModel.clientName }} |
|||
</DescriptionsItem> |
|||
<DescriptionsItem :label="$t('AbpAuditLogging.CorrelationId')"> |
|||
{{ auditLogModel.correlationId }} |
|||
</DescriptionsItem> |
|||
<DescriptionsItem |
|||
:label="$t('AbpAuditLogging.BrowserInfo')" |
|||
:label-style="{ width: '110px' }" |
|||
:span="2" |
|||
> |
|||
{{ auditLogModel.browserInfo }} |
|||
</DescriptionsItem> |
|||
<DescriptionsItem :label="$t('AbpAuditLogging.Comments')" :span="2"> |
|||
{{ auditLogModel.comments }} |
|||
</DescriptionsItem> |
|||
<DescriptionsItem |
|||
:label="$t('AbpAuditLogging.Exception')" |
|||
:span="2" |
|||
> |
|||
{{ auditLogModel.exceptions }} |
|||
</DescriptionsItem> |
|||
<DescriptionsItem |
|||
:label="$t('AbpAuditLogging.Additional')" |
|||
:span="2" |
|||
> |
|||
{{ auditLogModel.extraProperties }} |
|||
</DescriptionsItem> |
|||
</Descriptions> |
|||
</TabPane> |
|||
<TabPane |
|||
v-if="auditLogModel.actions?.length" |
|||
key="opera" |
|||
:tab="`${$t('AbpAuditLogging.InvokeMethod')}(${auditLogModel.actions?.length})`" |
|||
> |
|||
<ActionsGrid> |
|||
<template #parameters="{ row }"> |
|||
<Descriptions :colon="false" :column="1" bordered size="small"> |
|||
<DescriptionsItem :label="$t('AbpAuditLogging.Parameters')"> |
|||
<CodeEditor |
|||
:mode="MODE.JSON" |
|||
:value="row.parameters" |
|||
readonly |
|||
/> |
|||
</DescriptionsItem> |
|||
<DescriptionsItem :label="$t('AbpAuditLogging.Additional')"> |
|||
<CodeEditor |
|||
:mode="MODE.JSON" |
|||
:value="row.extraProperties" |
|||
readonly |
|||
/> |
|||
</DescriptionsItem> |
|||
</Descriptions> |
|||
</template> |
|||
</ActionsGrid> |
|||
</TabPane> |
|||
<TabPane |
|||
v-if="auditLogModel.entityChanges?.length" |
|||
key="changes" |
|||
:tab="`${$t('AbpAuditLogging.EntitiesChanged')}(${auditLogModel.entityChanges?.length})`" |
|||
> |
|||
<EntityChangeTable :data="auditLogModel.entityChanges" /> |
|||
</TabPane> |
|||
</Tabs> |
|||
</div> |
|||
</Drawer> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,326 @@ |
|||
<script setup lang="ts"> |
|||
import type { SortOrder } from '@abp/core'; |
|||
import type { VbenFormProps, VxeGridListeners, VxeGridProps } from '@abp/ui'; |
|||
|
|||
import type { AuditLogDto } from '../../types/audit-logs'; |
|||
|
|||
import { defineAsyncComponent, h, nextTick } from 'vue'; |
|||
|
|||
import { useVbenDrawer } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { formatToDateTime } from '@abp/core'; |
|||
import { useVbenVxeGrid } from '@abp/ui'; |
|||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue'; |
|||
import { Button, Popconfirm, Tag } from 'ant-design-vue'; |
|||
|
|||
import { deleteApi, getPagedListApi } from '../../api/audit-logs'; |
|||
import { AuditLogPermissions } from '../../constants/permissions'; |
|||
import { useAuditlogs } from '../../hooks/useAuditlogs'; |
|||
import { httpMethodOptions, httpStatusCodeOptions } from './mapping'; |
|||
|
|||
defineOptions({ |
|||
name: 'AuditLogTable', |
|||
}); |
|||
|
|||
const formOptions: VbenFormProps = { |
|||
// 默认展开 |
|||
collapsed: true, |
|||
collapsedRows: 2, |
|||
fieldMappingTime: [ |
|||
[ |
|||
'executionTime', |
|||
['startTime', 'endTime'], |
|||
['YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD HH:mm:ss'], |
|||
], |
|||
], |
|||
schema: [ |
|||
{ |
|||
component: 'RangePicker', |
|||
fieldName: 'executionTime', |
|||
formItemClass: 'col-span-2 items-baseline', |
|||
label: $t('AbpAuditLogging.ExecutionTime'), |
|||
}, |
|||
{ |
|||
component: 'Input', |
|||
fieldName: 'url', |
|||
formItemClass: 'col-span-2 items-baseline', |
|||
label: $t('AbpAuditLogging.RequestUrl'), |
|||
}, |
|||
{ |
|||
component: 'Select', |
|||
componentProps: { |
|||
options: httpStatusCodeOptions, |
|||
}, |
|||
fieldName: 'httpStatusCode', |
|||
label: $t('AbpAuditLogging.HttpStatusCode'), |
|||
}, |
|||
{ |
|||
component: 'Select', |
|||
componentProps: { |
|||
options: httpMethodOptions, |
|||
}, |
|||
fieldName: 'httpMethod', |
|||
label: $t('AbpAuditLogging.HttpMethod'), |
|||
}, |
|||
{ |
|||
component: 'Input', |
|||
fieldName: 'applicationName', |
|||
label: $t('AbpAuditLogging.ApplicationName'), |
|||
}, |
|||
{ |
|||
component: 'Input', |
|||
fieldName: 'userName', |
|||
label: $t('AbpAuditLogging.UserName'), |
|||
}, |
|||
{ |
|||
component: 'Input', |
|||
fieldName: 'clientId', |
|||
label: $t('AbpAuditLogging.ClientId'), |
|||
}, |
|||
{ |
|||
component: 'Input', |
|||
fieldName: 'clientIpAddress', |
|||
label: $t('AbpAuditLogging.ClientIpAddress'), |
|||
}, |
|||
{ |
|||
component: 'InputNumber', |
|||
fieldName: 'minExecutionDuration', |
|||
label: $t('AbpAuditLogging.MinExecutionDuration'), |
|||
labelWidth: 150, |
|||
}, |
|||
{ |
|||
component: 'InputNumber', |
|||
fieldName: 'maxExecutionDuration', |
|||
label: $t('AbpAuditLogging.MaxExecutionDuration'), |
|||
labelWidth: 150, |
|||
}, |
|||
{ |
|||
component: 'Input', |
|||
fieldName: 'correlationId', |
|||
formItemClass: 'col-span-2 items-baseline', |
|||
label: $t('AbpAuditLogging.CorrelationId'), |
|||
}, |
|||
{ |
|||
component: 'Checkbox', |
|||
componentProps: { |
|||
render: () => { |
|||
return h('span', $t('AbpAuditLogging.HasException')); |
|||
}, |
|||
}, |
|||
fieldName: 'hasException', |
|||
label: $t('AbpAuditLogging.HasException'), |
|||
}, |
|||
], |
|||
// 控制表单是否显示折叠按钮 |
|||
showCollapseButton: true, |
|||
// 按下回车时是否提交表单 |
|||
submitOnEnter: true, |
|||
wrapperClass: 'grid-cols-4', |
|||
}; |
|||
|
|||
const gridOptions: VxeGridProps<AuditLogDto> = { |
|||
columns: [ |
|||
{ |
|||
align: 'left', |
|||
field: 'url', |
|||
slots: { default: 'url' }, |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.RequestUrl'), |
|||
width: 500, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'userName', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.UserName'), |
|||
width: 120, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'executionTime', |
|||
formatter: ({ cellValue }) => { |
|||
return cellValue ? formatToDateTime(cellValue) : cellValue; |
|||
}, |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.ExecutionTime'), |
|||
width: 150, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'executionDuration', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.ExecutionDuration'), |
|||
width: 140, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'clientId', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.ClientId'), |
|||
width: 150, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'clientIpAddress', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.ClientIpAddress'), |
|||
width: 150, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'applicationName', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.ApplicationName'), |
|||
width: 160, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'correlationId', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.CorrelationId'), |
|||
width: 160, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'tenantName', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.TenantName'), |
|||
width: 100, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'browserInfo', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.BrowserInfo'), |
|||
width: 300, |
|||
}, |
|||
{ |
|||
field: 'action', |
|||
fixed: 'right', |
|||
slots: { default: 'action' }, |
|||
title: $t('AbpUi.Actions'), |
|||
width: 220, |
|||
}, |
|||
], |
|||
exportConfig: {}, |
|||
keepSource: true, |
|||
proxyConfig: { |
|||
ajax: { |
|||
query: async ({ page }, formValues) => { |
|||
return await getPagedListApi({ |
|||
maxResultCount: page.pageSize, |
|||
skipCount: (page.currentPage - 1) * page.pageSize, |
|||
...formValues, |
|||
}); |
|||
}, |
|||
}, |
|||
response: { |
|||
total: 'totalCount', |
|||
list: 'items', |
|||
}, |
|||
}, |
|||
sortConfig: { |
|||
remote: true, |
|||
}, |
|||
toolbarConfig: { |
|||
custom: true, |
|||
export: true, |
|||
// import: true, |
|||
refresh: true, |
|||
zoom: true, |
|||
}, |
|||
}; |
|||
|
|||
const gridEvents: VxeGridListeners<AuditLogDto> = { |
|||
sortChange: onSort, |
|||
}; |
|||
const [Grid, { formApi, query }] = useVbenVxeGrid({ |
|||
formOptions, |
|||
gridEvents, |
|||
gridOptions, |
|||
}); |
|||
const { getHttpMethodColor, getHttpStatusCodeColor } = useAuditlogs(); |
|||
const [AuditLogDrawer, logDrawerApi] = useVbenDrawer({ |
|||
connectedComponent: defineAsyncComponent( |
|||
() => import('./AuditLogDrawer.vue'), |
|||
), |
|||
}); |
|||
|
|||
function onUpdate(row: AuditLogDto) { |
|||
logDrawerApi.setData(row); |
|||
logDrawerApi.open(); |
|||
} |
|||
|
|||
async function onDelete(row: AuditLogDto) { |
|||
await deleteApi(row.id).then(() => query()); |
|||
} |
|||
|
|||
function onSort(params: { field: string; order: SortOrder }) { |
|||
const sorting = params.order ? `${params.field} ${params.order}` : undefined; |
|||
query({ sorting }); |
|||
} |
|||
|
|||
function onFilter(field: string, value: any) { |
|||
nextTick(() => { |
|||
formApi.setFieldValue(field, value); |
|||
formApi.validateAndSubmitForm(); |
|||
}); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Grid :table-title="$t('AbpAuditLogging.AuditLog')"> |
|||
<template #url="{ row }"> |
|||
<Tag |
|||
:color="getHttpStatusCodeColor(row.httpStatusCode)" |
|||
@click="onFilter('httpStatusCode', row.httpStatusCode)" |
|||
> |
|||
{{ row.httpStatusCode }} |
|||
</Tag> |
|||
<Tag |
|||
:color="getHttpMethodColor(row.httpMethod)" |
|||
style="margin-left: 5px" |
|||
@click="onFilter('httpMethod', row.httpMethod)" |
|||
> |
|||
{{ row.httpMethod }} |
|||
</Tag> |
|||
<a |
|||
class="link" |
|||
href="javaScript:void(0);" |
|||
@click="onFilter('url', row.url)" |
|||
>{{ row.url }} |
|||
</a> |
|||
</template> |
|||
<template #action="{ row }"> |
|||
<div class="flex flex-row"> |
|||
<Button |
|||
:icon="h(EditOutlined)" |
|||
block |
|||
type="link" |
|||
v-access:code="[AuditLogPermissions.Default]" |
|||
@click="onUpdate(row)" |
|||
> |
|||
{{ $t('AbpAuditLogging.ShowLogDialog') }} |
|||
</Button> |
|||
<Popconfirm |
|||
:title="$t('AbpUi.ItemWillBeDeletedMessage')" |
|||
@confirm="onDelete(row)" |
|||
> |
|||
<Button |
|||
:icon="h(DeleteOutlined)" |
|||
block |
|||
danger |
|||
type="link" |
|||
v-access:code="[AuditLogPermissions.Delete]" |
|||
> |
|||
{{ $t('AbpUi.Delete') }} |
|||
</Button> |
|||
</Popconfirm> |
|||
</div> |
|||
</template> |
|||
</Grid> |
|||
<AuditLogDrawer /> |
|||
</template> |
|||
|
|||
<style lang="scss" scoped></style> |
|||
@ -0,0 +1,57 @@ |
|||
const httpMethodOptions = [ |
|||
{ label: 'GET', value: 'GET' }, |
|||
{ label: 'PUT', value: 'PUT' }, |
|||
{ label: 'POST', value: 'POST' }, |
|||
{ label: 'PATCH', value: 'PATCH' }, |
|||
{ label: 'DELETE', value: 'DELETE' }, |
|||
{ label: 'OPTIONS', value: 'OPTIONS' }, |
|||
{ label: 'HEAD', value: 'HEAD' }, |
|||
]; |
|||
|
|||
const httpStatusCodeOptions = [ |
|||
{ label: '100 - Continue', value: 100 }, |
|||
{ label: '101 - Switching Protocols', value: 101 }, |
|||
{ label: '200 - OK', value: 200 }, |
|||
{ label: '201 - Created', value: 201 }, |
|||
{ label: '202 - Accepted', value: 202 }, |
|||
{ label: '203 - Non Authoritative Information', value: 203 }, |
|||
{ label: '204 - No Content', value: 204 }, |
|||
{ label: '205 - Reset Content', value: 205 }, |
|||
{ label: '206 - Partial Content', value: 206 }, |
|||
{ label: '300 - Multiple Choices', value: 300 }, |
|||
{ label: '301 - Moved Permanently', value: 301 }, |
|||
{ label: '302 - Found & Redirect', value: 302 }, |
|||
{ label: '303 - See Other', value: 303 }, |
|||
{ label: '304 - Not Modified', value: 304 }, |
|||
{ label: '305 - Use Proxy', value: 305 }, |
|||
{ label: '306 - Switch Proxy', value: 306 }, |
|||
{ label: '307 - Temporary Redirect', value: 307 }, |
|||
{ label: '308 - Permanent Redirect', value: 308 }, |
|||
{ label: '400 - Bad Request', value: 400 }, |
|||
{ label: '401 - Unauthorized', value: 401 }, |
|||
{ label: '402 - Payment Required', value: 402 }, |
|||
{ label: '403 - Forbidden', value: 403 }, |
|||
{ label: '404 - Not Found', value: 404 }, |
|||
{ label: '405 - Method Not Allowed', value: 405 }, |
|||
{ label: '406 - Not Acceptable', value: 406 }, |
|||
{ label: '407 - Proxy Authentication Required', value: 407 }, |
|||
{ label: '408 - Request Timeout', value: 408 }, |
|||
{ label: '409 - Conflict', value: 409 }, |
|||
{ label: '410 - Gone', value: 410 }, |
|||
{ label: '411 - Length Required', value: 411 }, |
|||
{ label: '412 - Precondition Failed', value: 412 }, |
|||
{ label: '413 - Request Entity Too Large', value: 413 }, |
|||
{ label: '414 - Request Uri Too Long', value: 414 }, |
|||
{ label: '415 - Unsupported Media Type', value: 415 }, |
|||
{ label: '416 - Requested Range Not Satisfiable', value: 416 }, |
|||
{ label: '417 - Expectation Failed', value: 417 }, |
|||
{ label: '426 - Upgrade Required', value: 426 }, |
|||
{ label: '500 - Internal Server Error', value: 500 }, |
|||
{ label: '501 - Not mplemented', value: 501 }, |
|||
{ label: '502 - Bad Gateway', value: 502 }, |
|||
{ label: '503 - Service Unavailable', value: 503 }, |
|||
{ label: '504 - Gateway Timeout', value: 504 }, |
|||
{ label: '505 - Http Version Not Supported', value: 505 }, |
|||
]; |
|||
|
|||
export { httpMethodOptions, httpStatusCodeOptions }; |
|||
@ -0,0 +1,45 @@ |
|||
<script setup lang="ts"> |
|||
import type { |
|||
EntityChangeDto, |
|||
EntityChangeGetWithUsernameInput, |
|||
} from '../../types/entity-changes'; |
|||
|
|||
import { ref } from 'vue'; |
|||
|
|||
import { useVbenDrawer } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { getListWithUsernameApi } from '../../api/entity-changes'; |
|||
import EntityChangeTable from '../entity-changes/EntityChangeTable.vue'; |
|||
|
|||
const entityChanges = ref<EntityChangeDto[]>([]); |
|||
|
|||
const [Drawer, drawerApi] = useVbenDrawer({ |
|||
class: 'w-auto', |
|||
onCancel() { |
|||
drawerApi.close(); |
|||
}, |
|||
onConfirm: async () => {}, |
|||
onOpenChange: async (isOpen: boolean) => { |
|||
if (isOpen) { |
|||
const input = drawerApi.getData<EntityChangeGetWithUsernameInput>(); |
|||
const { items } = await getListWithUsernameApi(input); |
|||
entityChanges.value = items.map((item) => { |
|||
return { |
|||
...item.entityChange, |
|||
userName: item.userName, |
|||
}; |
|||
}); |
|||
} |
|||
}, |
|||
title: $t('AbpAuditLogging.EntitiesChanged'), |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<Drawer> |
|||
<EntityChangeTable :data="entityChanges" show-user-name /> |
|||
</Drawer> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,172 @@ |
|||
<script setup lang="ts"> |
|||
import type { VxeGridDefines, VxeGridPropTypes } from 'vxe-table'; |
|||
|
|||
import type { |
|||
EntityChangeDto, |
|||
PropertyChange, |
|||
} from '../../types/entity-changes'; |
|||
|
|||
import { computed, reactive, watchEffect } from 'vue'; |
|||
|
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { formatToDateTime } from '@abp/core'; |
|||
import { Tag } from 'ant-design-vue'; |
|||
import { VxeGrid } from 'vxe-table'; |
|||
|
|||
import { useAuditlogs } from '../../hooks/useAuditlogs'; |
|||
|
|||
defineOptions({ |
|||
name: 'EntityChangeTable', |
|||
}); |
|||
|
|||
const props = defineProps<{ |
|||
data: EntityChangeDto[]; |
|||
showUserName?: boolean; |
|||
}>(); |
|||
|
|||
const { getChangeTypeColor, getChangeTypeValue } = useAuditlogs(); |
|||
|
|||
/** 实体变更表格分页配置 */ |
|||
const pagerConfig = reactive<VxeGridPropTypes.PagerConfig>({ |
|||
currentPage: 1, |
|||
pageSize: 10, |
|||
pageSizes: [10, 25, 50, 100], |
|||
total: 0, |
|||
}); |
|||
/** 实体变更表格数据列配置 */ |
|||
const columnsConfig = reactive<VxeGridPropTypes.Columns<EntityChangeDto>>([ |
|||
{ |
|||
align: 'left', |
|||
field: 'propertyChanges', |
|||
slots: { |
|||
content: 'changes', |
|||
}, |
|||
type: 'expand', |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'userName', |
|||
title: $t('AbpAuditLogging.UserName'), |
|||
visible: props.showUserName, |
|||
width: 100, |
|||
}, |
|||
{ |
|||
align: 'center', |
|||
field: 'changeType', |
|||
slots: { default: 'type' }, |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.ChangeType'), |
|||
width: 100, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'changeTime', |
|||
formatter: ({ cellValue }) => { |
|||
return cellValue ? formatToDateTime(cellValue) : cellValue; |
|||
}, |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.StartTime'), |
|||
width: 200, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'entityTypeFullName', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.EntityTypeFullName'), |
|||
width: 'auto', |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'entityId', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.EntityId'), |
|||
width: 280, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'entityTenantId', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.TenantId'), |
|||
width: 280, |
|||
}, |
|||
]); |
|||
/** 属性变更表格数据列配置 */ |
|||
const subColumnsConfig = reactive<VxeGridPropTypes.Columns<PropertyChange>>([ |
|||
{ |
|||
align: 'left', |
|||
field: 'propertyName', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.PropertyName'), |
|||
width: 120, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
className: 'font-medium text-green-600', |
|||
field: 'newValue', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.NewValue'), |
|||
width: 200, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
className: 'font-medium text-red-600', |
|||
field: 'originalValue', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.OriginalValue'), |
|||
width: 200, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'propertyTypeFullName', |
|||
sortable: true, |
|||
title: $t('AbpAuditLogging.PropertyTypeFullName'), |
|||
width: 220, |
|||
}, |
|||
]); |
|||
/** 实体变更表格数据源(计算分页) */ |
|||
const getEntityChanges = computed(() => { |
|||
const entityChanges = props.data ?? []; |
|||
const current = pagerConfig.currentPage ?? 1; |
|||
const pageSize = pagerConfig.pageSize ?? 10; |
|||
return entityChanges.slice((current - 1) * pageSize, pageSize * current); |
|||
}); |
|||
/** 页码变更事件 */ |
|||
function onPageChange( |
|||
params: VxeGridDefines.PageChangeEventParams<EntityChangeDto>, |
|||
) { |
|||
pagerConfig.currentPage = params.currentPage; |
|||
pagerConfig.pageSize = params.pageSize; |
|||
} |
|||
watchEffect(() => { |
|||
pagerConfig.total = props.data.length; |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<VxeGrid |
|||
:columns="columnsConfig" |
|||
:data="getEntityChanges" |
|||
:expand-config="{ |
|||
padding: true, |
|||
trigger: 'row', |
|||
}" |
|||
:pager-config="pagerConfig" |
|||
@page-change="onPageChange" |
|||
> |
|||
<template #type="{ row }"> |
|||
<Tag :color="getChangeTypeColor(row.changeType)"> |
|||
{{ getChangeTypeValue(row.changeType) }} |
|||
</Tag> |
|||
</template> |
|||
<template #changes="{ row }"> |
|||
<VxeGrid |
|||
:border="true" |
|||
:columns="subColumnsConfig" |
|||
:data="row.propertyChanges" |
|||
/> |
|||
</template> |
|||
</VxeGrid> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,2 @@ |
|||
export { default as AuditLogTable } from './audit-logs/AuditLogTable.vue'; |
|||
export { default as EntityChangeDrawer } from './entity-changes/EntityChangeDrawer.vue'; |
|||
@ -0,0 +1 @@ |
|||
export * from './permissions'; |
|||
@ -0,0 +1,6 @@ |
|||
/** 审计日志权限 */ |
|||
export const AuditLogPermissions = { |
|||
Default: 'AbpAuditing.AuditLog', |
|||
/** 删除 */ |
|||
Delete: 'AbpAuditing.AuditLog.Delete', |
|||
}; |
|||
@ -0,0 +1,62 @@ |
|||
import { computed } from 'vue'; |
|||
|
|||
import { useLocalization } from '@abp/core'; |
|||
|
|||
import { ChangeType } from '../types/entity-changes'; |
|||
|
|||
export function useAuditlogs() { |
|||
const { L } = useLocalization(['AbpAuditLogging', 'AbpUi']); |
|||
|
|||
const changeTypeColorMap = { |
|||
[ChangeType.Created]: { color: '#87d068', value: L('Created') }, |
|||
[ChangeType.Deleted]: { color: 'red', value: L('Deleted') }, |
|||
[ChangeType.Updated]: { color: '#108ee9', value: L('Updated') }, |
|||
}; |
|||
|
|||
const methodColorMap: { [key: string]: string } = { |
|||
DELETE: 'red', |
|||
GET: 'blue', |
|||
OPTIONS: 'cyan', |
|||
PATCH: 'pink', |
|||
POST: 'green', |
|||
PUT: 'orange', |
|||
}; |
|||
const getChangeTypeColor = computed(() => { |
|||
return (changeType: ChangeType) => changeTypeColorMap[changeType].color; |
|||
}); |
|||
const getChangeTypeValue = computed(() => { |
|||
return (changeType: ChangeType) => changeTypeColorMap[changeType].value; |
|||
}); |
|||
const getHttpMethodColor = computed(() => { |
|||
return (method?: string) => { |
|||
return method ? methodColorMap[method] : ''; |
|||
}; |
|||
}); |
|||
const getHttpStatusCodeColor = computed(() => { |
|||
return (statusCode?: number) => { |
|||
if (!statusCode) { |
|||
return ''; |
|||
} |
|||
if (statusCode >= 200 && statusCode < 300) { |
|||
return '#87d068'; |
|||
} |
|||
if (statusCode >= 300 && statusCode < 400) { |
|||
return '#108ee9'; |
|||
} |
|||
if (statusCode >= 400 && statusCode < 500) { |
|||
return 'orange'; |
|||
} |
|||
if (statusCode >= 500) { |
|||
return 'red'; |
|||
} |
|||
return 'cyan'; |
|||
}; |
|||
}); |
|||
|
|||
return { |
|||
getChangeTypeColor, |
|||
getChangeTypeValue, |
|||
getHttpMethodColor, |
|||
getHttpStatusCodeColor, |
|||
}; |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export * from './components'; |
|||
export * from './constants'; |
|||
export * from './types'; |
|||
@ -0,0 +1,62 @@ |
|||
import type { |
|||
ExtraPropertyDictionary, |
|||
PagedAndSortedResultRequestDto, |
|||
} from '@abp/core'; |
|||
|
|||
import type { EntityChangeDto } from './entity-changes'; |
|||
|
|||
interface Action { |
|||
[key: string]: any; |
|||
executionDuration?: number; |
|||
executionTime: Date; |
|||
extraProperties?: ExtraPropertyDictionary; |
|||
id: string; |
|||
methodName?: string; |
|||
parameters?: string; |
|||
serviceName?: string; |
|||
} |
|||
|
|||
interface AuditLogDto { |
|||
[key: string]: any; |
|||
actions?: Action[]; |
|||
applicationName?: string; |
|||
browserInfo?: string; |
|||
clientId?: string; |
|||
clientIpAddress?: string; |
|||
clientName?: string; |
|||
comments?: string; |
|||
correlationId?: string; |
|||
entityChanges?: EntityChangeDto[]; |
|||
exceptions?: string; |
|||
executionDuration?: number; |
|||
executionTime?: Date; |
|||
extraProperties?: ExtraPropertyDictionary; |
|||
httpMethod?: string; |
|||
httpStatusCode?: number; |
|||
id: string; |
|||
impersonatorTenantId?: string; |
|||
impersonatorUserId?: string; |
|||
tenantId?: string; |
|||
tenantName?: string; |
|||
url?: string; |
|||
userId?: string; |
|||
userName?: string; |
|||
} |
|||
interface AuditLogGetListInput extends PagedAndSortedResultRequestDto { |
|||
applicationName?: string; |
|||
clientId?: string; |
|||
clientIpAddress?: string; |
|||
correlationId?: string; |
|||
endTime?: Date; |
|||
hasException?: boolean; |
|||
httpMethod?: string; |
|||
httpStatusCode?: number; |
|||
maxExecutionDuration?: number; |
|||
minExecutionDuration?: number; |
|||
startTime?: Date; |
|||
url?: string; |
|||
userId?: string; |
|||
userName?: string; |
|||
} |
|||
|
|||
export type { Action, AuditLogDto, AuditLogGetListInput }; |
|||
@ -0,0 +1,63 @@ |
|||
import type { |
|||
ExtraPropertyDictionary, |
|||
PagedAndSortedResultRequestDto, |
|||
} from '@abp/core'; |
|||
|
|||
export enum ChangeType { |
|||
Created = 0, |
|||
Deleted = 2, |
|||
Updated = 1, |
|||
} |
|||
|
|||
interface PropertyChange { |
|||
id: string; |
|||
newValue?: string; |
|||
originalValue?: string; |
|||
propertyName?: string; |
|||
propertyTypeFullName?: string; |
|||
} |
|||
|
|||
interface EntityChangeDto { |
|||
[key: string]: any; |
|||
changeTime?: Date; |
|||
changeType: ChangeType; |
|||
entityId?: string; |
|||
entityTenantId?: string; |
|||
entityTypeFullName?: string; |
|||
extraProperties?: ExtraPropertyDictionary; |
|||
id: string; |
|||
propertyChanges?: PropertyChange[]; |
|||
} |
|||
|
|||
interface EntityChangeWithUsernameDto { |
|||
entityChange: EntityChangeDto; |
|||
userName?: string; |
|||
} |
|||
|
|||
interface EntityChangeGetListInput extends PagedAndSortedResultRequestDto { |
|||
auditLogId?: string; |
|||
changeType?: ChangeType; |
|||
endTime?: Date; |
|||
entityId?: string; |
|||
entityTypeFullName?: string; |
|||
startTime?: Date; |
|||
} |
|||
|
|||
interface EntityChangeGetWithUsernameInput { |
|||
entityId?: string; |
|||
entityTypeFullName?: string; |
|||
} |
|||
|
|||
interface RestoreEntityInput { |
|||
entityChangeId?: string; |
|||
entityId: string; |
|||
} |
|||
|
|||
export type { |
|||
EntityChangeDto, |
|||
EntityChangeGetListInput, |
|||
EntityChangeGetWithUsernameInput, |
|||
EntityChangeWithUsernameDto, |
|||
PropertyChange, |
|||
RestoreEntityInput, |
|||
}; |
|||
@ -0,0 +1,2 @@ |
|||
export * from './audit-logs'; |
|||
export * from './entity-changes'; |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/tsconfig/web.json", |
|||
"include": ["src"], |
|||
"exclude": ["node_modules"] |
|||
} |
|||
@ -1,3 +1,4 @@ |
|||
export * from './useLocalization'; |
|||
export * from './useSettings'; |
|||
export * from './useValidation'; |
|||
export * from './useWindowSizeFn'; |
|||
|
|||
@ -0,0 +1,45 @@ |
|||
import { tryOnMounted, tryOnUnmounted, useDebounceFn } from '@vueuse/core'; |
|||
|
|||
interface WindowSizeOptions { |
|||
immediate?: boolean; |
|||
listenerOptions?: AddEventListenerOptions | boolean; |
|||
once?: boolean; |
|||
} |
|||
|
|||
interface Fn<T = any, R = T> { |
|||
(...arg: T[]): R; |
|||
} |
|||
|
|||
export function useWindowSizeFn<T>( |
|||
fn: Fn<T>, |
|||
wait = 150, |
|||
options?: WindowSizeOptions, |
|||
) { |
|||
let handler = () => { |
|||
fn(); |
|||
}; |
|||
const handleSize = useDebounceFn(handler, wait); |
|||
handler = handleSize; |
|||
|
|||
const start = () => { |
|||
if (options && options.immediate) { |
|||
handler(); |
|||
} |
|||
window.addEventListener('resize', handler, { |
|||
passive: true, |
|||
}); |
|||
}; |
|||
|
|||
const stop = () => { |
|||
window.removeEventListener('resize', handler); |
|||
}; |
|||
|
|||
tryOnMounted(() => { |
|||
start(); |
|||
}); |
|||
|
|||
tryOnUnmounted(() => { |
|||
stop(); |
|||
}); |
|||
return [start, stop]; |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
type SortOrder = '' | 'asc' | 'desc' | null; |
|||
|
|||
export type { SortOrder }; |
|||
@ -0,0 +1,61 @@ |
|||
<script lang="ts" setup> |
|||
import type { PropType } from 'vue'; |
|||
import { computed } from 'vue'; |
|||
|
|||
import { isString } from '@vben-core/shared/utils'; |
|||
|
|||
import CodeMirror from './codemirror/CodeMirror.vue'; |
|||
import { MODE } from './codemirror/types'; |
|||
|
|||
const props = defineProps({ |
|||
autoFormat: { default: true, type: Boolean }, |
|||
mode: { |
|||
default: MODE.JSON, |
|||
type: String as PropType<MODE>, |
|||
validator(value: any) { |
|||
// 这个值必须匹配下列字符串中的一个 |
|||
return Object.values(MODE).includes(value); |
|||
}, |
|||
}, |
|||
readonly: { type: Boolean }, |
|||
value: { default: '', type: [String, Object] }, |
|||
}); |
|||
|
|||
const emit = defineEmits<{ |
|||
(event: 'change', value: string): void; |
|||
(event: 'formatError', error: string): void; |
|||
(event: 'update:value', value: string): void; |
|||
}>(); |
|||
|
|||
const getValue = computed(() => { |
|||
const { autoFormat, mode, value } = props; |
|||
if (!autoFormat || mode !== MODE.JSON) { |
|||
return value as string; |
|||
} |
|||
let result = value; |
|||
if (isString(value)) { |
|||
try { |
|||
result = JSON.parse(value); |
|||
} catch { |
|||
emit('formatError', value); |
|||
return value as string; |
|||
} |
|||
} |
|||
return JSON.stringify(result, null, 2); |
|||
}); |
|||
|
|||
function handleValueChange(v: string) { |
|||
emit('update:value', v); |
|||
emit('change', v); |
|||
} |
|||
</script> |
|||
<template> |
|||
<div class="h-full"> |
|||
<CodeMirror |
|||
:mode="mode" |
|||
:readonly="readonly" |
|||
:value="getValue" |
|||
@change="handleValueChange" |
|||
/> |
|||
</div> |
|||
</template> |
|||
@ -0,0 +1,135 @@ |
|||
<script lang="ts" setup> |
|||
import type { Nullable } from '@vben-core/typings'; |
|||
|
|||
import type { PropType } from 'vue'; |
|||
import { |
|||
nextTick, |
|||
onMounted, |
|||
onUnmounted, |
|||
ref, |
|||
unref, |
|||
watch, |
|||
watchEffect, |
|||
} from 'vue'; |
|||
|
|||
import { usePreferences } from '@vben-core/preferences'; |
|||
|
|||
import { useWindowSizeFn } from '@abp/core'; |
|||
import { useDebounceFn } from '@vueuse/core'; |
|||
import CodeMirror from 'codemirror'; |
|||
|
|||
import { MODE } from './types'; |
|||
|
|||
// css |
|||
import './codemirror.css'; |
|||
import 'codemirror/theme/idea.css'; |
|||
import 'codemirror/theme/material-palenight.css'; |
|||
|
|||
// modes |
|||
import 'codemirror/mode/javascript/javascript'; |
|||
import 'codemirror/mode/css/css'; |
|||
import 'codemirror/mode/htmlmixed/htmlmixed'; |
|||
|
|||
const props = defineProps({ |
|||
mode: { |
|||
default: MODE.JSON, |
|||
type: String as PropType<MODE>, |
|||
validator(value: any) { |
|||
// 这个值必须匹配下列字符串中的一个 |
|||
return Object.values(MODE).includes(value); |
|||
}, |
|||
}, |
|||
readonly: { default: false, type: Boolean }, |
|||
value: { default: '', type: String }, |
|||
}); |
|||
|
|||
const emit = defineEmits(['change']); |
|||
|
|||
const el = ref(); |
|||
let editor: Nullable<CodeMirror.Editor>; |
|||
|
|||
const debounceRefresh = useDebounceFn(refresh, 100); |
|||
|
|||
const { theme } = usePreferences(); |
|||
|
|||
watch( |
|||
() => props.value, |
|||
async (value) => { |
|||
await nextTick(); |
|||
const oldValue = editor?.getValue(); |
|||
if (value !== oldValue) { |
|||
const jsonVal = value ? JSON.stringify(JSON.parse(value), null, 2) : ''; |
|||
editor?.setValue(jsonVal || ''); |
|||
setTimeout(refresh, 50); |
|||
} |
|||
}, |
|||
{ flush: 'post' }, |
|||
); |
|||
|
|||
watchEffect(() => { |
|||
editor?.setOption('mode', props.mode); |
|||
}); |
|||
|
|||
watch( |
|||
() => theme.value, |
|||
async () => { |
|||
setTheme(); |
|||
}, |
|||
{ |
|||
deep: true, |
|||
immediate: true, |
|||
}, |
|||
); |
|||
|
|||
function setTheme() { |
|||
unref(editor)?.setOption( |
|||
'theme', |
|||
theme.value === 'light' ? 'idea' : 'material-palenight', |
|||
); |
|||
} |
|||
|
|||
function refresh() { |
|||
editor?.refresh(); |
|||
} |
|||
|
|||
async function init() { |
|||
const addonOptions = { |
|||
autoCloseBrackets: true, |
|||
autoCloseTags: true, |
|||
foldGutter: true, |
|||
gutters: ['CodeMirror-linenumbers'], |
|||
}; |
|||
|
|||
editor = CodeMirror(el.value!, { |
|||
fixedGutter: true, |
|||
lineNumbers: true, |
|||
lineWrapping: true, |
|||
mode: props.mode, |
|||
readOnly: props.readonly, |
|||
smartIndent: true, |
|||
tabSize: 2, |
|||
theme: 'material-palenight', |
|||
value: '', |
|||
...addonOptions, |
|||
}); |
|||
editor?.setValue(props.value); |
|||
setTheme(); |
|||
editor?.on('change', () => { |
|||
emit('change', editor?.getValue()); |
|||
}); |
|||
} |
|||
|
|||
onMounted(async () => { |
|||
await nextTick(); |
|||
init(); |
|||
useWindowSizeFn<void>(debounceRefresh); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
editor = null; |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<div ref="el" class="relative !h-full w-full overflow-hidden"></div> |
|||
</template> |
|||
@ -0,0 +1,525 @@ |
|||
/* BASICS */ |
|||
|
|||
.CodeMirror { |
|||
--base: #545281; |
|||
--comment: hsl(210deg 25% 60%); |
|||
--keyword: #af4ab1; |
|||
--variable: #0055d1; |
|||
--function: #c25205; |
|||
--string: #2ba46d; |
|||
--number: #c25205; |
|||
--tags: #d00; |
|||
--qualifier: #ff6032; |
|||
--important: var(--string); |
|||
|
|||
position: relative; |
|||
height: auto; |
|||
height: 100%; |
|||
overflow: hidden; |
|||
font-family: var(--font-code); |
|||
background: white; |
|||
direction: ltr; |
|||
} |
|||
|
|||
/* PADDING */ |
|||
|
|||
.CodeMirror-lines { |
|||
min-height: 1px; /* prevents collapsing before first draw */ |
|||
padding: 4px 0; /* Vertical padding around content */ |
|||
cursor: text; |
|||
} |
|||
|
|||
.CodeMirror-scrollbar-filler, |
|||
.CodeMirror-gutter-filler { |
|||
background-color: white; /* The little square between H and V scrollbars */ |
|||
} |
|||
|
|||
/* GUTTER */ |
|||
|
|||
.CodeMirror-gutters { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 5 !important; |
|||
z-index: 3; |
|||
min-height: 100%; |
|||
white-space: nowrap; |
|||
background-color: transparent; |
|||
border-right: 1px solid #ddd; |
|||
} |
|||
|
|||
.CodeMirror-linenumber { |
|||
min-width: 20px; |
|||
padding: 1 8px 0 5px; |
|||
color: var(--comment); |
|||
text-align: left !important; |
|||
white-space: nowrap; |
|||
opacity: 0.6; |
|||
} |
|||
|
|||
.CodeMirror-guttermarker { |
|||
color: black; |
|||
} |
|||
|
|||
.CodeMirror-guttermarker-subtle { |
|||
color: #999; |
|||
} |
|||
|
|||
/* FOLD GUTTER */ |
|||
|
|||
.CodeMirror-foldmarker { |
|||
font-family: arial; |
|||
line-height: 0.3; |
|||
color: #414141; |
|||
text-shadow: #f96 1px 1px 2px, #f96 -1px -1px 2px, #f96 1px -1px 2px, #f96 -1px 1px 2px; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.CodeMirror-foldgutter { |
|||
width: 0.7em; |
|||
} |
|||
|
|||
.CodeMirror-foldgutter-open, |
|||
.CodeMirror-foldgutter-folded { |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.CodeMirror-foldgutter-open::after, |
|||
.CodeMirror-foldgutter-folded::after { |
|||
position: relative; |
|||
top: -0.1em; |
|||
display: inline-block; |
|||
font-size: 0.8em; |
|||
content: '>'; |
|||
opacity: 0.8; |
|||
transform: rotate(90deg); |
|||
transition: transform 0.2s; |
|||
} |
|||
|
|||
.CodeMirror-foldgutter-folded::after { |
|||
transform: none; |
|||
} |
|||
|
|||
/* CURSOR */ |
|||
|
|||
.CodeMirror-cursor { |
|||
position: absolute; |
|||
width: 0; |
|||
pointer-events: none; |
|||
border-right: none; |
|||
border-left: 1px solid black; |
|||
} |
|||
|
|||
/* Shown when moving in bi-directional text */ |
|||
.CodeMirror div.CodeMirror-secondarycursor { |
|||
border-left: 1px solid silver; |
|||
} |
|||
|
|||
.cm-fat-cursor .CodeMirror-cursor { |
|||
width: auto; |
|||
background: #7e7; |
|||
border: 0 !important; |
|||
} |
|||
|
|||
.cm-fat-cursor div.CodeMirror-cursors { |
|||
z-index: 1; |
|||
} |
|||
|
|||
.cm-fat-cursor-mark { |
|||
background-color: rgb(20 255 20 / 50%); |
|||
animation: blink 1.06s steps(1) infinite; |
|||
} |
|||
|
|||
.cm-animate-fat-cursor { |
|||
width: auto; |
|||
background-color: #7e7; |
|||
border: 0; |
|||
animation: blink 1.06s steps(1) infinite; |
|||
} |
|||
@keyframes blink { |
|||
50% { |
|||
background-color: transparent; |
|||
} |
|||
} |
|||
@keyframes blink { |
|||
50% { |
|||
background-color: transparent; |
|||
} |
|||
} |
|||
@keyframes blink { |
|||
50% { |
|||
background-color: transparent; |
|||
} |
|||
} |
|||
|
|||
.cm-tab { |
|||
display: inline-block; |
|||
text-decoration: inherit; |
|||
} |
|||
|
|||
.CodeMirror-rulers { |
|||
position: absolute; |
|||
top: -50px; |
|||
right: 0; |
|||
bottom: -20px; |
|||
left: 0; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.CodeMirror-ruler { |
|||
position: absolute; |
|||
top: 0; |
|||
bottom: 0; |
|||
border-left: 1px solid #ccc; |
|||
} |
|||
|
|||
/* DEFAULT THEME */ |
|||
.cm-s-default.CodeMirror { |
|||
background-color: transparent; |
|||
} |
|||
|
|||
.cm-s-default .cm-header { |
|||
color: blue; |
|||
} |
|||
|
|||
.cm-s-default .cm-quote { |
|||
color: #090; |
|||
} |
|||
|
|||
.cm-negative { |
|||
color: #d44; |
|||
} |
|||
|
|||
.cm-positive { |
|||
color: #292; |
|||
} |
|||
|
|||
.cm-header, |
|||
.cm-strong { |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.cm-em { |
|||
font-style: italic; |
|||
} |
|||
|
|||
.cm-link { |
|||
text-decoration: underline; |
|||
} |
|||
|
|||
.cm-strikethrough { |
|||
text-decoration: line-through; |
|||
} |
|||
|
|||
.cm-s-default .cm-atom, |
|||
.cm-s-default .cm-def, |
|||
.cm-s-default .cm-property, |
|||
.cm-s-default .cm-variable-2, |
|||
.cm-s-default .cm-variable-3, |
|||
.cm-s-default .cm-punctuation { |
|||
color: var(--base); |
|||
} |
|||
|
|||
.cm-s-default .cm-hr, |
|||
.cm-s-default .cm-comment { |
|||
color: var(--comment); |
|||
} |
|||
|
|||
.cm-s-default .cm-attribute, |
|||
.cm-s-default .cm-keyword { |
|||
color: var(--keyword); |
|||
} |
|||
|
|||
.cm-s-default .cm-variable { |
|||
color: var(--variable); |
|||
} |
|||
|
|||
.cm-s-default .cm-bracket, |
|||
.cm-s-default .cm-tag { |
|||
color: var(--tags); |
|||
} |
|||
|
|||
.cm-s-default .cm-number { |
|||
color: var(--number); |
|||
} |
|||
|
|||
.cm-s-default .cm-string, |
|||
.cm-s-default .cm-string-2 { |
|||
color: var(--string); |
|||
} |
|||
|
|||
.cm-s-default .cm-type { |
|||
color: #085; |
|||
} |
|||
|
|||
.cm-s-default .cm-meta { |
|||
color: #555; |
|||
} |
|||
|
|||
.cm-s-default .cm-qualifier { |
|||
color: var(--qualifier); |
|||
} |
|||
|
|||
.cm-s-default .cm-builtin { |
|||
color: #7539ff; |
|||
} |
|||
|
|||
.cm-s-default .cm-link { |
|||
color: var(--flash); |
|||
} |
|||
|
|||
.cm-s-default .cm-error { |
|||
color: #ff008c; |
|||
} |
|||
|
|||
.cm-invalidchar { |
|||
color: #ff008c; |
|||
} |
|||
|
|||
.CodeMirror-composing { |
|||
border-bottom: 2px solid; |
|||
} |
|||
|
|||
/* Default styles for common addons */ |
|||
|
|||
div.CodeMirror span.CodeMirror-matchingbracket { |
|||
color: #0b0; |
|||
} |
|||
|
|||
div.CodeMirror span.CodeMirror-nonmatchingbracket { |
|||
color: #a22; |
|||
} |
|||
|
|||
.CodeMirror-matchingtag { |
|||
background: rgb(255 150 0 / 30%); |
|||
} |
|||
|
|||
.CodeMirror-activeline-background { |
|||
background: #e8f2ff; |
|||
} |
|||
|
|||
/* STOP */ |
|||
|
|||
/* The rest of this file contains styles related to the mechanics of |
|||
the editor. You probably shouldn't touch them. */ |
|||
|
|||
.CodeMirror-scroll { |
|||
position: relative; |
|||
height: 100%; |
|||
padding-bottom: 30px; |
|||
margin-right: -30px; |
|||
|
|||
/* 30px is the magic margin used to hide the element's real scrollbars */ |
|||
|
|||
/* See overflow: hidden in .CodeMirror */ |
|||
margin-bottom: -30px; |
|||
overflow: scroll !important; /* Things will break if this is overridden */ |
|||
outline: none; /* Prevent dragging from highlighting the element */ |
|||
} |
|||
|
|||
.CodeMirror-sizer { |
|||
position: relative; |
|||
margin-bottom: 20px !important; |
|||
border-right: 30px solid transparent; |
|||
} |
|||
|
|||
/* The fake, visible scrollbars. Used to force redraw during scrolling |
|||
before actual scrolling happens, thus preventing shaking and |
|||
flickering artifacts. */ |
|||
.CodeMirror-vscrollbar, |
|||
.CodeMirror-hscrollbar, |
|||
.CodeMirror-scrollbar-filler, |
|||
.CodeMirror-gutter-filler { |
|||
position: absolute; |
|||
z-index: 6; |
|||
display: none; |
|||
} |
|||
|
|||
.CodeMirror-vscrollbar { |
|||
top: 0; |
|||
right: 0; |
|||
overflow-x: hidden; |
|||
overflow-y: scroll; |
|||
} |
|||
|
|||
.CodeMirror-hscrollbar { |
|||
bottom: 0; |
|||
left: 0; |
|||
overflow-x: scroll; |
|||
overflow-y: hidden; |
|||
} |
|||
|
|||
.CodeMirror-scrollbar-filler { |
|||
right: 0; |
|||
bottom: 0; |
|||
} |
|||
|
|||
.CodeMirror-gutter-filler { |
|||
bottom: 0; |
|||
left: 0; |
|||
} |
|||
|
|||
.CodeMirror-gutter { |
|||
display: inline-block; |
|||
height: 100%; |
|||
margin-bottom: -30px; |
|||
white-space: normal; |
|||
vertical-align: top; |
|||
} |
|||
|
|||
.CodeMirror-gutter-wrapper { |
|||
position: absolute; |
|||
z-index: 4; |
|||
background: none !important; |
|||
border: none !important; |
|||
} |
|||
|
|||
.CodeMirror-gutter-background { |
|||
position: absolute; |
|||
top: 0; |
|||
bottom: 0; |
|||
z-index: 4; |
|||
} |
|||
|
|||
.CodeMirror-gutter-elt { |
|||
position: absolute; |
|||
z-index: 4; |
|||
cursor: default; |
|||
} |
|||
|
|||
.CodeMirror-gutter-wrapper ::selection { |
|||
background-color: transparent; |
|||
} |
|||
|
|||
.CodeMirrorwrapper ::selection { |
|||
background-color: transparent; |
|||
} |
|||
|
|||
.CodeMirror pre { |
|||
position: relative; |
|||
z-index: 2; |
|||
padding: 0 4px; /* Horizontal padding of content */ |
|||
margin: 0; |
|||
overflow: visible; |
|||
font-family: inherit; |
|||
font-size: inherit; |
|||
line-height: inherit; |
|||
color: inherit; |
|||
word-wrap: normal; |
|||
white-space: pre; |
|||
background: transparent; |
|||
border-width: 0; |
|||
|
|||
/* Reset some styles that the rest of the page might have set */ |
|||
border-radius: 0; |
|||
-webkit-tap-highlight-color: transparent; |
|||
font-variant-ligatures: contextual; |
|||
} |
|||
|
|||
.CodeMirror-wrap pre { |
|||
word-break: normal; |
|||
word-wrap: break-word; |
|||
white-space: pre-wrap; |
|||
} |
|||
|
|||
.CodeMirror-linebackground { |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
left: 0; |
|||
z-index: 0; |
|||
} |
|||
|
|||
.CodeMirror-linewidget { |
|||
position: relative; |
|||
z-index: 2; |
|||
padding: 0.1px; /* Force widget margins to stay inside of the container */ |
|||
} |
|||
|
|||
.CodeMirror-rtl pre { |
|||
direction: rtl; |
|||
} |
|||
|
|||
.CodeMirror-code { |
|||
outline: none; |
|||
} |
|||
|
|||
/* Force content-box sizing for the elements where we expect it */ |
|||
.CodeMirror-scroll, |
|||
.CodeMirror-sizer, |
|||
.CodeMirror-gutter, |
|||
.CodeMirror-gutters, |
|||
.CodeMirror-linenumber { |
|||
box-sizing: content-box; |
|||
} |
|||
|
|||
.CodeMirror-measure { |
|||
position: absolute; |
|||
width: 100%; |
|||
height: 0; |
|||
overflow: hidden; |
|||
visibility: hidden; |
|||
} |
|||
|
|||
.CodeMirror-measure pre { |
|||
position: static; |
|||
} |
|||
|
|||
div.CodeMirror-cursors { |
|||
position: relative; |
|||
z-index: 3; |
|||
visibility: hidden; |
|||
} |
|||
|
|||
div.CodeMirror-dragcursors { |
|||
visibility: visible; |
|||
} |
|||
|
|||
.CodeMirror-focused div.CodeMirror-cursors { |
|||
visibility: visible; |
|||
} |
|||
|
|||
.CodeMirror-selected { |
|||
background: #d9d9d9; |
|||
} |
|||
|
|||
.CodeMirror-focused .CodeMirror-selected { |
|||
background: #d7d4f0; |
|||
} |
|||
|
|||
.CodeMirror-crosshair { |
|||
cursor: crosshair; |
|||
} |
|||
|
|||
.CodeMirror-line::selection, |
|||
.CodeMirror-line > span::selection, |
|||
.CodeMirror-line > span > span::selection { |
|||
background: #d7d4f0; |
|||
} |
|||
|
|||
.cm-searching { |
|||
background-color: #ffa; |
|||
background-color: rgb(255 255 0 / 40%); |
|||
} |
|||
|
|||
/* Used to force a border model for a node */ |
|||
.cm-force-border { |
|||
padding-right: 0.1px; |
|||
} |
|||
|
|||
@media print { |
|||
/* Hide the cursor when printing */ |
|||
.CodeMirror div.CodeMirror-cursors { |
|||
visibility: hidden; |
|||
} |
|||
} |
|||
|
|||
/* See issue #2901 */ |
|||
.cm-tab-wrap-hack::after { |
|||
content: ''; |
|||
} |
|||
|
|||
/* Help users use markselection to safely style text background */ |
|||
span.CodeMirror-selectedtext { |
|||
background: none; |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
import './codemirror.css'; |
|||
import 'codemirror/theme/idea.css'; |
|||
import 'codemirror/theme/material-palenight.css'; |
|||
// import 'codemirror/addon/lint/lint.css';
|
|||
|
|||
// modes
|
|||
import 'codemirror/mode/javascript/javascript'; |
|||
import 'codemirror/mode/css/css'; |
|||
import 'codemirror/mode/htmlmixed/htmlmixed'; |
|||
// addons
|
|||
// import 'codemirror/addon/edit/closebrackets';
|
|||
// import 'codemirror/addon/edit/closetag';
|
|||
// import 'codemirror/addon/comment/comment';
|
|||
// import 'codemirror/addon/fold/foldcode';
|
|||
// import 'codemirror/addon/fold/foldgutter';
|
|||
// import 'codemirror/addon/fold/brace-fold';
|
|||
// import 'codemirror/addon/fold/indent-fold';
|
|||
// import 'codemirror/addon/lint/json-lint';
|
|||
// import 'codemirror/addon/fold/comment-fold';
|
|||
|
|||
export { default as CodeMirror } from 'codemirror'; |
|||
@ -0,0 +1,2 @@ |
|||
export { default as CodeMirror } from './CodeMirror.vue'; |
|||
export * from './types'; |
|||
@ -0,0 +1,5 @@ |
|||
export enum MODE { |
|||
HTML = 'htmlmixed', |
|||
JS = 'javascript', |
|||
JSON = 'application/json', |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
export { default as CodeEditor } from './CodeEditor.vue'; |
|||
export * from './codemirror'; |
|||
@ -1,4 +1,5 @@ |
|||
export * from './adapter/component'; |
|||
export * from './adapter/form'; |
|||
export * from './adapter/vxe-table'; |
|||
export * from './components'; |
|||
export type * from '@vben/plugins/vxe-table'; |
|||
|
|||
Loading…
Reference in new issue