diff --git a/apps/web-antd/src/api/core/file-gallery.ts b/apps/web-antd/src/api/core/file-gallery.ts index 68263d3f..6f8f2015 100644 --- a/apps/web-antd/src/api/core/file-gallery.ts +++ b/apps/web-antd/src/api/core/file-gallery.ts @@ -5,13 +5,39 @@ const prefix = 'file-gallery/'; export interface FileGalleryItem { id: number; url: string; + file_name?: string; file_size: number; file_size_text: string; + type?: number; + type_text?: string; + type_icon?: string; created_at?: string; original_name?: string; + object_key?: string; + source?: number; } -export async function getFileGalleryList(params?: { page?: number; page_size?: number; pid?: number; type?: number }) { +export interface FileTypeItem { + id: number; + name: string; + icon?: string; + value: number; + suffix: string; + sort: number; +} + +export interface FileGalleryDetail extends FileGalleryItem { + source_text?: string; + updated_at?: string; +} + +export async function getFileGalleryList(params?: { + page?: number; + page_size?: number; + pid?: number; + type?: number; + keyword?: string; +}) { return requestClient.get<{ items: FileGalleryItem[]; total: number; @@ -20,10 +46,33 @@ export async function getFileGalleryList(params?: { page?: number; page_size?: n }>(`${prefix}list`, { params }); } +export async function getFileGalleryDetail(id: number) { + return requestClient.get(`${prefix}detail`, { params: { id } }); +} + export async function deleteFileGalleryItem(id: number) { return requestClient.post(`${prefix}delete`, { id }); } -export async function syncFileGalleryFromOss() { - return requestClient.post<{ added: number; skipped: number }>(`${prefix}sync-oss`); +export async function renameFileGalleryItem(id: number, file_name: string) { + return requestClient.post(`${prefix}rename`, { id, file_name }); +} + +export async function moveFileGalleryType(id: number, type: number) { + return requestClient.post(`${prefix}move-type`, { id, type }); +} + +export async function batchArchiveFiles() { + return requestClient.post<{ + reclassified: number; + file_name_filled: number; + }>(`${prefix}batch-archive`); +} + +export async function getFileTypes() { + return requestClient.get(`${prefix}file-types`); +} + +export async function syncFileGalleryFromOss() { + return requestClient.post<{ synced: number; skipped: number; scanned: number }>(`${prefix}sync-oss`); } diff --git a/apps/web-antd/src/api/core/file-type.ts b/apps/web-antd/src/api/core/file-type.ts new file mode 100644 index 00000000..e8131dc7 --- /dev/null +++ b/apps/web-antd/src/api/core/file-type.ts @@ -0,0 +1,35 @@ +import { requestClient } from '#/api/request'; + +const prefix = 'file-type/'; + +export interface FileTypeRecord { + id: number; + name: string; + icon: string; + value: number; + suffix: string; + sort: number; + status: number; + created_at?: number; + updated_at?: number; +} + +export async function getFileTypeList() { + return requestClient.get(`${prefix}list`); +} + +export async function getFileTypeDetail(id: number) { + return requestClient.get(`${prefix}detail`, { params: { id } }); +} + +export async function createFileType(data: Partial) { + return requestClient.post(`${prefix}create`, data); +} + +export async function updateFileType(data: Partial & { id: number }) { + return requestClient.post(`${prefix}update`, data); +} + +export async function deleteFileType(id: number) { + return requestClient.post(`${prefix}delete`, { id }); +} diff --git a/apps/web-antd/src/api/core/upload.ts b/apps/web-antd/src/api/core/upload.ts index bba6b0ef..309f7e2c 100644 --- a/apps/web-antd/src/api/core/upload.ts +++ b/apps/web-antd/src/api/core/upload.ts @@ -61,7 +61,13 @@ export async function getOssSignature() { /** * OSS 直传成功后登记 xk_file */ -export async function registerOssFile(data: { url: string; type?: number; source?: number; file_size?: number }) { +export async function registerOssFile(data: { + url: string; + type?: number; + source?: number; + file_size?: number; + file_name?: string; +}) { return requestClient.post('/upload/register-oss-file', data); } diff --git a/apps/web-antd/src/app.vue b/apps/web-antd/src/app.vue index ea81f39f..5d779892 100644 --- a/apps/web-antd/src/app.vue +++ b/apps/web-antd/src/app.vue @@ -8,6 +8,7 @@ import { useUserStore as useVbenUserStore } from '@vben/stores'; import { App, ConfigProvider, theme } from 'ant-design-vue'; import { antdLocale } from '#/locales'; +import { GlobalContextMenu } from '#/components/context-menu'; import { useWebSocket } from '#/views/business/chat/composables/useWebSocket'; import { useChatStore } from '#/views/business/chat/stores/chat'; import { useUserStore } from '#/views/business/chat/stores/user'; @@ -80,6 +81,7 @@ watch(userInfoLoaded, (loaded) => { + diff --git a/apps/web-antd/src/components/context-menu/global-context-menu.vue b/apps/web-antd/src/components/context-menu/global-context-menu.vue new file mode 100644 index 00000000..c58596e8 --- /dev/null +++ b/apps/web-antd/src/components/context-menu/global-context-menu.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/apps/web-antd/src/components/context-menu/index.ts b/apps/web-antd/src/components/context-menu/index.ts new file mode 100644 index 00000000..ea779660 --- /dev/null +++ b/apps/web-antd/src/components/context-menu/index.ts @@ -0,0 +1,3 @@ +export type { ContextMenuItem } from './types'; +export { showContextMenu, hideContextMenu } from './use-context-menu'; +export { default as GlobalContextMenu } from './global-context-menu.vue'; diff --git a/apps/web-antd/src/components/context-menu/types.ts b/apps/web-antd/src/components/context-menu/types.ts new file mode 100644 index 00000000..55993f3d --- /dev/null +++ b/apps/web-antd/src/components/context-menu/types.ts @@ -0,0 +1,19 @@ +import type { Component } from 'vue'; + +export interface ContextMenuItem { + key: string; + label: string; + icon?: Component; + iconName?: string; + disabled?: boolean; + handler?: (payload?: any) => void; + children?: ContextMenuItem[]; +} + +export interface ContextMenuState { + visible: boolean; + x: number; + y: number; + items: ContextMenuItem[]; + payload: any; +} diff --git a/apps/web-antd/src/components/context-menu/use-context-menu.ts b/apps/web-antd/src/components/context-menu/use-context-menu.ts new file mode 100644 index 00000000..3fe4cc2a --- /dev/null +++ b/apps/web-antd/src/components/context-menu/use-context-menu.ts @@ -0,0 +1,110 @@ +import { reactive } from 'vue'; + +import type { ContextMenuItem, ContextMenuState } from './types'; + +const MENU_WIDTH = 180; +const MENU_ITEM_HEIGHT = 36; +const SUBMENU_GAP = 4; + +let listenersBound = false; + +export const contextMenuState = reactive({ + visible: false, + x: 0, + y: 0, + items: [], + payload: null, +}); + +export const submenuState = reactive({ + activeKey: null as string | null, + items: [] as ContextMenuItem[], + left: 0, + top: 0, +}); + +function bindGlobalListeners() { + if (listenersBound) { + return; + } + listenersBound = true; + + const hide = () => hideContextMenu(); + + document.addEventListener('click', hide, true); + document.addEventListener('scroll', hide, true); + window.addEventListener('resize', hide); +} + +export function resetSubmenu() { + submenuState.activeKey = null; + submenuState.items = []; + submenuState.left = 0; + submenuState.top = 0; +} + +export function showSubmenu(key: string, items: ContextMenuItem[], left: number, top: number) { + submenuState.activeKey = key; + submenuState.items = items; + submenuState.left = left; + submenuState.top = top; +} + +export function showContextMenu( + e: MouseEvent, + items: ContextMenuItem[], + payload?: any, +) { + e.preventDefault(); + hideContextMenu(); + + const estimatedHeight = estimateMenuHeight(items); + let x = e.clientX; + let y = e.clientY; + + if (x + MENU_WIDTH > window.innerWidth) { + x = Math.max(8, window.innerWidth - MENU_WIDTH - 8); + } + if (y + estimatedHeight > window.innerHeight) { + y = Math.max(8, window.innerHeight - estimatedHeight - 8); + } + + contextMenuState.x = x; + contextMenuState.y = y; + contextMenuState.items = items; + contextMenuState.payload = payload ?? null; + contextMenuState.visible = true; + + bindGlobalListeners(); +} + +export function hideContextMenu() { + contextMenuState.visible = false; + contextMenuState.items = []; + contextMenuState.payload = null; + resetSubmenu(); +} + +export function estimateMenuHeight(items: ContextMenuItem[]): number { + return items.length * MENU_ITEM_HEIGHT + 8; +} + +export function getSubmenuPosition( + parentRect: DOMRect, + childCount: number, +): { left: number; top: number } { + const submenuHeight = childCount * MENU_ITEM_HEIGHT + 8; + let left = parentRect.right + SUBMENU_GAP; + let top = parentRect.top; + + if (left + MENU_WIDTH > window.innerWidth) { + left = parentRect.left - MENU_WIDTH - SUBMENU_GAP; + } + if (top + submenuHeight > window.innerHeight) { + top = Math.max(8, window.innerHeight - submenuHeight - 8); + } + + return { left, top }; +} + +export { MENU_WIDTH, MENU_ITEM_HEIGHT }; diff --git a/apps/web-antd/src/components/form/components/gallery-pick-link.vue b/apps/web-antd/src/components/form/components/gallery-pick-link.vue index 82b40109..e9cc9248 100644 --- a/apps/web-antd/src/components/form/components/gallery-pick-link.vue +++ b/apps/web-antd/src/components/form/components/gallery-pick-link.vue @@ -36,7 +36,7 @@ function onSelect(urls: string[]) {