diff --git a/ui/src/api/API_README.md b/ui/src/api/API_README.md index 0c3696616db..46229f12e4f 100644 --- a/ui/src/api/API_README.md +++ b/ui/src/api/API_README.md @@ -107,8 +107,9 @@ API 枚举与类型统一在 `src/api` 范围内管理,相关规则由本文 - Admin 普通 JSON 请求使用 Axios;`request.ts` 导出 Axios 实例以及 `promise`、`get`、 `post`、`put` 和 `del` 请求封装。 - 正常 JSON 接口返回 `Promise`,请求层负责解包后端 `{ code, message, data }` 响应。 -- GET 文件导出使用 `getExportFile`,由请求层统一获取 Blob、解析 `Content-Disposition` 文件名 - 并触发浏览器下载;业务 API 只需传入默认文件名、接口地址和可选请求参数。 +- GET 文件导出使用 `getExportFile`;需要通过 POST 同时传递查询参数和可选请求体的 Excel 导出 + 使用 `postExportExcel`。请求层统一获取 Blob、解析 `Content-Disposition` 文件名并触发浏览器 + 下载;业务 API 只需传入默认文件名、接口地址及业务参数。 - 业务代码通过 `api.method().then(...)` 处理接口成功后的状态变化;通用接口错误由请求层统一 提示,不在调用处重复使用 `try/catch` 或 `.catch()` 提示相同错误。只有业务降级、状态恢复等 非提示类失败处理可以按需保留失败分支。 diff --git a/ui/src/api/admin/core/request.ts b/ui/src/api/admin/core/request.ts index cdeb350a282..65728786d92 100644 --- a/ui/src/api/admin/core/request.ts +++ b/ui/src/api/admin/core/request.ts @@ -98,6 +98,31 @@ async function getResponseErrorMessage(error: unknown) { return responseData?.message } +async function downloadExportResponse( + response: AxiosResponse, + fileName: string, + mimeType = 'application/octet-stream', +) { + if (response.data.type.includes('application/json')) { + const text = await response.data.text() + try { + const data = JSON.parse(text) as Partial> + MsgError(data.message || text) + } catch { + MsgError(text) + } + throw new Error('Response is not a valid file') + } + + const blob = new Blob([response.data], { type: mimeType }) + const link = document.createElement('a') + link.href = URL.createObjectURL(blob) + link.download = extractFilename(response.headers['content-disposition']) || fileName + link.click() + URL.revokeObjectURL(link.href) + return true +} + export const request = axios.create({ baseURL: `${ADMIN_BASE_PATH.replace(/\/+$/, '')}/api`, timeout: DEFAULT_TIMEOUT, @@ -197,24 +222,29 @@ export async function getExportFile( skipGlobalErrorMessage: true, } as ExportRequestConfig) - if (response.data.type.includes('application/json')) { - const text = await response.data.text() - try { - const data = JSON.parse(text) as Partial> - MsgError(data.message || text) - } catch { - MsgError(text) - } - throw new Error('Response is not a valid file') - } + return downloadExportResponse(response, fileName) + } finally { + finishLoading(loading) + } +} - const blob = new Blob([response.data], { type: 'application/octet-stream' }) - const link = document.createElement('a') - link.href = URL.createObjectURL(blob) - link.download = extractFilename(response.headers['content-disposition']) || fileName - link.click() - URL.revokeObjectURL(link.href) - return true +/** 发送 POST 请求并将 Blob 响应下载为 Excel 文件。 */ +export async function postExportExcel( + fileName: string, + url: string, + params?: RequestParams, + data?: TData, + loading?: LoadingTarget, +): Promise { + startLoading(loading) + try { + const response = await request.post(url, data, { + params, + responseType: 'blob', + skipGlobalErrorMessage: true, + } as ExportRequestConfig) + + return downloadExportResponse(response, fileName, 'application/vnd.ms-excel') } finally { finishLoading(loading) } @@ -231,7 +261,6 @@ export function post( return promise(request.post>(url, data, { params, timeout }), loading) } - /** 发送 PUT 请求。 */ export function put( url: string, diff --git a/ui/src/api/admin/system/operate-log.ts b/ui/src/api/admin/system/operate-log.ts index 4c5666b3375..062f5e5e987 100644 --- a/ui/src/api/admin/system/operate-log.ts +++ b/ui/src/api/admin/system/operate-log.ts @@ -1,11 +1,11 @@ -import { get, post, postBlob } from '../core/request' +import { postExportExcel, get, post } from '../core/request' import type { ParamsPage, ResponsePage } from '../core/types' -import type { OperateLog, OperateLogMenuOption, OperateLogQuery } from '@/api/types' +import type { OperateLog, OperateLogMenuOption, RequestParams } from '@/api/types' const prefix = '/operate_log' /** 获取操作日志分页列表。 */ -const getOperateLogPage = (page: ParamsPage, query: OperateLogQuery) => { +const getOperateLogPage = (page: ParamsPage, query: RequestParams) => { return get>(`${prefix}/${page.currentPage}/${page.pageSize}`, query) } @@ -14,25 +14,25 @@ const getOperateLogMenuOptions = () => { return get(`${prefix}/menu_operation_option/`) } -/** 导出符合当前筛选条件的操作日志。 */ -const postOperateLogExport = (query: OperateLogQuery) => { - return postBlob(`${prefix}/export/`, query) +/** 导出操作日志。 */ +const exportOperateLog = (query: RequestParams) => { + return postExportExcel('log.xlsx', `${prefix}/export/`, query) } -/** 保存操作日志自动清理天数。 */ -const postOperateLogCleanTime = (cleanTime: number) => { - return post<{ clean_time: number }, boolean>(`${prefix}/save`, { clean_time: cleanTime }) -} - -/** 获取操作日志自动清理天数。 */ +/** 获取对话日志自动清理天数。 */ const getOperateLogCleanTime = () => { return get(`${prefix}/get_clean_time`) } +/** 保存对话日志自动清理天数。 */ +const postOperateLogCleanTime = (cleanTime: number) => { + return post<{ clean_time: number }, boolean>(`${prefix}/save`, { clean_time: cleanTime }) +} + export default { + exportOperateLog, getOperateLogCleanTime, getOperateLogMenuOptions, getOperateLogPage, postOperateLogCleanTime, - postOperateLogExport, } diff --git a/ui/src/api/types/system-operate-log.ts b/ui/src/api/types/system-operate-log.ts index 7d4af1956ae..b5fa820e4fc 100644 --- a/ui/src/api/types/system-operate-log.ts +++ b/ui/src/api/types/system-operate-log.ts @@ -22,13 +22,3 @@ export interface OperateLogMenuOption { menu: string menu_label: string } - -export interface OperateLogQuery extends RequestParams { - end_time?: string - ip_address?: string - menu?: string - start_time?: string - status?: string - user?: string - workspace_ids?: string -} diff --git a/ui/src/assets/iconfont.js b/ui/src/assets/iconfont.js index f3e5118851f..c6474a99a97 100644 --- a/ui/src/assets/iconfont.js +++ b/ui/src/assets/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_5200172='',(l=>{var a=(h=(h=document.getElementsByTagName("script"))[h.length-1]).getAttribute("data-injectcss"),h=h.getAttribute("data-disable-injectsvg");if(!h){var o,v,t,i,c,d=function(a,h){h.parentNode.insertBefore(a,h)};if(a&&!l.__iconfont__svg__cssinject__){l.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}o=function(){var a,h=document.createElement("div");h.innerHTML=l._iconfont_svg_string_5200172,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(a=document.body).firstChild?d(h,a.firstChild):a.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(o,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),o()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(t=o,i=l.document,c=!1,m(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,e())})}function e(){c||(c=!0,t())}function m(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(m,50)}e()}})(window); \ No newline at end of file +window._iconfont_svg_string_5200172='',(l=>{var a=(h=(h=document.getElementsByTagName("script"))[h.length-1]).getAttribute("data-injectcss"),h=h.getAttribute("data-disable-injectsvg");if(!h){var v,o,t,i,c,d=function(a,h){h.parentNode.insertBefore(a,h)};if(a&&!l.__iconfont__svg__cssinject__){l.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}v=function(){var a,h=document.createElement("div");h.innerHTML=l._iconfont_svg_string_5200172,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(a=document.body).firstChild?d(h,a.firstChild):a.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(v,0):(o=function(){document.removeEventListener("DOMContentLoaded",o,!1),v()},document.addEventListener("DOMContentLoaded",o,!1)):document.attachEvent&&(t=v,i=l.document,c=!1,m(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,e())})}function e(){c||(c=!0,t())}function m(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(m,50)}e()}})(window); \ No newline at end of file diff --git a/ui/src/components.d.ts b/ui/src/components.d.ts index b99373c1ee4..ce77e2843c7 100644 --- a/ui/src/components.d.ts +++ b/ui/src/components.d.ts @@ -28,6 +28,7 @@ declare module 'vue' { MkSearchInput: typeof import('./components/global/mk-search-input/index.vue')['default'] MkStatusLabel: typeof import('./components/global/mk-status-label/index.vue')['default'] MkTable: typeof import('./components/global/mk-table/index.vue')['default'] + MkTableFilter: typeof import('./components/global/mk-table/mk-table-filter.vue')['default'] MkTableMoreDropdown: typeof import('./components/global/mk-table/mk-table-more-dropdown.vue')['default'] MkTagGroup: typeof import('./components/global/mk-tag-group/index.vue')['default'] MkViewLayout: typeof import('./components/global/mk-view-layout/index.vue')['default'] @@ -55,6 +56,7 @@ declare global { const MkSearchInput: typeof import('./components/global/mk-search-input/index.vue')['default'] const MkStatusLabel: typeof import('./components/global/mk-status-label/index.vue')['default'] const MkTable: typeof import('./components/global/mk-table/index.vue')['default'] + const MkTableFilter: typeof import('./components/global/mk-table/mk-table-filter.vue')['default'] const MkTableMoreDropdown: typeof import('./components/global/mk-table/mk-table-more-dropdown.vue')['default'] const MkTagGroup: typeof import('./components/global/mk-tag-group/index.vue')['default'] const MkViewLayout: typeof import('./components/global/mk-view-layout/index.vue')['default'] diff --git a/ui/src/components/COMPONENT_README.md b/ui/src/components/COMPONENT_README.md index 33b93a72145..8a6c7cf4c54 100644 --- a/ui/src/components/COMPONENT_README.md +++ b/ui/src/components/COMPONENT_README.md @@ -78,9 +78,13 @@ src/components/ │ │ └── index.vue # 布尔状态图标和文案 │ ├── mk-table/ │ │ ├── index.vue # 表格、分页、列宽拖拽和批量操作 +│ │ ├── mk-table-filter.vue # 表头多选筛选器 │ │ └── mk-table-more-dropdown.vue # 表格操作列 More 下拉菜单 │ └── mk-tag-group/ │ └── index.vue # 标签折叠和剩余标签浮层 +├── mk-date-range/ +│ ├── index.vue # 日期预设与自定义日期区间组合筛选器,手动导入 +│ └── types.ts # 日期筛选结果类型 ├── mk-search-list/ │ └── index.vue # 搜索框与剩余空间滚动列表,手动导入 ├── mk-form-list/ @@ -100,6 +104,7 @@ Vue 模板中使用,不需要手动导入。其他共享组件必须从具体 ```ts import MkSearchList from '@/components/mk-search-list/index.vue' import MkFormList from '@/components/mk-form-list/index.vue' +import MkDateRange from '@/components/mk-date-range/index.vue' import PythonCodeEditor from '@/components/codemirror-editor/python.vue' import JsonInput from '@/components/codemirror-editor/Json.vue' import MkSourceCard from '@/components/mk-source-card/index.vue' @@ -472,7 +477,26 @@ const paginationConfig = ref({ `maxTableHeight` 表示窗口中除表格外需要扣除的高度,默认为 `250`;组件会在窗口尺寸变化时 重新计算 `max-height`。传入 `resizable` 后启用列宽拖拽,并隐藏为拖拽借用的原生边框视觉。 -需要显示 Element Plus 原生边框时直接使用 `el-table`。 +`resizable` 采用白名单式启用:只有需求明确指定的页面级表格才能开启;未明确指定的表格,以及 +Dialog、Drawer、Popover、嵌套区域等其他大、小表格均禁止开启。需要显示 Element Plus 原生边框 +时直接使用 `el-table`。 + +表头需要多选筛选时使用 `MkTableFilter`。`label` 设置表头文案,`options` 接收 +`OptionItem[]`,`v-model` 绑定已选值;初始不选择任何选项,确认或重置后通过 `change` +返回筛选值。过长的选项文案会显示省略号,悬停时可查看完整文案。 + +```vue + + + +``` 表格操作列需要 More 菜单时使用 `MkTableMoreDropdown`。组件统一提供点击型、右下定位的 More 按钮以及 `MkDropdownMenu`,默认插槽中直接放置 `MkDropdownItem`。其他 Dropdown 属性和事件通过 @@ -513,6 +537,31 @@ const paginationConfig = ref({ ## 手动导入组件 +### MkDateRange + +组合日期预设下拉框和自定义日期区间选择器。默认显示“过去 7 天”,仅在用户修改筛选条件时通过 +`change` 返回 `{ startTime, endTime }`;组件挂载时不主动触发 `change`。预设日期的 `endTime` 为 +空字符串,自定义日期清空时两个字段均为空字符串。组件不绑定具体接口字段,使用方负责初始化 +默认查询参数,并将筛选结果映射为业务查询参数。 + +```vue + + + +``` + ### PythonCodeEditor 基于 CodeMirror 6 的 Python 代码编辑器,通过 `v-model` 管理代码,并在组件内部调用工具 pylint diff --git a/ui/src/components/business/workspace-relation-tags/index.vue b/ui/src/components/business/workspace-relation-tags/index.vue index c77373535a9..111fd19d5e3 100644 --- a/ui/src/components/business/workspace-relation-tags/index.vue +++ b/ui/src/components/business/workspace-relation-tags/index.vue @@ -22,7 +22,7 @@ const tableData = computed(() => { -
+