diff --git a/config/proxy.ts b/config/proxy.ts index e1a47c0..0ac6638 100644 --- a/config/proxy.ts +++ b/config/proxy.ts @@ -15,7 +15,7 @@ export default { // localhost:8000/api/** -> https://preview.pro.ant.design/api/** '/admin-api/': { // 要代理的地址 - target: 'http://114.132.60.20:48080', + target: 'http://192.168.1.231:48080', // 配置了这个可以从 http 代理到 https // 依赖 origin 的功能可能需要这个,比如 cookie changeOrigin: true, diff --git a/package.json b/package.json index ae4e0d4..430245f 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "jsencrypt": "^3.5.4", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-infinite-scroll-component": "^6.1.0", "tinymce": "^8.1.2", "web-storage-cache": "^1.1.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62cc0fc..62a038a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: react-dom: specifier: ^19.1.0 version: 19.1.1(react@19.1.1) + react-infinite-scroll-component: + specifier: ^6.1.0 + version: 6.1.0(react@19.1.1) tinymce: specifier: ^8.1.2 version: 8.1.2 @@ -9023,6 +9026,11 @@ packages: react: ^16.6.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0 + react-infinite-scroll-component@6.1.0: + resolution: {integrity: sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==} + peerDependencies: + react: '>=16.0.0' + react-intl@3.12.1: resolution: {integrity: sha512-cgumW29mwROIqyp8NXStYsoIm27+8FqnxykiLSawWjOxGIBeLuN/+p2srei5SRIumcJefOkOIHP+NDck05RgHg==} peerDependencies: @@ -10232,6 +10240,10 @@ packages: thread-stream@0.15.2: resolution: {integrity: sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==} + throttle-debounce@2.3.0: + resolution: {integrity: sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==} + engines: {node: '>=8'} + throttle-debounce@5.0.2: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} @@ -23302,6 +23314,11 @@ snapshots: react-fast-compare: 3.2.2 shallowequal: 1.1.0 + react-infinite-scroll-component@6.1.0(react@19.1.1): + dependencies: + react: 19.1.1 + throttle-debounce: 2.3.0 + react-intl@3.12.1(@types/react@19.1.12)(react@19.1.1): dependencies: '@formatjs/intl-displaynames': 1.2.10 @@ -24854,6 +24871,8 @@ snapshots: dependencies: real-require: 0.1.0 + throttle-debounce@2.3.0: {} + throttle-debounce@5.0.2: {} through@2.3.8: {} diff --git a/src/app.tsx b/src/app.tsx index e216283..00649f3 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,15 +1,9 @@ import type { Settings as LayoutSettings } from '@ant-design/pro-components'; import { SettingDrawer } from '@ant-design/pro-components'; import type { RequestConfig, RunTimeLayoutConfig } from '@umijs/max'; -import { history, Link, Navigate } from '@umijs/max'; -import { Modal, Spin } from 'antd'; -import React, { - Children, - Component, - createContext, - JSX, - Suspense, -} from 'react'; +import { history, Navigate } from '@umijs/max'; +import { Spin } from 'antd'; +import React from 'react'; import { AvatarDropdown, AvatarName, @@ -18,17 +12,13 @@ import { SelectLang, } from '@/components'; import { getInfo } from '@/services/login'; -import type { TokenType, UserInfoVO, UserVO } from '@/services/login/types'; +import type { UserInfoVO } from '@/services/login/types'; import defaultSettings from '../config/defaultSettings'; import { errorConfig } from './requestErrorConfig'; import '@ant-design/v5-patch-for-react-19'; -import { getAccessToken, getRefreshToken, getTenantId } from '@/utils/auth'; -import { - transformBackendMenuToFlatRoutes, - transformMenuToRoutes, -} from '@/utils/menuUtils'; +import { getAccessToken, getTenantId } from '@/utils/auth'; + import { CACHE_KEY, useCache } from './hooks/web/useCache'; -import { MenuVO } from './services/system/menu'; const isDev = process.env.NODE_ENV === 'development'; const isDevOrTest = isDev || process.env.CI; @@ -198,7 +188,7 @@ export const request: RequestConfig = { }; // 如果有token,则添加Authorization头 if (token) { - headers['Authorization'] = `Bearer ${getAccessToken()}`; + headers.Authorization = `Bearer·${getAccessToken()}`; } return { url, options: { ...options, headers } }; }, @@ -235,20 +225,19 @@ export const request: RequestConfig = { // umi 4 使用 modifyRoutes export function patchClientRoutes({ routes }: { routes: any }) { const { wsCache } = useCache(); - console.log(2222); const globalMenus = wsCache.get(CACHE_KEY.ROLE_ROUTERS); const routerIndex = routes.findIndex((item: any) => item.path === '/'); const parentId = routes[routerIndex].id; if (globalMenus) { - routes[routerIndex]['routes'].push(...loopMenuItem(globalMenus, parentId)); + routes[routerIndex].routes.push(...loopMenuItem(globalMenus, parentId)); } } const loopMenuItem = (menus: any[], pId: number | string): any[] => { return menus.flatMap((item) => { let Component: React.ComponentType | null = null; - console.log(findFirstLeafRoute(item), 'item'); + // console.log(findFirstLeafRoute(item), 'item'); if (item.component && item.component.length > 0) { // 防止配置了路由,但本地暂未添加对应的页面,产生的错误 Component = React.lazy(() => { @@ -299,7 +288,7 @@ const loopMenuItem = (menus: any[], pId: number | string): any[] => { }); }; -const findFirstLeafRoute = (menuItem: any, parent = '/'): string | null => { +const _findFirstLeafRoute = (menuItem: any, parent = '/'): string | null => { // 如果没有子菜单,返回当前路径 if (!menuItem.children || menuItem.children.length === 0) { @@ -308,7 +297,8 @@ const findFirstLeafRoute = (menuItem: any, parent = '/'): string | null => { // 递归查找第一个叶子节点 for (const child of menuItem.children) { - const leafRoute = findFirstLeafRoute(child, menuItem.path + '/'); + // const leafRoute = findFirstLeafRoute(child, menuItem.path + "/"); + const leafRoute = _findFirstLeafRoute(child, `${menuItem.path}/`); if (leafRoute) { return leafRoute; } diff --git a/src/components/EnhancedProTable/index.tsx b/src/components/EnhancedProTable/index.tsx index 16c7016..02755de 100644 --- a/src/components/EnhancedProTable/index.tsx +++ b/src/components/EnhancedProTable/index.tsx @@ -1,13 +1,12 @@ // components/EnhancedProTable/EnhancedProTable.tsx -import { PlusOutlined } from '@ant-design/icons'; import { type ActionType, type ParamsType, ProTable, } from '@ant-design/pro-components'; -import { Button, Space, Table } from 'antd'; -import React, { forwardRef, useCallback, useMemo, useState } from 'react'; +import { Button } from 'antd'; +import React, { forwardRef, useCallback } from 'react'; import { formatPaginationTotal } from '@/utils/antd/tableHelpers'; import type { BaseRecord, EnhancedProTableProps } from './types'; @@ -20,10 +19,9 @@ function EnhancedProTable( request, // actions = [], // permissions = [], - checkPermission = () => true, + // checkPermission = () => true, toolbarActions, - showIndex = true, - showSelection = true, + // showIndex = true, // showActions = true, // maxActionCount = 2, // onAdd, @@ -32,28 +30,65 @@ function EnhancedProTable( // onView, // onExport, // customToolbarRender, - customActionRender, + // showSelection = true, rowKey = 'id', + // onRow, + // rowClassName, + // enableRowClick = false, + // clickableRowClassName = "clickable-row", // 添加可点击样式 ...restProps } = props; - const [selectedRowKeys, setSelectedRowKeys] = useState([]); - const [selectedRows, setSelectedRows] = useState([]); - // 行选择配置 - const rowSelection = useMemo(() => { - if (!showSelection) return undefined; - return { - selectedRowKeys, - selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT], - onChange: (keys: React.Key[], rows: T[]) => { - setSelectedRowKeys(keys); - setSelectedRows(rows); - }, - getCheckboxProps: (record: T) => ({ - name: record[rowKey]?.toString(), - }), - }; - }, [showSelection, selectedRowKeys]); + // const [selectedRowKeys, setSelectedRowKeys] = useState([]); + // const [selectedRows, setSelectedRows] = useState([]); + // // 行选择配置 + // const rowSelection = useMemo(() => { + // if (!showSelection) return undefined; + // return { + // selectedRowKeys, + // selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT], + // onChange: (keys: React.Key[], rows: T[]) => { + // setSelectedRowKeys(keys); + // setSelectedRows(rows); + // }, + // getCheckboxProps: (record: T) => ({ + // name: record[rowKey]?.toString(), + // }), + // }; + // }, [showSelection, selectedRowKeys]); + + // // 处理行点击事件 + // const handleRowClick = useCallback( + // (record: T, index?: number) => { + // console.log("handleRowClick"); + // if (!enableRowClick) return {}; + // return { + // onClick: (event: React.MouseEvent) => { + // // 阻止事件冒泡到其他元素 + // event.stopPropagation(); + + // // 如果点击的是 checkbox 或其他交互元素,不处理行选中 + // const target = event.target as HTMLElement; + // console.log("handleRowClick"); + // if ( + // target.closest(".ant-checkbox") || + // target.closest(".ant-btn") || + // target.closest(".ant-dropdown") || + // target.closest("a") + // ) { + // return; + // } + + // // // 切换选中状态 + // // handleRowSelect(record, !isSelected); + + // // // 调用原始的 onRow 点击事件 + // onRow?.(record, index)?.onClick?.(event); + // }, + // }; + // }, + // [enableRowClick, onRow] + // ); const toolBarRender = useCallback(() => { const toolbarElements = @@ -64,8 +99,8 @@ function EnhancedProTable( type={action.type} danger={action.danger} disabled={action.disabled} - icon={} - onClick={() => action.onClick(selectedRowKeys, selectedRows)} + // icon={action.icon ?? } + onClick={() => action.onClick()} > {action.label} @@ -80,7 +115,7 @@ function EnhancedProTable( actionRef={ref} request={request} rowKey={rowKey} - rowSelection={rowSelection} + rowSelection={props.rowSelection ?? false} toolBarRender={toolBarRender} manualRequest={false} showSorterTooltip diff --git a/src/components/EnhancedProTable/types.ts b/src/components/EnhancedProTable/types.ts index 25586b1..d930279 100644 --- a/src/components/EnhancedProTable/types.ts +++ b/src/components/EnhancedProTable/types.ts @@ -58,6 +58,8 @@ export interface EnhancedProTableProps params: U & { current?: number; pageSize?: number }, ) => Promise>; loading?: boolean; // 添加 loading 属性 + enableRowClick?: boolean; //开启行内点击 + clickableRowClassName?: string; actions?: TableAction[]; toolbarActions?: ToolbarAction[]; permissions?: string[]; diff --git a/src/components/GroupTag/GroupTagCore.less b/src/components/GroupTag/GroupTagCore.less new file mode 100644 index 0000000..6a7a248 --- /dev/null +++ b/src/components/GroupTag/GroupTagCore.less @@ -0,0 +1,301 @@ +// GroupTagCore.less +.group-tag-core { + width: 100%; + border: 1px solid #f0f0f0; + border-radius: 6px; + overflow: hidden; + + // 编辑模式下的整体样式 + &.edit-mode { + .search-wrapper { + .ant-input-affix-wrapper { + background-color: #f5f5f5; + + .ant-input { + background-color: #f5f5f5; + cursor: not-allowed; + } + } + } + } + + .search-wrapper { + padding: 8px; + border-bottom: 1px solid #f0f0f0; + + .ant-input-affix-wrapper { + width: 100%; + } + } + + .content-wrapper { + flex: 1; + overflow: hidden; + + .loading-wrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 200px; + text-align: center; + } + + .split-layout { + display: flex; + height: 100%; + + .groups-panel { + width: 200px; + border-right: 1px solid #f0f0f0; + display: flex; + flex-direction: column; + + .panel-header { + padding: 8px 12px; + background-color: #fafafa; + border-bottom: 1px solid #f0f0f0; + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 500; + font-size: 14px; + } + + .groups-list { + overflow: auto; + height: 40vh; + .group-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + cursor: pointer; + transition: background-color 0.2s; + border-bottom: 1px solid #f5f5f5; + + &:hover { + background-color: #f5f5f5; + } + + &.active { + background-color: #e6f7ff; + border-color: #91d5ff; + } + + &.editing { + background-color: #fff7e6; + cursor: default; + + &:hover { + background-color: #fff7e6; + } + } + + &.disabled { + cursor: not-allowed; + opacity: 0.6; + + &:hover { + background-color: inherit; + } + + &.active:hover { + background-color: #e6f7ff; + } + } + + .group-content { + flex: 1; + display: flex; + justify-content: space-between; + align-items: center; + + .group-name { + font-size: 13px; + color: #262626; + } + + .edit-input-wrapper { + display: flex; + align-items: center; + gap: 4px; + width: 100%; + + .ant-input { + flex: 1; + font-size: 12px; + } + + .edit-actions { + display: flex; + gap: 2px; + + .ant-btn { + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + + .anticon { + font-size: 10px; + } + } + } + } + } + + .ant-checkbox-wrapper { + margin-left: 8px; + } + } + } + } + + .tags-panel { + flex: 1; + display: flex; + flex-direction: column; + + .panel-header { + padding: 8px 12px; + background-color: #fafafa; + border-bottom: 1px solid #f0f0f0; + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 500; + font-size: 14px; + } + + .tags-list { + overflow: auto; + height: 40vh; + padding: 4px 0; + + .loading-wrapper { + display: flex; + justify-content: center; + align-items: center; + height: 60px; + } + .infinite-scroll-container { + height: 50vh !important; + overflow-y: auto; + } + .tag-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 12px; + transition: background-color 0.2s; + + &:hover { + background-color: #f5f5f5; + } + + &.editing { + background-color: #fff7e6; + + &:hover { + background-color: #fff7e6; + } + } + + &.disabled { + opacity: 0.6; + + &:hover { + background-color: inherit; + } + } + + .ant-checkbox-wrapper { + flex: 1; + + span:last-child { + color: #595959; + font-size: 13px; + } + } + + .tag-name-only { + flex: 1; + color: #8c8c8c; + font-size: 13px; + + .anticon { + color: #bfbfbf; + } + } + + .tag-actions { + display: flex; + gap: 2px; + opacity: 0; + transition: opacity 0.2s; + + .ant-btn { + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + + .anticon { + font-size: 10px; + } + } + } + + &:hover .tag-actions { + opacity: 1; + } + + .edit-input-wrapper { + display: flex; + align-items: center; + gap: 4px; + width: 100%; + + .ant-input { + flex: 1; + font-size: 12px; + } + + .edit-actions { + display: flex; + gap: 2px; + + .ant-btn { + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + + .anticon { + font-size: 10px; + } + } + } + } + } + + .ant-empty { + padding: 40px 20px; + + .ant-empty-description { + color: #8c8c8c; + font-size: 12px; + } + } + } + } + } + } +} diff --git a/src/components/GroupTag/GroupTagCore.tsx b/src/components/GroupTag/GroupTagCore.tsx new file mode 100644 index 0000000..9c150ac --- /dev/null +++ b/src/components/GroupTag/GroupTagCore.tsx @@ -0,0 +1,343 @@ +// GroupTagCore.tsx - 修复缺失函数和API参数 + +import { PlusOutlined, SearchOutlined } from '@ant-design/icons'; +import { + Button, + type CheckboxChangeEvent, + Input, + message, + Space, + Spin, +} from 'antd'; +import React, { useCallback, useEffect, useState } from 'react'; +import type { GroupItem, GroupTagCoreProps, TagItem } from './types'; +import './GroupTagCore.less'; +import { renderGroups, renderTags } from './component'; +import TagsModal from './TagsModal'; + +const GroupTagCore: React.FC = (props) => { + const { + requset: { groupsApi, tagsApi }, + editable = false, + value = [], + onChange, + } = props; + const [isInEditMode, _setIsInEditMode] = useState(false); + // 编辑状态 + const [loading, setLoading] = useState(false); + const [loadingTags, setLoadingTags] = useState(false); + const [groups, setGroups] = useState([]); + const [currentGroup, setCurrentGroup] = useState(); + const [modalType, setModalType] = useState<'add' | 'edit' | 'delete'>(); + const [type, setType] = useState<'group' | 'tag'>('group'); + const [visible, setVisible] = useState(false); + const [name, setName] = useState(''); + const [total, setTotal] = useState(0); + const [currentId, setCurrentId] = useState(); + const [tagsModalValue, setTagsModalValue] = useState<{ + tagName?: string; + groupIds?: number[]; + id?: number; + }>({}); + const [pageNo, setPageNo] = useState(1); + const fetchGroups = useCallback(async () => { + try { + setLoading(true); + const groups = await groupsApi.get(); + groups.length && setCurrentGroup(groups[0]); + setGroups(groups); + setCurrentId(groups[0].id); + } finally { + setLoading(false); + } + }, [groupsApi]); + + const fetchTagsApi = useCallback(async () => { + try { + setLoadingTags(true); + const res = await tagsApi.get({ groupId: currentId, pageNo: 1 }); + const newGroup = { ...currentGroup, tags: res.list }; + const newData = groups.map((g) => (g.id === currentId ? newGroup : g)); + setGroups(newData as GroupItem[]); + setCurrentGroup(newGroup as GroupItem); + setTotal(res.total); + setPageNo(1); + } finally { + setLoadingTags(false); + } + }, [tagsApi, groups, currentId]); + useEffect(() => { + fetchGroups(); + }, []); + + useEffect(() => { + if (currentId) { + fetchTagsApi(); + } + }, [currentId]); + + const onGroupClick = (group: GroupItem) => { + if (currentId !== group.id) { + setCurrentId(group.id); + setCurrentGroup(group); + } + }; + + useEffect(() => { + if (modalType === 'edit' && visible) { + if (type === 'group' && currentGroup) { + setName(currentGroup.groupName); + } else if (type === 'tag' && tagsModalValue) { + setName(tagsModalValue.tagName || ''); + } + } + if (modalType === 'add') { + setName(''); + } + }, [type, modalType, currentGroup, tagsModalValue, visible]); + const handleGroup = (type: 'add' | 'edit' | 'delete') => { + setType('group'); + setModalType(type); + if (type === 'add' || type === 'edit') { + setVisible(true); + } + }; + + const handleTag = (type: 'add' | 'edit' | 'delete', tag?: TagItem) => { + setType('tag'); + setModalType(type); + if (type === 'add' || type === 'edit') { + setVisible(true); + } + if (type === 'edit') { + setTagsModalValue({ tagName: tag?.tagName, id: tag?.id }); + } + }; + + const handleAdd = async (value: { + groupName?: string; + groupIds?: number[]; + tagName?: string; + }) => { + if (type === 'group') { + const id = await groupsApi.create({ groupName: value.groupName }); + setGroups([{ id, groupName: value.groupName || '' }, ...groups]); + } else { + await tagsApi.create(value); + message.success('添加成功'); + fetchTagsApi(); + } + setVisible(false); + }; + + const handleEdit = async (value: { + tagName?: string; + groupIds?: number[]; + }) => { + if (type === 'group') { + try { + setLoading(true); + await groupsApi.update({ id: currentGroup?.id, groupName: name }); + const newData = groups.map((item) => { + if (item.id === currentGroup?.id) { + const itemData = { ...item, groupName: name }; + setCurrentGroup(itemData); + return itemData; + } else { + return item; + } + }); + setGroups(newData); + } finally { + setLoading(false); + } + } else { + await tagsApi.update({ + groupIds: [currentGroup?.id], + id: tagsModalValue?.id, + ...value, + }); + message.success('修改成功'); + const newTag = currentGroup?.tags?.map((tag) => { + if (tag.id === tagsModalValue?.id) { + return { ...tag, tagName: value.tagName }; + } else { + return tag; + } + }); + + const newGroup = { ...currentGroup, tags: newTag }; + setCurrentGroup(newGroup as GroupItem); + } + setVisible(false); + }; + + const handleDelete = async (type: 'group' | 'tag', id: number) => { + if (type === 'group' && currentGroup) { + await groupsApi.delete(id); + const newGroups = groups.filter((group) => group.id !== id); + setGroups(newGroups); + if (id === currentId) { + setCurrentId(newGroups[0].id); + setCurrentGroup(newGroups[0]); + } + } else { + tagsApi.delete(id); + } + }; + + const handleConfirm = useCallback( + async (value: { tagName?: string; groupIds?: number[] }) => { + if (modalType === 'add') { + await handleAdd(value); + } + + if (modalType === 'edit') { + await handleEdit(value); + } + }, + [visible, type, modalType], + ); + + const handleCancle = useCallback(() => { + setVisible(false); + }, [visible]); + + const onTagItemChange = (tag: TagItem, e: CheckboxChangeEvent) => { + if (e.target.checked) { + const data = [...value, tag]; + onChange?.(data); + } else if (!editable) { + const data = value.filter((item) => item.id !== tag.id); + onChange?.(data); + } + }; + + const loadMoreData = async () => { + const res = await tagsApi.get({ groupId: currentId, pageNo: pageNo + 1 }); + if (res.list.length === 0) { + return; + } + + const newGroup = { + ...currentGroup, + tags: [...(currentGroup?.tags || []), ...res.list], + }; + setCurrentGroup(newGroup as GroupItem); + setPageNo((prev) => prev + 1); + }; + + const onSearchTags = async (e: React.KeyboardEvent) => { + const searchValue = (e.target as HTMLInputElement).value; + const res = await tagsApi.get({ + groupId: currentId, + pageNo: 1, + tagName: searchValue, + }); + const newGroup = { + ...currentGroup, + tags: res.list, + }; + document.getElementById('scrollableDiv')?.scrollTo(0, 0); + setCurrentGroup(newGroup as GroupItem); + setPageNo(1); + }; + + return ( +
+
+ } + placeholder="搜索" + onPressEnter={onSearchTags} + allowClear + disabled={isInEditMode} + /> +
+ +
+ {loading ? ( +
+ +
加载数据中...
+
+ ) : ( +
+ {/* 左侧分组列表 */} +
+
+ 分组 + {editable && !isInEditMode && ( + + )} +
+
+ {renderGroups({ + editable, + groups, + currentId, + onGroupClick, + onEdit: () => handleGroup('edit'), + onDelete: (id) => handleDelete('group', id), + })} +
+
+ + {/* 右侧标签列表 */} +
+
+ 标签 + + {editable && currentGroup && !isInEditMode && ( + + )} + +
+ +
+ + {renderTags({ + editable, + value, + list: currentGroup?.tags ?? [], + total, + loadMoreData, + onEdit: (tag) => handleTag('edit', tag), + onDelete: (id) => handleDelete('tag', id), + onTagItemChange: onTagItemChange, + })} + +
+
+
+ )} +
+ {/* 添加标签Modal */} + +
+ ); +}; + +export default GroupTagCore; diff --git a/src/components/GroupTag/GroupTagModal.tsx b/src/components/GroupTag/GroupTagModal.tsx new file mode 100644 index 0000000..a25a2fa --- /dev/null +++ b/src/components/GroupTag/GroupTagModal.tsx @@ -0,0 +1,63 @@ +// GroupTagModal.tsx - 修复死循环问题 + +import { Modal } from 'antd'; +import React, { useCallback, useEffect, useState } from 'react'; +import GroupTagCore from './GroupTagCore'; +import type { GroupTagModalProps, TagItem } from './types'; + +const GroupTagModal: React.FC = ({ + visible, + title, + width, + editable, + request, + onCancel, + onChange, +}) => { + const [data, setData] = useState([]); + + const handleOk = () => { + if (onChange) { + // const ids = data.map((item) => item.id); + onChange(data); + } + + onCancel(); + }; + + useEffect(() => { + if (!visible) { + setData([]); + } + }, [visible]); + + const handleChange = useCallback( + (data: TagItem[]) => { + setData(data); + }, + [data], + ); + + return ( + + + + ); +}; + +export default GroupTagModal; diff --git a/src/components/GroupTag/GroupTagSelect.less b/src/components/GroupTag/GroupTagSelect.less new file mode 100644 index 0000000..b8e2143 --- /dev/null +++ b/src/components/GroupTag/GroupTagSelect.less @@ -0,0 +1,400 @@ +// GroupTagSelect.less - 添加编辑模式样式 +.group-tag-select-wrapper { + width: 100%; + + .group-tag-select { + width: 100%; + + .ant-select-selector { + min-height: 32px; + } + + .selected-tag { + margin: 2px; + font-size: 12px; + line-height: 20px; + border-radius: 4px; + } + } + + &.disabled { + .selected-tag { + background-color: #f0f0f0; + border-color: #d9d9d9; + color: rgba(0, 0, 0, 0.25); + } + } +} + +.group-tag-select-dropdown-wrapper { + .ant-select-dropdown { + padding: 0; + width: 600px !important; + min-width: 600px !important; + } +} + +.group-tag-select-dropdown { + padding: 0; + width: 100%; + + // 编辑模式下的整体样式 + &.edit-mode { + .search-wrapper { + .ant-input-affix-wrapper { + background-color: #f5f5f5; + + .ant-input { + background-color: #f5f5f5; + cursor: not-allowed; + } + } + } + } + + .search-wrapper { + padding: 8px; + border-bottom: 1px solid #f0f0f0; + + .ant-input-affix-wrapper { + width: 100%; + } + } + + .content-wrapper { + height: 400px; + overflow: hidden; + + .loading-wrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + text-align: center; + } + + .split-layout { + display: flex; + height: 100%; + + .groups-panel { + width: 200px; + border-right: 1px solid #f0f0f0; + display: flex; + flex-direction: column; + + .panel-header { + padding: 8px 12px; + background-color: #fafafa; + border-bottom: 1px solid #f0f0f0; + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 500; + font-size: 14px; + } + + .groups-list { + flex: 1; + overflow-y: auto; + + .group-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + cursor: pointer; + transition: background-color 0.2s; + border-bottom: 1px solid #f5f5f5; + + &:hover { + background-color: #f5f5f5; + } + + &.active { + background-color: #e6f7ff; + border-color: #91d5ff; + } + + // 编辑状态样式 + &.editing { + background-color: #fff7e6; + cursor: default; + + &:hover { + background-color: #fff7e6; + } + } + + // 禁用状态样式 + &.disabled { + cursor: not-allowed; + opacity: 0.6; + + &:hover { + background-color: inherit; + } + + &.active:hover { + background-color: #e6f7ff; + } + } + + .group-content { + flex: 1; + display: flex; + justify-content: space-between; + align-items: center; + + .group-name { + font-size: 13px; + color: #262626; + } + + // 编辑输入框样式 + .edit-input-wrapper { + display: flex; + align-items: center; + gap: 4px; + width: 100%; + + .ant-input { + flex: 1; + font-size: 12px; + } + + .edit-actions { + display: flex; + gap: 2px; + + .ant-btn { + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + + .anticon { + font-size: 10px; + } + } + } + } + } + + .ant-checkbox-wrapper { + margin-left: 8px; + } + } + } + } + + .tags-panel { + flex: 1; + display: flex; + flex-direction: column; + + .panel-header { + padding: 8px 12px; + background-color: #fafafa; + border-bottom: 1px solid #f0f0f0; + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 500; + font-size: 14px; + } + + .tags-list { + flex: 1; + overflow-y: auto; + padding: 4px 0; + + .loading-wrapper { + display: flex; + justify-content: center; + align-items: center; + height: 60px; + } + + .tag-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 12px; + transition: background-color 0.2s; + + &:hover { + background-color: #f5f5f5; + } + + // 编辑状态样式 + &.editing { + background-color: #fff7e6; + + &:hover { + background-color: #fff7e6; + } + } + + // 禁用状态样式 + &.disabled { + opacity: 0.6; + + &:hover { + background-color: inherit; + } + } + + .ant-checkbox-wrapper { + flex: 1; + + span:last-child { + color: #595959; + font-size: 13px; + } + } + + // 编辑状态下只显示标签名称的样式 + .tag-name-only { + flex: 1; + color: #8c8c8c; + font-size: 13px; + + .anticon { + color: #bfbfbf; + } + } + + // 标签操作按钮 + .tag-actions { + display: flex; + gap: 2px; + opacity: 0; + transition: opacity 0.2s; + + .ant-btn { + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + + .anticon { + font-size: 10px; + } + } + } + + &:hover .tag-actions { + opacity: 1; + } + + // 编辑输入框样式 + .edit-input-wrapper { + display: flex; + align-items: center; + gap: 4px; + width: 100%; + + .ant-input { + flex: 1; + font-size: 12px; + } + + .edit-actions { + display: flex; + gap: 2px; + + .ant-btn { + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + + .anticon { + font-size: 10px; + } + } + } + } + } + + .ant-empty { + padding: 40px 20px; + + .ant-empty-description { + color: #8c8c8c; + font-size: 12px; + } + } + } + } + } + } +} + +// 响应式设计 +@media (max-width: 768px) { + .group-tag-select-dropdown-wrapper { + .ant-select-dropdown { + width: 90vw !important; + min-width: 320px !important; + } + } + + .group-tag-select-dropdown { + .content-wrapper { + height: 300px; + + .split-layout { + .groups-panel { + width: 120px; + + .panel-header { + font-size: 12px; + padding: 6px 8px; + } + + .groups-list { + .group-item { + padding: 6px 8px; + + .group-name { + font-size: 12px; + } + } + } + } + + .tags-panel { + .panel-header { + font-size: 12px; + padding: 6px 8px; + } + + .tags-list { + .tag-item { + padding: 4px 8px; + + .ant-checkbox-wrapper { + span:last-child { + font-size: 12px; + } + } + + .tag-name-only { + font-size: 12px; + } + } + } + } + } + } + } +} diff --git a/src/components/GroupTag/GroupTagSelect.tsx b/src/components/GroupTag/GroupTagSelect.tsx new file mode 100644 index 0000000..7adbc67 --- /dev/null +++ b/src/components/GroupTag/GroupTagSelect.tsx @@ -0,0 +1,100 @@ +// GroupTagSelect.tsx - 适配真实数据结构 + +import { Select, Tag } from 'antd'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import GroupTagCore from './GroupTagCore'; +import type { GroupTagSelectProps, TagItem } from './types'; +import './GroupTagSelect.less'; + +const GroupTagSelect: React.FC = ({ + value = [], + onChange, + placeholder = '请选择标签', + className = '', + request, + editable = false, +}) => { + const [dropdownVisible, setDropdownVisible] = useState(false); + const [selectedTags, setSelectedTags] = useState([]); + + useEffect(() => { + const v = selectedTags.map((tag) => tag.id); + onChange?.(v); + }, [selectedTags]); + const onTagsItem = (value: TagItem[]) => { + console.log(value, 'value'); + setSelectedTags(value); + }; + // 渲染下拉内容 + const dropdownRender = useCallback( + () => ( + + ), + [selectedTags, request, editable], + ); + + // const + + // 获取选中标签的详细信息 + const options = useMemo(() => { + return selectedTags.map((item: TagItem) => ({ + ...item, + label: item.tagName, + value: item.id, + })); + }, [selectedTags]); + return ( +
+ + + )} + {type === 'tag' ? ( + + + + ) : ( + + + + )} + + + ); +}; + +export default TagsModal; diff --git a/src/components/GroupTag/component.tsx b/src/components/GroupTag/component.tsx new file mode 100644 index 0000000..47d96f6 --- /dev/null +++ b/src/components/GroupTag/component.tsx @@ -0,0 +1,179 @@ +import { DeleteFilled, EditFilled } from '@ant-design/icons'; +import { + Button, + Checkbox, + type CheckboxChangeEvent, + Divider, + Empty, + List, + Skeleton, +} from 'antd'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import type { GroupItem, TagItem } from './types'; + +interface GroupsProps { + groups: GroupItem[]; + onGroupClick: (group: GroupItem) => void; + currentId?: number; + editable?: boolean; + total?: number; + loadMoreData?: () => void; + pageSize?: number; + onEdit?: () => void; + onDelete?: (id: number) => void; +} +export const renderGroups = (data: GroupsProps) => { + const { + groups, + currentId, + total = groups.length, + onEdit, + onDelete, + loadMoreData, + onGroupClick, + pageSize = 10, + editable = false, + } = data; + const hasMore = groups.length < total * pageSize; + if (groups.length === 0) { + return ( + + ); + } + + return ( + {}} + hasMore={loadMoreData ? hasMore : false} + loader={} + endMessage={loadMoreData ? 我已经是底线了 : ''} + scrollableTarget="scrollableDiv" + > + ( + onGroupClick(item)} + className={`group-item ${currentId === item.id ? 'active' : ''}`} + > +
{item.groupName}
+ {editable && ( +
+
+ )} +
+ )} + /> +
+ ); +}; + +interface GroupTagProps { + list: TagItem[]; + total: number; + // onTagItemClick: (tag: TagItem) => void; + onEdit?: (tag: TagItem) => Promise | void; + loadMoreData: () => void; + onDelete?: (id: number) => void; + editable?: boolean; + pageSize?: number; + value: TagItem[]; + onTagItemChange: (tag: TagItem, e: CheckboxChangeEvent) => void; +} +export const renderTags = (data: GroupTagProps) => { + const { + list, + total = list.length, + loadMoreData, + onEdit, + onDelete, + onTagItemChange, + editable, + value, + } = data; + if (list.length === 0) { + return ( + + ); + } + console.log(list.length); + return ( + } + endMessage={我已经是底线了} + scrollableTarget="scrollableDiv" + > + ( + onTagItemClick(item)} + className="tag-item" + > +
+ {!editable && ( + tag.id === item.id) ? true : false + !!value.find((tag) => tag.id === item.id) + } + onChange={(e) => onTagItemChange(item, e)} + style={{ marginRight: '8px' }} + /> + )} + {item.tagName} +
+ + {editable && ( +
+
+ )} +
+ )} + /> +
+ ); +}; diff --git a/src/components/GroupTag/types.ts b/src/components/GroupTag/types.ts new file mode 100644 index 0000000..cb4b8e9 --- /dev/null +++ b/src/components/GroupTag/types.ts @@ -0,0 +1,96 @@ +// types.ts +export interface TagItem { + id: number; + tagName: string; + createTime: string; + groupId?: number; // 可选,因为API返回的数据可能没有这个字段 +} + +export interface GroupItem { + id: number; + groupName: string; + createTime?: string; + tags?: TagItem[]; +} + +// API响应类型 +export interface TagsResponse { + list: TagItem[]; + total: number; +} + +// export interface GroupsResponse { +// data: GroupItem[]; +// } + +// 核心组件的Props +export interface GroupTagCoreProps { + // groups: GroupItem[]; + requset: RequsetConfig; + height?: number; + editable?: boolean; + onChange?: (value: TagItem[]) => void; + value: TagItem[]; + // selectedTags: number[]; + // onTagChange?: (selectedTags: number[]) => void; + // onGroupClick?: (groupId: number) => void; + // onLoadMoreTags?: (groupId: number, page: number) => Promise; + // loading?: boolean; + // loadingGroups?: Set; + // loadingMoreTags?: Set; + // editable?: boolean; + // onAddGroup?: () => void; + // onAddTag?: (groupId: number) => void; + // onDeleteGroup?: (groupId: number) => Promise | void; + // onDeleteTag?: (tagId: number) => Promise | void; + // onEditGroup?: (groupId: number, newName: string) => Promise | void; + // onEditTag?: (tagId: number, newName: string) => Promise | void; + // className?: string; + // height?: number; + // showSearch?: boolean; + // pageSize?: number; +} + +interface RequsetConfig { + groupsApi: { + get: (params?: T) => Promise; + create: (params: any) => Promise; + delete: (id: number) => Promise; + update: (params: T) => Promise; + }; + tagsApi: { + get: (params?: T) => Promise; + create: (params: T) => Promise; + delete: (id: number) => Promise; + update: (params: T) => Promise; + }; +} +// Select组件的Props +export interface GroupTagSelectProps { + value?: number[]; + onChange?: (value: number[]) => void; + placeholder?: string; + className?: string; + request: RequsetConfig; + editable?: boolean; + // onAddGroup?: (groupName: string) => Promise | void; + // onAddTag?: (groupId: number, tagName: string) => Promise | void; + // onDeleteGroup?: (groupId: number) => Promise | void; + // onDeleteTag?: (tagId: number) => Promise | void; + // pageSize?: number; +} + +// Modal组件的Props +export interface GroupTagModalProps { + visible: boolean; + editable?: boolean; + onChange?: (value: TagItem[]) => void; + className?: string; + onCancel: () => void; + request: RequsetConfig; + onOk?: (selectedTags: TagItem[]) => void; + title?: string; + width?: number; + height?: number; + value?: TagItem[]; +} diff --git a/src/components/RenameRule/index.less b/src/components/RenameRule/index.less new file mode 100644 index 0000000..537e2f9 --- /dev/null +++ b/src/components/RenameRule/index.less @@ -0,0 +1,170 @@ +// RenameRule.less +.rename-rule-card { + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + .rename-rule-card-preview { + display: flex; + .left { + width: 200px; + } + } + &:hover { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); + } + + .ant-card-head { + background: linear-gradient(135deg, #1890ff, #36cfc9); + border-radius: 8px 8px 0 0; + + .ant-card-head-title { + color: white; + font-weight: 600; + } + + .ant-card-extra { + .ant-btn { + border-radius: 6px; + transition: all 0.3s ease; + + &:hover { + background: rgba(255, 255, 255, 0.3) !important; + transform: translateY(-1px); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + } + } + + .ant-card-body { + padding: 24px; + } + + // 输入框样式 + .ant-input, + .ant-input-number, + .ant-select-selector { + border-radius: 6px; + transition: all 0.3s ease; + + &:focus, + &.ant-select-focused { + border-color: #40a9ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } + } + + // 行间距 + .ant-row { + align-items: center; + + .ant-col { + display: flex; + align-items: center; + } + } + + // 预览信息样式 + .preview-info { + background: #f6f8fa; + border: 1px solid #e1e4e8; + border-radius: 6px; + padding: 12px 16px; + margin-top: 8px; + + .ant-typography { + margin: 0; + } + } + + // Checkbox 样式 + .ant-checkbox-wrapper { + .ant-checkbox { + &.ant-checkbox-checked { + .ant-checkbox-inner { + background-color: #1890ff; + border-color: #1890ff; + } + } + } + } + + // 响应式设计 + @media (max-width: 768px) { + .ant-card-body { + padding: 16px; + } + + .ant-row { + margin-bottom: 16px; + + .ant-col { + margin-bottom: 8px; + + &:last-child { + margin-bottom: 0; + } + } + } + + .ant-card-extra { + .ant-space { + flex-direction: column; + width: 100%; + + .ant-btn { + width: 100%; + } + } + } + } + + // 暗色主题 + @media (prefers-color-scheme: dark) { + background: #1f1f1f; + border-color: #303030; + + .ant-card-head { + background: linear-gradient(135deg, #1668dc, #13c2c2); + } + + .preview-info { + background: #262626; + border-color: #434343; + } + } + + // 动画效果 + .ant-space-item { + transition: all 0.3s ease; + } + + // 禁用状态 + .ant-input:disabled, + .ant-input-number:disabled { + background-color: #f5f5f5; + opacity: 0.6; + cursor: not-allowed; + } +} + +// 全局动画 +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.rename-rule-card { + animation: fadeIn 0.3s ease-out; +} diff --git a/src/components/RenameRule/index.tsx b/src/components/RenameRule/index.tsx new file mode 100644 index 0000000..1e96eac --- /dev/null +++ b/src/components/RenameRule/index.tsx @@ -0,0 +1,398 @@ +// RenameRule.tsx - 修复占位符替换顺序 + +import { + DownOutlined, + ExclamationCircleOutlined, + QuestionCircleOutlined, + UpOutlined, +} from '@ant-design/icons'; +import { + Alert, + Button, + Card, + Input, + InputNumber, + Select, + Space, + Tooltip, + Typography, +} from 'antd'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import './index.less'; + +const { Text } = Typography; +const { Option } = Select; + +export interface FileItem { + id: number; + originalName: string; +} + +export interface RenameRule { + pattern: string; + startNumber: number; + numberDirection: 'up' | 'down'; + matchPattern: string; + useMatch: boolean; + numberPadding: number; +} + +export interface RenameResult extends FileItem { + newName: string; + isChanged: boolean; + isMatched: boolean; +} + +interface RenameRuleProps { + files: FileItem[]; + onPreview?: (results: RenameResult[]) => void; + onRename?: (results: RenameResult[]) => void; + loading?: boolean; + className?: string; +} + +// 占位符配置 +// const PLACEHOLDERS = [ +// { key: "$n", desc: "数字序号(不补零)", example: "1, 2, 3..." }, +// { key: "$nn", desc: "数字序号(2位补零)", example: "01, 02, 03..." }, +// { key: "$nnn", desc: "数字序号(3位补零)", example: "001, 002, 003..." }, +// { key: "$nnnn", desc: "数字序号(4位补零)", example: "0001, 0002, 0003..." }, +// { key: "$name", desc: "原文件名", example: "photo1" }, +// { key: "$ext", desc: "扩展名(不含点)", example: "jpg" }, +// { key: "$EXT", desc: "扩展名(大写)", example: "JPG" }, +// ]; + +const RenameRuleComponent: React.FC = ({ + files, + onPreview, + className, +}) => { + const [rule, setRule] = useState({ + pattern: '', + startNumber: 1, + numberDirection: 'up', + matchPattern: '', + useMatch: false, + numberPadding: 2, + }); + + const [matchError, setMatchError] = useState(''); + + // 初始化时自动设置模式 + useEffect(() => { + console.log(files); + if (files.length > 0 && !rule.pattern) { + setRule((prev) => ({ ...prev, pattern: '$name$nn' })); + } + }, [files, rule.pattern]); + + // 安全的正则表达式匹配 + const isFileMatched = useCallback( + (file: FileItem, pattern: string): boolean => { + if (!pattern.trim()) return true; + + try { + const regex = new RegExp(pattern, 'i'); + const fullFileName = file.originalName; + + return ( + regex.test(file.originalName) || + // regex.test(file.extension) || + // regex.test(file.extension?.replace(".", "")) || + regex.test(fullFileName) + ); + } catch (error) { + console.warn('正则表达式错误:', error); + setMatchError( + `正则表达式错误: ${ + error instanceof Error ? error.message : '未知错误' + }`, + ); + return false; + } + }, + [], + ); + + // 修复的占位符替换函数 + const replacePlaceholders = useCallback( + (pattern: string, file: FileItem, fileIndex: number) => { + let result = pattern; + + // 计算序号 + let number: number; + if (rule.numberDirection === 'up') { + number = rule.startNumber + fileIndex; + } else { + number = rule.startNumber - fileIndex; + } + + // 先用临时标记替换非数字占位符 + const tempMarkers = { + name: '___TEMP_NAME___', + ext: '___TEMP_EXT___', + EXT: '___TEMP_EXT_UPPER___', + }; + + // 第一步:替换非数字占位符为临时标记 + result = result.replace(/\$name/g, tempMarkers.name); + result = result.replace(/\$EXT/g, tempMarkers.EXT); + result = result.replace(/\$ext/g, tempMarkers.ext); + + // 第二步:替换数字占位符(按长度从长到短,避免冲突) + result = result.replace(/\$nnnn/g, number.toString().padStart(4, '0')); + result = result.replace(/\$nnn/g, number.toString().padStart(3, '0')); + result = result.replace(/\$nn/g, number.toString().padStart(2, '0')); + result = result.replace(/\$n/g, number.toString()); + + // 第三步:将临时标记替换为实际值 + result = result.replace( + new RegExp(tempMarkers.name, 'g'), + file.originalName, + ); + // result = result.replace( + // new RegExp(tempMarkers.ext, "g"), + // // file.extension.replace(".", "") + // ); + // result = result.replace( + // new RegExp(tempMarkers.EXT, "g"), + // file.extension.replace(".", "").toUpperCase() + // ); + + return result; + }, + [rule.startNumber, rule.numberDirection], + ); + + // 使用 useMemo 缓存计算结果 + const previewResults = useMemo(() => { + setMatchError(''); + + const matchedFiles: FileItem[] = []; + const allResults: RenameResult[] = []; + + files.forEach((file) => { + const isMatched = rule.useMatch + ? isFileMatched(file, rule.matchPattern) + : true; + + if (isMatched) { + matchedFiles.push(file); + } + }); + + files.forEach((file) => { + const isMatched = rule.useMatch + ? isFileMatched(file, rule.matchPattern) + : true; + + if (!isMatched || !rule.pattern) { + allResults.push({ + ...file, + newName: file.originalName, + isChanged: false, + isMatched, + }); + return; + } + + const matchedIndex = matchedFiles.findIndex((f) => f.id === file.id); + const newName = replacePlaceholders(rule.pattern, file, matchedIndex); + const isChanged = newName !== file.originalName; + + allResults.push({ + ...file, + newName, + isChanged, + isMatched, + }); + }); + + return allResults; + }, [files, rule, isFileMatched, replacePlaceholders]); + + // 调用预览回调 + useEffect(() => { + if (onPreview) { + onPreview(previewResults); + } + }, [previewResults]); + + // // 重置规则 + // const handleReset = useCallback(() => { + // setRule({ + // pattern: "$name$nn", + // startNumber: 1, + // numberDirection: "up", + // matchPattern: "", + // useMatch: false, + // numberPadding: 2, + // }); + // setMatchError(""); + // }, []); + + // 规则更新处理函数 + const updateRule = useCallback((updates: Partial) => { + setRule((prev) => ({ ...prev, ...updates })); + if (updates.matchPattern !== undefined) { + setMatchError(''); + } + }, []); + + // // 插入占位符 + // const insertPlaceholder = useCallback( + // (placeholder: string) => { + // const input = document.querySelector( + // ".pattern-input" + // ) as HTMLInputElement; + // if (input) { + // const start = input.selectionStart || 0; + // const end = input.selectionEnd || 0; + // const currentValue = rule.pattern; + // const newValue = + // currentValue.slice(0, start) + placeholder + currentValue.slice(end); + // updateRule({ pattern: newValue }); + + // setTimeout(() => { + // input.focus(); + // input.setSelectionRange( + // start + placeholder.length, + // start + placeholder.length + // ); + // }, 0); + // } + // }, + // [rule.pattern, updateRule] + // ); + + // // 递增起始编号 + // const incrementStartNumber = useCallback(() => { + // updateRule({ startNumber: rule.startNumber + 1 }); + // }, [rule.startNumber, updateRule]); + + // // 递减起始编号 + // const decrementStartNumber = useCallback(() => { + // updateRule({ startNumber: Math.max(0, rule.startNumber - 1) }); + // }, [rule.startNumber, updateRule]); + + // 快速设置常用模式 + const setQuickPattern = useCallback( + (pattern: string) => { + updateRule({ pattern }); + }, + [updateRule], + ); + return ( + +
+
+ + 预览: + + {previewResults.map((item) => ( +

{`${item.newName}`}

+ ))} +
+ + {/* 错误提示 */} + {matchError && ( + } + closable + onClose={() => setMatchError('')} + /> + )} + + + 快速模式: + + + + + + + {/* 命名模式 */} + + + updateRule({ pattern: e.target.value })} + suffix={ + + + + } + /> + + updateRule({ matchPattern: e.target.value })} + /> + + 起始编号: + updateRule({ startNumber: value || 1 })} + style={{ width: '60%', textAlign: 'center' }} + controls={true} + /> + + + +
+
+ ); +}; + +export default React.memo(RenameRuleComponent); diff --git a/src/components/TagEditor/index.tsx b/src/components/TagEditor/index.tsx index 512dcec..a8937be 100644 --- a/src/components/TagEditor/index.tsx +++ b/src/components/TagEditor/index.tsx @@ -7,11 +7,12 @@ import React, { useMemo, useState } from 'react'; interface TagEditorProps { value?: string[]; - onChange?: (value: string[]) => void; + onChange?: (value: string[], active?: number) => void; placeholder?: string; maxCount?: number; tagProps?: TagProps; disabled?: boolean; + editable?: boolean; } const TagEditor: React.FC = ({ @@ -20,6 +21,7 @@ const TagEditor: React.FC = ({ placeholder = '请输入标签', maxCount, tagProps, + editable = true, disabled = false, }) => { // const [tags, setTags] = useState(value); @@ -27,12 +29,15 @@ const TagEditor: React.FC = ({ const [inputValue, setInputValue] = useState(''); const tags = useMemo(() => { + console.log(value); + return value || []; }, [value]); const handleClose = (removedTag: string) => { + const active = tags.indexOf(removedTag); const newTags = tags.filter((tag) => tag !== removedTag); - onChange?.(newTags); + onChange?.(newTags, active); }; const showInput = () => { @@ -85,6 +90,7 @@ const TagEditor: React.FC = ({ /> ) : ( canAddMore && + editable && !disabled && ( = ({ // 实际的后端接口上传 const uploadToServer = async (file: File) => { const formData = new FormData(); - formData.append('file', file); - // formData.append("type", "audio"); // 可以添加额外参数 + formData.append('files', file); const response = await createSample(formData); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return response.json(); + message.success('上传成功!'); + return response; }; const customRequest: UploadProps['customRequest'] = async (options) => { @@ -81,27 +77,22 @@ const AudioUploader: React.FC = ({ try { setUploading(true); - // 模拟进度更新 onProgress?.({ percent: 10 }); - // 调用后端接口 const result = await uploadToServer(file as File); - + console.log(result, 'res'); onProgress?.({ percent: 100 }); - - if (result.success && result.data) { + if (result) { // 构造返回数据 const responseData = { - url: result.data.url, - name: result.data.filename || (file as File).name, + url: result[0].fileUrl, + name: result[0].fileName || (file as File).name, uid: (file as any).uid, status: 'done' as const, - response: result.data, + response: result, }; - onSuccess?.(responseData); - message.success('音频上传成功!'); } else { throw new Error(result.message || '上传失败'); } @@ -114,13 +105,13 @@ const AudioUploader: React.FC = ({ } }; - const handleChange: UploadProps['onChange'] = ({ - fileList: newFileList, - file, - }) => { - setFileList(newFileList); - onChange?.(newFileList); - }; + // const handleChange: UploadProps["onChange"] = ({ + // fileList: newFileList, + // file, + // }) => { + // setFileList(newFileList); + // onChange?.(newFileList); + // }; const handleRemove = (file: UploadFile): boolean => { const newFileList = fileList.filter((item) => item.uid !== file.uid); diff --git a/src/pages/ai/sample-tag/components/tag-manager.tsx b/src/pages/ai/sample-tag/components/tag-manager.tsx new file mode 100644 index 0000000..c66cc13 --- /dev/null +++ b/src/pages/ai/sample-tag/components/tag-manager.tsx @@ -0,0 +1,83 @@ +// 使用示例 - App.tsx + +import { Modal, message, Space } from 'antd'; +import React, { useEffect, useState } from 'react'; +import RenameRule, { + type FileItem, + type RenameResult, +} from '@/components/RenameRule'; +import { updateSamples } from '@/services/ai/sample'; + +const TagManager: React.FC<{ + visible: boolean; + files: FileItem[]; + onCancel?: (e: React.MouseEvent) => void; + onOk?: () => void; +}> = ({ visible, files: beforeFiles, onCancel, onOk }) => { + const [files, setFiles] = useState(beforeFiles); + useEffect(() => { + setFiles(beforeFiles); + }, [beforeFiles]); + + const [previewResults, setPreviewResults] = useState([]); + const [loading, setLoading] = useState(false); + + // 预览回调o+ + const handlePreview = (results: RenameResult[]) => { + setPreviewResults(results); + }; + + const handleOk = async () => { + try { + setLoading(true); + const results = previewResults.map((result) => ({ + id: result.id, + sampleName: `${result.newName}`, + })); + await updateSamples(results); + onOk?.(); + message.success('重命名成功'); + } finally { + setLoading(false); + } + }; + + // // 重命名回调 + // const handleRename = async (results: RenameResult[]) => { + // setLoading(true); + // try { + // console.log("执行预览:", results); + // // 模拟API调用 + // await new Promise((resolve) => setTimeout(resolve, 1000)); + // message.success(`成功重命名 ${results.length} 个文件`); + // } catch (error) { + // message.error("重命名失败"); + // } finally { + // setLoading(false); + // } + // }; + + return ( + +
+ + {/* 独立的重命名规则组件 */} + + +
+
+ ); +}; + +export default TagManager; diff --git a/src/pages/ai/sample-tag/config.tsx b/src/pages/ai/sample-tag/config.tsx index 3212101..989c70c 100644 --- a/src/pages/ai/sample-tag/config.tsx +++ b/src/pages/ai/sample-tag/config.tsx @@ -1,14 +1,21 @@ -import type { - ProColumns, - ProFormColumnsType, -} from '@ant-design/pro-components'; -import dayjs from 'dayjs'; -import TagEditor from '@/components/TagEditor'; -import TinyMCEEditor from '@/components/Tinymce'; -export const baseTenantColumns: ProColumns[] = [ +import type { ProColumns } from '@ant-design/pro-components'; +import GroupTagSelect from '@/components/GroupTag/GroupTagSelect'; +import { + type AiSampleRespVO, + createSampleTag, + createSampleTagGroup, + deleteSampleTag, + deleteSampleTagGroup, + getSampleTagGroup, + getSampleTagPage, + updateSampleTag, + updateSampleTagGroup, +} from '@/services/ai/sample'; +export const baseTenantColumns: ProColumns[] = [ { title: '样本名称', - dataIndex: 'sample_name', + dataIndex: 'sampleName', + // width: 500, }, { title: '注释', @@ -19,6 +26,36 @@ export const baseTenantColumns: ProColumns[] = [ title: '标签', hideInTable: true, dataIndex: 'tag_name', + valueType: 'select', + search: { + transform: (value) => { + console.log(value); + return value.join(','); + }, + }, + + renderFormItem: (_) => { + return ( + + ); + }, }, { title: '样本格式', @@ -27,107 +64,107 @@ export const baseTenantColumns: ProColumns[] = [ }, ]; -export const formColumns = (data: { - type: string; - grade: number; -}): ProFormColumnsType[] => [ - { - title: '类目', - dataIndex: 'grade', - valueType: 'radio', - fieldProps: { - value: data.grade || 1, - options: [ - { label: '一级类目', value: 1 }, - { label: '二级类目', value: 2 }, - { label: '三级类目', value: 3 }, - ], - disabled: data.type === 'create', - }, - }, - { - title: '类目名称', - dataIndex: 'username', - formItemProps: { - rules: [ - { - required: true, - message: '请输入用户名', - }, - ], - }, - }, - { - title: '排序权重', - dataIndex: 'sort', - valueType: 'digit', - }, - { - title: '类目描述', - dataIndex: 'description', - valueType: 'textarea', - renderFormItem: () => { - return ; - }, - }, - { - title: '关联父级', - dataIndex: 'parentId', - valueType: 'select', - hideInForm: data.grade - 1 === 0, - }, - { - title: '类目icon', - dataIndex: 'icon', - }, - { - title: '类目标签', - dataIndex: 'tages', - renderFormItem: () => { - return ( - - ); - }, - }, - { - title: '类目状态', - dataIndex: 'status', - hideInForm: data.type === 'create', - }, - { - title: '类目ID', - dataIndex: 'categoryId', - hideInForm: data.type === 'create', - }, - { - title: '创建时间', - dataIndex: 'createTime', - valueType: 'dateTime', - hideInForm: data.type === 'create', - }, - { - title: '创建人', - dataIndex: 'creator', - hideInForm: data.type === 'create', - }, - { - title: '更新时间', - dataIndex: 'updateTime', - valueType: 'dateTime', - hideInForm: data.type === 'create', - }, - { - title: '更新人', - dataIndex: 'updator', - hideInForm: data.type === 'create', - }, -]; +// export const formColumns = (data: { +// type: string; +// grade: number; +// }): ProFormColumnsType[] => [ +// { +// title: "类目", +// dataIndex: "grade", +// valueType: "radio", +// fieldProps: { +// value: data.grade || 1, +// options: [ +// { label: "一级类目", value: 1 }, +// { label: "二级类目", value: 2 }, +// { label: "三级类目", value: 3 }, +// ], +// disabled: data.type === "create", +// }, +// }, +// { +// title: "类目名称", +// dataIndex: "username", +// formItemProps: { +// rules: [ +// { +// required: true, +// message: "请输入用户名", +// }, +// ], +// }, +// }, +// { +// title: "排序权重", +// dataIndex: "sort", +// valueType: "digit", +// }, +// { +// title: "类目描述", +// dataIndex: "description", +// valueType: "textarea", +// renderFormItem: () => { +// return ; +// }, +// }, +// { +// title: "关联父级", +// dataIndex: "parentId", +// valueType: "select", +// hideInForm: data.grade - 1 === 0, +// }, +// { +// title: "类目icon", +// dataIndex: "icon", +// }, +// { +// title: "类目标签", +// dataIndex: "tages", +// renderFormItem: () => { +// return ( +// +// ); +// }, +// }, +// { +// title: "类目状态", +// dataIndex: "status", +// hideInForm: data.type === "create", +// }, +// { +// title: "类目ID", +// dataIndex: "categoryId", +// hideInForm: data.type === "create", +// }, +// { +// title: "创建时间", +// dataIndex: "createTime", +// valueType: "dateTime", +// hideInForm: data.type === "create", +// }, +// { +// title: "创建人", +// dataIndex: "creator", +// hideInForm: data.type === "create", +// }, +// { +// title: "更新时间", +// dataIndex: "updateTime", +// valueType: "dateTime", +// hideInForm: data.type === "create", +// }, +// { +// title: "更新人", +// dataIndex: "updator", +// hideInForm: data.type === "create", +// }, +// ]; // { // title: "模板内容", diff --git a/src/pages/ai/sample-tag/detail.tsx b/src/pages/ai/sample-tag/detail.tsx index 75b1261..40c7811 100644 --- a/src/pages/ai/sample-tag/detail.tsx +++ b/src/pages/ai/sample-tag/detail.tsx @@ -1,81 +1,332 @@ -import { - ProForm, - ProFormCascader, - ProFormCheckbox, - ProFormColorPicker, - ProFormDigit, - ProFormDigitRange, - ProFormGroup, - ProFormRadio, - ProFormSelect, - ProFormSlider, - ProFormSwitch, - ProFormText, -} from '@ant-design/pro-components'; -import { Switch } from 'antd'; +import { ProForm, ProFormGroup, ProFormText } from '@ant-design/pro-components'; +import { Button, message, Space, Tag } from 'antd'; +import type { RowSelectionType } from 'antd/es/table/interface'; import type { FormInstance } from 'antd/lib'; -import Mock from 'mockjs'; -import { useRef, useState } from 'react'; -import TagEditor from '@/components/TagEditor'; +import dayjs from 'dayjs'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import GroupTagModal from '@/components/GroupTag/GroupTagModal'; +import type { TagItem } from '@/components/GroupTag/types'; +import type { FileItem } from '@/components/RenameRule'; +import { + createSampleTag, + createSampleTagGroup, + deleteSample, + deleteSampleTag, + deleteSampleTagGroup, + deleteSampleTagRelate, + getSampleTagGroup, + getSampleTagPage, + relateSample, + updateSamples, + updateSampleTag, + updateSampleTagGroup, +} from '@/services/ai/sample'; +import TagManager from './components/tag-manager'; -export const waitTime = (time: number = 100) => { - return new Promise((resolve) => { - setTimeout(() => { - resolve(true); - }, time); - }); -}; +interface SampleTagDetailProps { + onRefresh?: (type?: string) => void; + type: RowSelectionType; + data?: T[]; +} -const SampleTagDetail = () => { - const [readonly, setReadonly] = useState(false); +const SampleTagDetail = >( + props: SampleTagDetailProps, +) => { const formRef = useRef(null); + const { type = 'radio', data } = props; + const [modalVisible, setModalVisible] = useState(false); + const [tagManagerVisible, setTagManagerVisible] = useState(false); + const [value, setValue] = useState<{ + tagId?: number | number[]; + id?: number; + remark?: string; + sampleIds?: number[]; + tags?: TagItem[]; + updateTime?: number; + createTime?: number; + sampleSize?: number; + sampleMineType?: string; + sampleTime?: number; + }>({}); + const handleAddTag = useCallback(() => { + setModalVisible(true); + }, [modalVisible]); + + // const radioData = data && data[0]; + + // // 当 radioData 改变时更新表单值 + useEffect(() => { + if (type === 'radio') { + const item = data?.[0]; + setValue({ ...item, sampleIds: [item?.id] }); + } else { + const sampleIds = data?.map((sample) => sample.id) || []; + const tags = data?.map((sample) => { + return sample.tags; + }); + const map = new Map(); + const tags_total = tags + ?.flat() + .filter((v) => !map.has(v.id) && map.set(v.id, v)); + setValue({ tags: tags_total as TagItem[], sampleIds }); + } + }, [type, data]); + useEffect(() => { + formRef.current?.setFieldsValue(value); + }, [value]); + + const tagNames = useMemo(() => { + return ( + data?.map((tag) => { + return { + id: tag.id, + originalName: tag.sampleName, + }; + }) || [] + ); + }, [data]); + + const onListAddTag = async (tags: TagItem[]) => { + const sampleIds = data?.map((sample) => sample.id); + const ids = tags?.map((tag) => tag.id); + await relateSample({ tagId: ids, sampleIds: sampleIds as number[] }); + setModalVisible(false); + const map = new Map(); + const newTags = [...(value.tags || []), ...tags]; + const tags_total = newTags + ?.flat() + .filter((v) => !map.has(v.id) && map.set(v.id, v)); + setValue({ ...value, tags: tags_total }); + // 执行刷新回调 + message.success('添加标签成功'); + if (props.onRefresh) { + props.onRefresh(); + } + }; + + const handleDeleteAll = async () => { + const ids = data?.map((sample) => sample.id); + if (ids) { + await deleteSample(ids.join(',')); + message.success('删除成功'); + + if (props.onRefresh) { + props.onRefresh('delete'); + } + } + }; + + // 下载 + const handleDownloadAll = () => {}; + + const handleTagManager = () => { + setTagManagerVisible(true); + }; + + const forMap = (tags: TagItem[]) => ( +
+ {tags.map((tag) => ( + { + e.preventDefault(); + await deleteSampleTagRelate({ + sampleIds: value.sampleIds || [], + tagId: [tag.id], + }); + message.success('删除成功'); + props?.onRefresh?.(); + setValue({ ...value, tags: tags.filter((t) => t.id !== tag.id) }); + }} + > + {tag.tagName} + + ))} +
+ ); + + const onRename = () => { + props.onRefresh?.(); + setTagManagerVisible(false); + }; return ( <> - {/* */} - { - console.log(values); - }} - onFinish={async (value) => console.log(value)} - > - {/* */} + + {type === 'radio' && ( + + {data?.[0].sampleFilePath && ( + + )} + + )} + {type === 'radio' && ( + { + if (e.target.value) { + const newData = + data?.map((sample) => { + return { + id: sample.id, + sampleName: e.target.value, + }; + }) || []; + await updateSamples(newData); + props?.onRefresh?.(); + message.success('更新成功'); + } + }, + }} + rules={[{ required: true, message: '样本名称不能为空' }]} + /> + )} { + if (e.target.value) { + const newData = + data?.map((sample) => { + return { + id: sample.id, + remark: e.target.value, + }; + }) || []; + await updateSamples(newData); + props?.onRefresh?.(); + } + }, + }} + name="remark" + placeholder="请输入注释" /> - - + {/* */} + {forMap(value.tags || [])} + + + + + 添加日期: + {dayjs(value.createTime).format('YYYY-MM-DD HH:mm:ss')} + + + 修改日期 + {dayjs(value.updateTime).format('YYYY-MM-DD HH:mm:ss')} + + + 文件大小: + {value.sampleSize} + + + 格式: + {value.sampleMineType} + + + 时长: + {value.sampleTime} + + + {type === 'checkbox' && ( + <> + + + + + + )} + setModalVisible(false)} + onChange={onListAddTag} + editable={false} + value={value?.tags} + request={{ + groupsApi: { + get: getSampleTagGroup, + create: createSampleTagGroup, + delete: deleteSampleTagGroup, + update: updateSampleTagGroup, + }, + tagsApi: { + get: getSampleTagPage, + create: createSampleTag, + delete: deleteSampleTag, + update: updateSampleTag, + }, + }} + title="管理技术标签" + width={800} + height={500} + /> + setTagManagerVisible(false)} + > + + + + ); }; -export default SampleTagDetail; +export default React.memo(SampleTagDetail) as >( + props: SampleTagDetailProps, +) => React.ReactElement; diff --git a/src/pages/ai/sample-tag/index.module.less b/src/pages/ai/sample-tag/index.module.less index 5508333..bede711 100644 --- a/src/pages/ai/sample-tag/index.module.less +++ b/src/pages/ai/sample-tag/index.module.less @@ -2,14 +2,20 @@ display: flex; width: 100%; background: #fff; + overflow: auto; :global { .ant-pro-table { flex: 1 auto; } .detail { + display: flex; + flex-direction: column; border-left: 1px solid #e8e8e8; width: 400px; padding: 16px; + form { + flex: 1; + } } } } diff --git a/src/pages/ai/sample-tag/index.tsx b/src/pages/ai/sample-tag/index.tsx index 88463e2..93694c5 100644 --- a/src/pages/ai/sample-tag/index.tsx +++ b/src/pages/ai/sample-tag/index.tsx @@ -1,59 +1,125 @@ import type { ActionType } from '@ant-design/pro-components'; +import type { RowSelectionType } from 'antd/es/table/interface'; import React, { useRef, useState } from 'react'; import EnhancedProTable from '@/components/EnhancedProTable'; import type { ToolbarAction } from '@/components/EnhancedProTable/types'; +import GroupTagModal from '@/components/GroupTag/GroupTagModal'; import UploadCard from '@/components/Upload/UploadCard'; +import { + type AiSampleRespVO, + createSampleTag, + createSampleTagGroup, + deleteSampleTag, + deleteSampleTagGroup, + getSamplePage, + getSampleTagGroup, + getSampleTagPage, + type SampleReqVo, + updateSampleTag, + updateSampleTagGroup, +} from '@/services/ai/sample'; import { baseTenantColumns } from './config'; import SampleTagDetail from './detail'; import styles from './index.module.less'; const SampleTag: React.FC = () => { const tableRef = useRef(null); - // const [detail, setDetail] = useState(null); - // const [selectedRowKeys, setSelectedRowKeys] = useState([]); - const handleAll = () => { - // console.log(tableRef.current.getSelectedRowKeys()); + + const [modalVisible, setModalVisible] = useState(false); + + const [selectedRows, setSelectedRows] = useState([]); + + const [selectTableType, setSelectTableType] = + useState('radio'); + + const handleAll = (selectTableType: RowSelectionType) => { + setSelectedRows([]); + setSelectTableType(selectTableType === 'radio' ? 'checkbox' : 'radio'); + }; + + const handleTags = () => { + setModalVisible(true); }; const toolbarActions: ToolbarAction[] = [ { key: 'add', - label: '批量编辑', + label: selectTableType === 'checkbox' ? '取消编辑' : '批量编辑', + onClick: () => handleAll(selectTableType), + }, + { + key: 'tags', + label: '标签管理', type: 'primary', - - onClick: handleAll, + onClick: handleTags, }, ]; - const onFetch = async (_params: API.getProductCategoryCategoryListParams) => { - // const data = await getCategoryList({ - // ...params, - // }); - const data: any = [ - { sample_name: 111, id: 1, remark: 222 }, - { sample_name: 22, id: 2, remark: 33 }, - ]; + const onFetch = async (params: SampleReqVo) => { + const data = await getSamplePage({ + ...params, + }); return { - data: data, + data: data.list, + total: data.total, success: true, }; }; - + const onRefresh = (type?: string) => { + tableRef.current?.reload(); + tableRef.current?.onValuesChange({}, {}); + type && setSelectedRows([]); + }; return ( <>
- + ref={tableRef} columns={baseTenantColumns} request={onFetch} toolbarActions={toolbarActions} headerTitle="样本列表" showIndex={false} - showSelection={true} + enableRowClick={true} + rowSelection={{ + type: selectTableType, + selectedRowKeys: selectedRows.map((item) => item.id) as React.Key[], + onChange: (_, selectedRows) => { + setSelectedRows(selectedRows); + }, + }} /> -
- -
+ {selectedRows.length > 0 && ( +
+ + type={selectTableType} + data={selectedRows} + onRefresh={onRefresh} + /> +
+ )}
+ setModalVisible(false)} + editable={true} + request={{ + groupsApi: { + get: getSampleTagGroup, + create: createSampleTagGroup, + delete: deleteSampleTagGroup, + update: updateSampleTagGroup, + }, + tagsApi: { + get: getSampleTagPage, + create: createSampleTag, + delete: deleteSampleTag, + update: updateSampleTag, + }, + }} + title="管理技术标签" + width={800} + height={500} + /> ); }; diff --git a/src/requestErrorConfig.ts b/src/requestErrorConfig.ts index 1b1dc75..9286a97 100644 --- a/src/requestErrorConfig.ts +++ b/src/requestErrorConfig.ts @@ -1,25 +1,19 @@ import type { RequestOptions } from '@@/plugin-request/request'; import type { RequestConfig } from '@umijs/max'; import { request } from '@umijs/max'; -import { message, notification } from 'antd'; -import { deleteUserCache } from '@/hooks/web/useCache'; -import { - getAccessToken, - getRefreshToken, - getTenantId, - setToken, -} from './utils/auth'; +import { message } from 'antd'; +import { getAccessToken, getRefreshToken, setToken } from './utils/auth'; -const tenantEnable = process.env.VITE_APP_TENANT_ENABLE; +// const tenantEnable = process.env.VITE_APP_TENANT_ENABLE; // const { result_code, base_url, request_timeout } = config; // 错误处理方案: 错误类型 -enum ErrorShowType { - SILENT = 0, - WARN_MESSAGE = 1, - ERROR_MESSAGE = 2, - NOTIFICATION = 3, - REDIRECT = 9, -} +// enum ErrorShowType { +// SILENT = 0, +// WARN_MESSAGE = 1, +// ERROR_MESSAGE = 2, +// NOTIFICATION = 3, +// REDIRECT = 9, +// } // 与后端约定的响应数据格式 interface ResponseStructure { success: boolean; @@ -27,27 +21,28 @@ interface ResponseStructure { code?: number; msg?: string; } -const ignoreMsgs = [ - '无效的刷新令牌', // 刷新令牌被删除时,不用提示 - '刷新令牌已过期', // 使用刷新令牌,刷新获取新的访问令牌时,结果因为过期失败,此时需要忽略。否则,会导致继续 401,无法跳转到登出界面 -]; -import { EventEmitter } from 'events'; +// const ignoreMsgs = [ +// "无效的刷新令牌", // 刷新令牌被删除时,不用提示 +// "刷新令牌已过期", // 使用刷新令牌,刷新获取新的访问令牌时,结果因为过期失败,此时需要忽略。否则,会导致继续 401,无法跳转到登出界面 +// ]; + +import { EventEmitter } from 'node:events'; export const requestEventBus = new EventEmitter(); -const errorCode: { [key: string]: string } = { - '400': '请求参数不正确', - '401': '账号未登录', - '403': '当前操作没有权限', - '404': '访问资源不存在', - '405': '请求方法不正确', - '423': '请求失败,请稍后重试', - '429': '请求失败,请稍后重试', - '500': '系统异常', - '501': '功能未实现/未开启', - '502': '错误的配置项', - '900': '重复请求,请稍后重试', - default: '系统未知错误,请反馈给管理员', -}; +// const errorCode: { [key: string]: string } = { +// "400": "请求参数不正确", +// "401": "账号未登录", +// "403": "当前操作没有权限", +// "404": "访问资源不存在", +// "405": "请求方法不正确", +// "423": "请求失败,请稍后重试", +// "429": "请求失败,请稍后重试", +// "500": "系统异常", +// "501": "功能未实现/未开启", +// "502": "错误的配置项", +// "900": "重复请求,请稍后重试", +// default: "系统未知错误,请反馈给管理员", +// }; /** * @name 错误处理 @@ -113,7 +108,7 @@ export const errorConfig: RequestConfig = { const errorInfo: ResponseStructure | undefined = error.info; if (error.name === 'BizError') { if (errorInfo) { - const { msg, code } = errorInfo; + const { msg } = errorInfo; message.error(msg); } } else if (error.response) { @@ -123,7 +118,7 @@ export const errorConfig: RequestConfig = { } else if (error.request) { message.error('None response! Please retry.'); } else { - message.error('发送请求时出了点问题:' + error.msg); + message.error(`发送请求时出了点问题:${error.msg}`); } }, }, @@ -165,6 +160,9 @@ export const errorConfig: RequestConfig = { // // 如果是忽略的错误码,直接返回 msg 异常 // return Promise.reject(msg); // } + if (!config.url) { + throw new Error('请求URL不能为空'); + } // 发送请求时出了点问题 if (code === 401) { if (!isRefreshToken) { @@ -181,21 +179,25 @@ export const errorConfig: RequestConfig = { setToken(refreshTokenRes); // 发出 token 刷新事件 requestEventBus.emit('token-refreshed'); - } catch (e) { + } catch (_) { // 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。 // 提示是否要登出。即不回放当前请求!不然会形成递归 return handleAuthorized(); } finally { isRefreshToken = false; } - return request(config.url!, config); + return request(config.url, config); } else { console.log('刷新令牌失败'); //添加到队列,等待刷新获取到新的令牌 return new Promise((resolve) => { requestList.push(() => { - config.headers!.Authorization = 'Bearer ' + getAccessToken(); // 让每个请求携带自定义token 请根据实际情况自行修改 - resolve(request(config.url!, config)); + if (!config.url) { + throw new Error('请求URL不能为空'); + } + if (config.headers) + config.headers.Authorization = `Bearer ${getAccessToken()}`; // 让每个请求携带自定义token 请根据实际情况自行修改 + resolve(request(config.url, config)); }); }); } diff --git a/src/services/ai/sample/index.ts b/src/services/ai/sample/index.ts index b0e88d3..0fbffc9 100644 --- a/src/services/ai/sample/index.ts +++ b/src/services/ai/sample/index.ts @@ -94,7 +94,7 @@ export interface SampleReqVo extends PageParam { // 获得样本库分页 export const getSamplePage = async (params: SampleReqVo) => { - return request("/ai/sample/get", { + return request("/ai/sample/page", { method: "GET", params, }); @@ -106,30 +106,6 @@ export const getSample = async (id: number) => { }); }; -// 查询部门列表 -// export const getDeptPage = async (params: DeptReq): Promise => { -// return await request.get({ url: "/system/dept/list", params }); -// }; - -export const getDeptPage = (params: SampleReqVo) => { - return request("/system/dept/list", { - method: "GET", - params, - }); -}; - -// 查询部门详情 -// export const getDept = async (id: number) => { -// return await request.get({ url: "/system/dept/get?id=" + id }); -// }; - -export const getDept = (id: number) => { - return request("/system/dept/get", { - method: "GET", - params: { id }, - }); -}; - // 创建样本库 export const createSample = (formData: FormData) => { return request("/ai/sample/create", { @@ -148,14 +124,123 @@ export const updateSample = (params: SampleReqVo) => { }); }; -// 删除部门 -// export const deleteDept = async (id: number) => { -// return await request.delete({ url: "/system/dept/delete?id=" + id }); -// }; - -export const deleteSample = (id: number) => { +export const deleteSample = (id: string) => { return request("/ai/sample/delete", { method: "DELETE", params: { id }, }); }; + +//获得样本标签分组库列表 +export const getSampleTagGroup = async () => { + return request("/ai/sampleTagGroup/list", { + method: "GET", + }); +}; + +//创建样本标签分组库 +export const createSampleTagGroup = (data: { groupName: string }) => { + return request("/ai/sampleTagGroup/create", { + method: "POST", + data, + }); +}; + +//添加关联标签 + +export const relateSample = (data: { + tagId: number[]; + sampleIds: number[]; +}) => { + return request("/ai/sample/relate", { + method: "PUT", + data, + }); +}; + +// 删除管理标签 +export const deleteSampleTagRelate = (data: { + tagId: number[]; + sampleIds: number[]; +}) => { + return request("/ai/sample/deleteRelate", { + method: "DELETE", + data, + }); +}; + +//更新样本库 +export const updateSamples = ( + params: { id: number; remark?: string; sampleName?: string }[] +) => { + return request("/ai/sample/updates", { + method: "PUT", + data: params, + }); +}; + +// 删除样本标签分组库 + +export const deleteSampleTagGroup = async (id?: number) => { + return request("/ai/sampleTagGroup/delete", { + method: "DELETE", + params: { id }, + }); +}; + +//更新样本标签分组库 + +export const updateSampleTagGroup = async (params: { + id: number; + groupName: string; +}) => { + return request("/ai/sampleTagGroup/update", { + method: "PUT", + data: params, + }); +}; + +//获得样本标签库分页 + +export const getSampleTagPage = async (params: { + groupId?: number; + pageNo?: number; + pageSize?: number; +}) => { + return request("/ai/sampleTag/page", { + method: "GET", + params, + }); +}; + +//创建样本标签库 + +export const createSampleTag = (data: { + groupIds: number[]; + tagName: string; +}) => { + return request("/ai/sampleTag/create", { + method: "POST", + data, + }); +}; + +// 删除样本标签库 +export const deleteSampleTag = async (id?: number) => { + return request("/ai/sampleTag/delete", { + method: "DELETE", + params: { id }, + }); +}; + +//更新样本标签库 + +export const updateSampleTag = async (params: { + groupIds: number[]; + tagName: string; +}) => { + return request("/ai/sampleTag/update", { + method: "PUT", + data: params, + }); +};