Skip to content

Commit cfd7260

Browse files
feat: operate log
1 parent afb98d2 commit cfd7260

30 files changed

Lines changed: 664 additions & 394 deletions

File tree

‎ui/src/api/API_README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,9 @@ API 枚举与类型统一在 `src/api` 范围内管理,相关规则由本文
107107
- Admin 普通 JSON 请求使用 Axios;`request.ts` 导出 Axios 实例以及 `promise`、`get`、
108108
`post`、`put` 和 `del` 请求封装。
109109
- 正常 JSON 接口返回 `Promise<T>`,请求层负责解包后端 `{ code, message, data }` 响应。
110-
- GET 文件导出使用 `getExportFile`,由请求层统一获取 Blob、解析 `Content-Disposition` 文件名
111-
并触发浏览器下载;业务 API 只需传入默认文件名、接口地址和可选请求参数。
110+
- GET 文件导出使用 `getExportFile`;需要通过 POST 同时传递查询参数和可选请求体的 Excel 导出
111+
使用 `postExportExcel`。请求层统一获取 Blob、解析 `Content-Disposition` 文件名并触发浏览器
112+
下载;业务 API 只需传入默认文件名、接口地址及业务参数。
112113
- 业务代码通过 `api.method().then(...)` 处理接口成功后的状态变化;通用接口错误由请求层统一
113114
提示,不在调用处重复使用 `try/catch` 或 `.catch()` 提示相同错误。只有业务降级、状态恢复等
114115
非提示类失败处理可以按需保留失败分支。

‎ui/src/api/admin/core/request.ts‎

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,31 @@ async function getResponseErrorMessage(error: unknown) {
9898
return responseData?.message
9999
}
100100

101+
async function downloadExportResponse(
102+
response: AxiosResponse<Blob>,
103+
fileName: string,
104+
mimeType = 'application/octet-stream',
105+
) {
106+
if (response.data.type.includes('application/json')) {
107+
const text = await response.data.text()
108+
try {
109+
const data = JSON.parse(text) as Partial<ApiResponse<unknown>>
110+
MsgError(data.message || text)
111+
} catch {
112+
MsgError(text)
113+
}
114+
throw new Error('Response is not a valid file')
115+
}
116+
117+
const blob = new Blob([response.data], { type: mimeType })
118+
const link = document.createElement('a')
119+
link.href = URL.createObjectURL(blob)
120+
link.download = extractFilename(response.headers['content-disposition']) || fileName
121+
link.click()
122+
URL.revokeObjectURL(link.href)
123+
return true
124+
}
125+
101126
export const request = axios.create({
102127
baseURL: `${ADMIN_BASE_PATH.replace(/\/+$/, '')}/api`,
103128
timeout: DEFAULT_TIMEOUT,
@@ -197,24 +222,29 @@ export async function getExportFile(
197222
skipGlobalErrorMessage: true,
198223
} as ExportRequestConfig)
199224

200-
if (response.data.type.includes('application/json')) {
201-
const text = await response.data.text()
202-
try {
203-
const data = JSON.parse(text) as Partial<ApiResponse<unknown>>
204-
MsgError(data.message || text)
205-
} catch {
206-
MsgError(text)
207-
}
208-
throw new Error('Response is not a valid file')
209-
}
225+
return downloadExportResponse(response, fileName)
226+
} finally {
227+
finishLoading(loading)
228+
}
229+
}
210230

