From 75dd690188283542f58d425d8fb321f3f6fcec0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Thu, 18 Jun 2026 16:42:51 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E5=8F=B3=E9=94=AE=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E7=BB=84=E4=BB=B6=E5=B0=81=E8=A3=85=202.=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=AE=A1=E7=90=86=E4=BC=98=E5=8C=96=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=88=E5=90=8E=E7=BB=AD=E9=9C=80=E8=A6=81=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=88=86=E7=BB=84=E3=80=81=E4=B8=AA=E4=BA=BA=E6=94=B6?= =?UTF-8?q?=E8=97=8F=EF=BC=89=203.=20=E4=BC=98=E5=8C=96=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E3=80=81=E8=AF=8A=E6=89=80=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E7=9A=84=E5=B8=83=E5=B1=80=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E5=8A=A0=E9=9B=86=E4=B8=AD=EF=BC=8C=E5=87=8F=E5=B0=91=E8=A1=A8?= =?UTF-8?q?=E9=95=BF=E5=BA=A6=204.=20=E6=96=B0=E5=A2=9E=E8=AF=8A=E6=89=80?= =?UTF-8?q?=E7=9A=84=E6=97=B6=E5=80=99=E4=B8=9A=E5=8A=A1=E5=91=98=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=B0=94=E6=B3=A1=E5=8D=A1=E7=89=87=E7=82=B9=E5=87=BB?= =?UTF-8?q?=E9=80=89=E6=8B=A9=EF=BC=8C=E4=B8=8D=E7=94=A8=E5=86=8D=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E4=B8=9A=E5=8A=A1=E7=A0=81=205.=20=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=AD=9B=E9=80=89=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antd/src/api/core/file-gallery.ts | 55 +++- apps/web-antd/src/api/core/file-type.ts | 35 ++ apps/web-antd/src/api/core/upload.ts | 8 +- apps/web-antd/src/app.vue | 2 + .../context-menu/global-context-menu.vue | 166 ++++++++++ .../src/components/context-menu/index.ts | 3 + .../src/components/context-menu/types.ts | 19 ++ .../context-menu/use-context-menu.ts | 110 +++++++ .../form/components/gallery-pick-link.vue | 2 +- .../form/components/image-gallery-picker.vue | 162 +++++++++- .../composables/use-file-gallery-filter.ts | 104 ++++++ .../src/composables/use-file-type-icon.ts | 22 ++ apps/web-antd/src/utils/oss-upload.ts | 2 +- .../components/file-detail-drawer.vue | 148 +++++++++ .../src/views/system/file-gallery/index.vue | 299 ++++++++++++++++-- .../src/views/system/file-type/index.vue | 191 +++++++++++ 16 files changed, 1282 insertions(+), 46 deletions(-) create mode 100644 apps/web-antd/src/api/core/file-type.ts create mode 100644 apps/web-antd/src/components/context-menu/global-context-menu.vue create mode 100644 apps/web-antd/src/components/context-menu/index.ts create mode 100644 apps/web-antd/src/components/context-menu/types.ts create mode 100644 apps/web-antd/src/components/context-menu/use-context-menu.ts create mode 100644 apps/web-antd/src/composables/use-file-gallery-filter.ts create mode 100644 apps/web-antd/src/composables/use-file-type-icon.ts create mode 100644 apps/web-antd/src/views/system/file-gallery/components/file-detail-drawer.vue create mode 100644 apps/web-antd/src/views/system/file-type/index.vue 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[]) {