211-
const blob = new Blob([response.data], { type: 'application/octet-stream' })
212-
const link = document.createElement('a')
213-
link.href = URL.createObjectURL(blob)
214-
link.download = extractFilename(response.headers['content-disposition']) || fileName
215-
link.click()
216-
URL.revokeObjectURL(link.href)
217-
return true
231+
/** 发送 POST 请求并将 Blob 响应下载为 Excel 文件。 */
232+
export async function postExportExcel<TData = unknown>(
233+
fileName: string,
234+
url: string,
235+
params?: RequestParams,
236+
data?: TData,
237+
loading?: LoadingTarget,
238+
): Promise<boolean> {
239+
startLoading(loading)
240+
try {
241+
const response = await request.post<Blob>(url, data, {
242+
params,
243+
responseType: 'blob',
244+
skipGlobalErrorMessage: true,
245+
} as ExportRequestConfig)
246+
247+
return downloadExportResponse(response, fileName, 'application/vnd.ms-excel')
218248
} finally {
219249
finishLoading(loading)
220250
}
@@ -231,7 +261,6 @@ export function post<TData = unknown, T = unknown>(
231261
return promise<T>(request.post<ApiResponse<T>>(url, data, { params, timeout }), loading)
232262
}
233263

234-
235264
/** 发送 PUT 请求。 */
236265
export function put<TData = unknown, T = unknown>(
237266
url: string,
Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { get, post, postBlob } from '../core/request'
1+
import { postExportExcel, get, post } from '../core/request'
22
import type { ParamsPage, ResponsePage } from '../core/types'
3-
import type { OperateLog, OperateLogMenuOption, OperateLogQuery } from '@/api/types'
3+
import type { OperateLog, OperateLogMenuOption, RequestParams } from '@/api/types'
44

55
const prefix = '/operate_log'
66

77
/** 获取操作日志分页列表。 */
8-
const getOperateLogPage = (page: ParamsPage, query: OperateLogQuery) => {
8+
const getOperateLogPage = (page: ParamsPage, query: RequestParams) => {
99
return get<ResponsePage<OperateLog>>(`${prefix}/${page.currentPage}/${page.pageSize}`, query)
1010
}
1111

@@ -14,25 +14,25 @@ const getOperateLogMenuOptions = () => {
1414
return get<OperateLogMenuOption[]>(`${prefix}/menu_operation_option/`)
1515
}
1616

17-
/** 导出符合当前筛选条件的操作日志。 */
18-
const postOperateLogExport = (query: OperateLogQuery) => {
19-
return postBlob(`${prefix}/export/`, query)
17+
/** 导出操作日志。 */
18+
const exportOperateLog = (query: RequestParams) => {
19+
return postExportExcel('log.xlsx', `${prefix}/export/`, query)
2020
}
2121

22-
/** 保存操作日志自动清理天数。 */
23-
const postOperateLogCleanTime = (cleanTime: number) => {
24-
return post<{ clean_time: number }, boolean>(`${prefix}/save`, { clean_time: cleanTime })
25-
}
26-
27-
/** 获取操作日志自动清理天数。 */
22+
/** 获取对话日志自动清理天数。 */
2823
const getOperateLogCleanTime = () => {
2924
return get<number>(`${prefix}/get_clean_time`)
3025
}
3126

27+
/** 保存对话日志自动清理天数。 */
28+
const postOperateLogCleanTime = (cleanTime: number) => {
29+
return post<{ clean_time: number }, boolean>(`${prefix}/save`, { clean_time: cleanTime })
30+
}
31+
3232
export default {
33+
exportOperateLog,
3334
getOperateLogCleanTime,
3435
getOperateLogMenuOptions,
3536
getOperateLogPage,
3637
postOperateLogCleanTime,
37-
postOperateLogExport,
3838
}

‎ui/src/api/types/system-operate-log.ts‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,3 @@ export interface OperateLogMenuOption {
2222
menu: string
2323
menu_label: string
2424
}
25-
26-
export interface OperateLogQuery extends RequestParams {
27-
end_time?: string
28-
ip_address?: string
29-
menu?: string
30-
start_time?: string
31-
status?: string
32-
user?: string
33-
workspace_ids?: string
34-
}

‎ui/src/assets/iconfont.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎ui/src/components.d.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ declare module 'vue' {
2828
MkSearchInput: typeof import('./components/global/mk-search-input/index.vue')['default']
2929
MkStatusLabel: typeof import('./components/global/mk-status-label/index.vue')['default']
3030
MkTable: typeof import('./components/global/mk-table/index.vue')['default']
31+
MkTableFilter: typeof import('./components/global/mk-table/mk-table-filter.vue')['default']
3132
MkTableMoreDropdown: typeof import('./components/global/mk-table/mk-table-more-dropdown.vue')['default']
3233
MkTagGroup: typeof import('./components/global/mk-tag-group/index.vue')['default']
3334
MkViewLayout: typeof import('./components/global/mk-view-layout/index.vue')['default']
@@ -55,6 +56,7 @@ declare global {
5556
const MkSearchInput: typeof import('./components/global/mk-search-input/index.vue')['default']
5657
const MkStatusLabel: typeof import('./components/global/mk-status-label/index.vue')['default']
5758
const MkTable: typeof import('./components/global/mk-table/index.vue')['default']
59+
const MkTableFilter: typeof import('./components/global/mk-table/mk-table-filter.vue')['default']
5860
const MkTableMoreDropdown: typeof import('./components/global/mk-table/mk-table-more-dropdown.vue')['default']
5961
const MkTagGroup: typeof import('./components/global/mk-tag-group/index.vue')['default']
6062
const MkViewLayout: typeof import('./components/global/mk-view-layout/index.vue')['default']

‎ui/src/components/COMPONENT_README.md‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,13 @@ src/components/
7878
│ │ └── index.vue # 布尔状态图标和文案
7979
│ ├── mk-table/
8080
│ │ ├── index.vue # 表格、分页、列宽拖拽和批量操作
81+
│ │ ├── mk-table-filter.vue # 表头多选筛选器
8182
│ │ └── mk-table-more-dropdown.vue # 表格操作列 More 下拉菜单
8283
│ └── mk-tag-group/
8384
│ └── index.vue # 标签折叠和剩余标签浮层
85+
├── mk-date-range/
86+
│ ├── index.vue # 日期预设与自定义日期区间组合筛选器,手动导入
87+
│ └── types.ts # 日期筛选结果类型
8488
├── mk-search-list/
8589
│ └── index.vue # 搜索框与剩余空间滚动列表,手动导入
8690
├── mk-form-list/
@@ -100,6 +104,7 @@ Vue 模板中使用,不需要手动导入。其他共享组件必须从具体
100104
```ts
101105
import MkSearchList from '@/components/mk-search-list/index.vue'
102106
import MkFormList from '@/components/mk-form-list/index.vue'
107+
import MkDateRange from '@/components/mk-date-range/index.vue'
103108
import PythonCodeEditor from '@/components/codemirror-editor/python.vue'
104109
import JsonInput from '@/components/codemirror-editor/Json.vue'
105110
import MkSourceCard from '@/components/mk-source-card/index.vue'
@@ -472,7 +477,26 @@ const paginationConfig = ref({
472477

473478
`maxTableHeight` 表示窗口中除表格外需要扣除的高度,默认为 `250`;组件会在窗口尺寸变化时
474479
重新计算 `max-height`。传入 `resizable` 后启用列宽拖拽,并隐藏为拖拽借用的原生边框视觉。
475-
需要显示 Element Plus 原生边框时直接使用 `el-table`。
480+
`resizable` 采用白名单式启用:只有需求明确指定的页面级表格才能开启;未明确指定的表格,以及
481+
Dialog、Drawer、Popover、嵌套区域等其他大、小表格均禁止开启。需要显示 Element Plus 原生边框
482+
时直接使用 `el-table`。
483+
484+
表头需要多选筛选时使用 `MkTableFilter`。`label` 设置表头文案,`options` 接收
485+
`OptionItem<string>[]`,`v-model` 绑定已选值;初始不选择任何选项,确认或重置后通过 `change`
486+
返回筛选值。过长的选项文案会显示省略号,悬停时可查看完整文案。
487+
488+
```vue
489+
<el-table-column prop="menu" min-width="140">
490+
<template #header>
491+
<MkTableFilter
492+
v-model="selectedMenus"
493+
label="操作菜单"
494+
:options="menuOptions"
495+
@change="loadRecords()"
496+
/>
497+
</template>
498+
</el-table-column>
499+
```
476500

477501
表格操作列需要 More 菜单时使用 `MkTableMoreDropdown`。组件统一提供点击型、右下定位的 More
478502
按钮以及 `MkDropdownMenu`,默认插槽中直接放置 `MkDropdownItem`。其他 Dropdown 属性和事件通过
@@ -513,6 +537,31 @@ const paginationConfig = ref({
513537

514538
## 手动导入组件
515539

540+
### MkDateRange
541+
542+
组合日期预设下拉框和自定义日期区间选择器。默认显示“过去 7 天”,仅在用户修改筛选条件时通过
543+
`change` 返回 `{ startTime, endTime }`;组件挂载时不主动触发 `change`。预设日期的 `endTime` 为
544+
空字符串,自定义日期清空时两个字段均为空字符串。组件不绑定具体接口字段,使用方负责初始化
545+
默认查询参数,并将筛选结果映射为业务查询参数。
546+
547+
```vue
548+
<script setup lang="ts">
549+
import { ref } from 'vue'
550+
import MkDateRange from '@/components/mk-date-range/index.vue'
551+
import type { MkDateRangeValue } from '@/components/mk-date-range/types'
552+
import { beforeDay } from '@/utils/time'
553+
554+
const dateQuery = ref({ start_time: beforeDay(7), end_time: '' })
555+
556+
function handleDateRangeChange({ startTime, endTime }: MkDateRangeValue) {
557+
dateQuery.value = { start_time: startTime, end_time: endTime }
558+
loadRecords()
559+
}
560+
</script>
561+
562+
<MkDateRange @change="handleDateRangeChange" />
563+
```
564+
516565
### PythonCodeEditor
517566

518567
基于 CodeMirror 6 的 Python 代码编辑器,通过 `v-model` 管理代码,并在组件内部调用工具 pylint

‎ui/src/components/business/workspace-relation-tags/index.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const tableData = computed(() => {
2222
<template #reference>
2323
<MkTagGroup :tags="tags" popover-disabled class="cursor-pointer" />
2424
</template>
25-
<div class="px-3 py-2">
25+
<div class="p-6">
2626
<MkTable :data="tableData" max-height="300">
2727
<template v-for="(label, prop) in tableRenderParams" :key="prop">
2828
<el-table-column

‎ui/src/components/global/mk-table/index.vue‎

Lines changed: 19 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ const props = withDefaults(
1818
data?: unknown[]
1919
maxTableHeight?: number
2020
paginationConfig?: PaginationConfig
21-
resizable?: boolean
22-
isSearching?: boolean
21+
resizable?: boolean // 非指定表格禁止开启
2322
}>(),
2423
{
2524
data: () => [],
@@ -68,51 +67,6 @@ function clearSelection() {
6867
selectedRows.value = []
6968
}
7069
71-
/**
72-
* 列宽拖拽
73-
* resizable 借用 Element Plus border 开启拖拽交互,并补充固定在列边界的 Hover 提示线。
74-
*/
75-
const tableRootRef = ref<HTMLElement>()
76-
const resizeHoverStyle = ref<{ height: string; left: string; top: string }>()
77-
78-
function clearResizeHover() {
79-
resizeHoverStyle.value = undefined
80-
}
81-
82-
function handleTableMouseMove(event: MouseEvent) {
83-
if (!props.resizable) {
84-
clearResizeHover()
85-
return
86-
}
87-
88-
const resizeProxy = tableRootRef.value?.querySelector('.el-table__column-resize-proxy')
89-
90-
if (resizeProxy instanceof HTMLElement && resizeProxy.style.display !== 'none') {
91-
clearResizeHover()
92-
return
93-
}
94-
95-
const target = event.target instanceof Element ? event.target.closest('th.el-table__cell') : null
96-
97-
if (document.body.style.cursor !== 'col-resize' || !(target instanceof HTMLElement)) {
98-
clearResizeHover()
99-
return
100-
}
101-
102-
const rootRect = tableRootRef.value?.getBoundingClientRect()
103-
const targetRect = target.getBoundingClientRect()
104-
105-
if (!rootRect) {
106-
return
107-
}
108-
109-
resizeHoverStyle.value = {
110-
height: `${targetRect.height}px`,
111-
left: `${targetRect.right - rootRect.left}px`,
112-
top: `${targetRect.top - rootRect.top}px`,
113-
}
114-
}
115-
11670
/** 分页 */
11771
function handleCurrentPageChange(currentPage: number) {
11872
if (!props.paginationConfig) {
@@ -152,12 +106,7 @@ defineExpose({ clearSelection, tableRef })
152106
</script>
153107

154108
<template>
155-
<div
156-
ref="tableRootRef"
157-
class="mk-table relative flex w-full min-h-0 flex-1 flex-col"
158-
@mouseleave="clearResizeHover"
159-
@mousemove="handleTableMouseMove"
160-
>
109+
<div class="mk-table relative flex w-full min-h-0 flex-1 flex-col">
161110
<el-table
162111
ref="tableRef"
163112
:class="{ 'mk-table__resizable--borderless': props.resizable }"
@@ -171,12 +120,6 @@ defineExpose({ clearSelection, tableRef })
171120
<slot />
172121
</el-table>
173122

174-
<div
175-
v-if="props.resizable && resizeHoverStyle"
176-
class="mk-table__resize-hover-indicator"
177-
:style="resizeHoverStyle"
178-
/>
179-
180123
<div class="mt-4 flex justify-end" v-if="props.paginationConfig">
181124
<el-pagination
182125
background
@@ -212,6 +155,8 @@ defineExpose({ clearSelection, tableRef })
212155

213156
<style scoped lang="scss">
214157
:deep(.mk-table__resizable--borderless) {
158+
border: none !important;
159+
215160
&,
216161
&::after,
217162
&::before,
@@ -223,14 +168,21 @@ defineExpose({ clearSelection, tableRef })
223168
.el-table__border-left-patch {
224169
display: none;
225170
}
226-
}
227171
228-
.mk-table__resize-hover-indicator {
229-
background-color: color-mix(in srgb, var(--mk-primary) 20%, transparent);
230-
pointer-events: none;
231-
position: absolute;
232-
transform: translateX(-50%);
233-
width: 6px;
234-
z-index: 10;
172+
.el-table__header-wrapper:hover th.el-table__cell:not(:last-child)::after {
173+
background-color: var(--mk-N300);
174+
content: '';
175+
height: 22px;
176+
pointer-events: none;
177+
position: absolute;
178+
right: 0;
179+
top: 50%;
180+
transform: translateY(-50%);
181+
width: 2px;
182+
z-index: 1;
183+
}
184+
.el-table__column-resize-proxy {
185+
border-left: 2px solid var(--el-color-primary);
186+
}
235187
}
236188
</style>

0 commit comments

Comments
 (0)