diff --git a/packages/base/index.html b/packages/base/index.html index 439a2eb8b..48e4129ab 100644 --- a/packages/base/index.html +++ b/packages/base/index.html @@ -4,6 +4,15 @@ + + + + + + <%= title %> diff --git a/packages/base/src/App.tsx b/packages/base/src/App.tsx index 3578b121e..884b14864 100644 --- a/packages/base/src/App.tsx +++ b/packages/base/src/App.tsx @@ -27,7 +27,8 @@ import { ConfigProvider, Spin, theme as antdTheme } from 'antd'; import { ThemeData } from './theme'; import { StyleProvider, - legacyLogicalPropertiesTransformer + legacyLogicalPropertiesTransformer, + px2remTransformer } from '@ant-design/cssinjs'; import { useRequest } from 'ahooks'; import BasicInfo from '@actiontech/shared/lib/api/base/service/BasicInfo'; @@ -53,6 +54,7 @@ import sharedEmitterKey from '@actiontech/dms-kit/es/data/EmitterKey'; import useRecentlySelectedZone from '@actiontech/dms-kit/es/features/useRecentlySelectedZone'; import { debounce } from 'lodash'; import ErrorBoundary from './page/ErrorBoundary'; +import { useMedia } from '@actiontech/shared'; import './index.less'; dayjs.extend(updateLocale); dayjs.updateLocale('zh-cn', { @@ -95,7 +97,13 @@ export const Wrapper: React.FC<{ }, [initRenderApp, location.pathname, location.search, navigate, token]); return <>{!initRenderApp && children}; }; + +const px2rem = px2remTransformer({ + rootValue: 16 // 16px = 1rem; +}); + function App() { + const { isMobile } = useMedia(); const { isAfterLoggingIn } = useSelector((state: IReduxState) => ({ isAfterLoggingIn: !state.user.isLoggingIn && !!state.user.token })); @@ -239,7 +247,11 @@ function App() { = (props) => { useBrowserVersionTips(); - + const { isMobile } = useMedia(); return ( {/* #if [ee] */} - + + + {/* #else */} {/* #endif */} diff --git a/packages/dms-kit/docs/CHANGELOG.md b/packages/dms-kit/docs/CHANGELOG.md index 9486de44f..e56bc8a4f 100644 --- a/packages/dms-kit/docs/CHANGELOG.md +++ b/packages/dms-kit/docs/CHANGELOG.md @@ -32,4 +32,8 @@ nav: - ConfigItem组件descNode类型由string改为React.ReactNode - CustomAvatar中Tooltip title改动,用于适配用户名的空格展示 - CustomSelect样式变更,用于适配用户名的空格展示 -- BasicSelect样式变更,用于适配用户名的空格展示 \ No newline at end of file +- BasicSelect样式变更,用于适配用户名的空格展示 + +## 1.0.4 + +- ActiontechTable新增prop:enableOnlyRenderMoreButtons \ No newline at end of file diff --git a/packages/dms-kit/package.json b/packages/dms-kit/package.json index 4736ab03b..3754a3697 100644 --- a/packages/dms-kit/package.json +++ b/packages/dms-kit/package.json @@ -1,6 +1,6 @@ { "name": "@actiontech/dms-kit", - "version": "1.0.3", + "version": "1.0.4", "description": "DMS Kit - React UI Components Library", "main": "lib/index.js", "module": "es/index.js", diff --git a/packages/dms-kit/src/components/ActiontechTable/README.md b/packages/dms-kit/src/components/ActiontechTable/README.md index a1d69c499..04e93af34 100644 --- a/packages/dms-kit/src/components/ActiontechTable/README.md +++ b/packages/dms-kit/src/components/ActiontechTable/README.md @@ -45,6 +45,10 @@ group: +### 仅渲染更多按钮(enableOnlyRenderMoreButtons) + + + ## API ActiontechTable 继承 Ant Design Table 的所有属性,完整 API 请参考 [Table 文档](https://ant.design/components/table-cn)。 @@ -58,6 +62,7 @@ ActiontechTable 继承 Ant Design Table 的所有属性,完整 API 请参考 [ | actions | 操作列配置,自动生成操作列 | `ActiontechTableActionsConfig` \| `ActiontechTableActionMeta[]` | - | - | | setting | 列设置配置,用户的列选择会存储到 localStorage | `ColumnsSettingProps` \| `false` | - | - | | isPaginationFixed | 分页器是否固定在页面底部 | `boolean` | `true` | - | +| enableOnlyRenderMoreButtons | 是否只渲染更多按钮。默认为 `false`:当 `actions.buttons` 为空时,会从 `moreButtons` 中自动“外露”最多 2 个按钮到表格内联按钮;设置为 `true` 时不会外露,全部操作都放入更多按钮中(适配窄屏/移动端)。 | `boolean` | `false` | - | | errorMessage | 错误消息,用于请求失败时显示 | `string` | - | - | ### 常用 Table 属性 diff --git a/packages/dms-kit/src/components/ActiontechTable/Table.tsx b/packages/dms-kit/src/components/ActiontechTable/Table.tsx index 5c6a918be..85f7dccdd 100644 --- a/packages/dms-kit/src/components/ActiontechTable/Table.tsx +++ b/packages/dms-kit/src/components/ActiontechTable/Table.tsx @@ -21,6 +21,7 @@ const ActiontechTable = < isPaginationFixed = true, toolbar, filterContainerProps, + enableOnlyRenderMoreButtons = false, ...props }: ActiontechTableProps) => { const { t } = useTranslation(); @@ -40,12 +41,20 @@ const ActiontechTable = < >(tableName, username); const mergerColumns = useMemo(() => { - const operatorColumn = renderActionInTable(props.actions); + const operatorColumn = renderActionInTable( + props.actions, + enableOnlyRenderMoreButtons + ); return ( operatorColumn ? [...columns, operatorColumn] : columns ) as ActiontechTableColumn; - }, [columns, props.actions, renderActionInTable]); + }, [ + columns, + props.actions, + renderActionInTable, + enableOnlyRenderMoreButtons + ]); const innerColumns = useMemo(() => { if (!tableSetting) { diff --git a/packages/dms-kit/src/components/ActiontechTable/demo/enableOnlyRenderMoreButtons.tsx b/packages/dms-kit/src/components/ActiontechTable/demo/enableOnlyRenderMoreButtons.tsx new file mode 100644 index 000000000..7320d75ca --- /dev/null +++ b/packages/dms-kit/src/components/ActiontechTable/demo/enableOnlyRenderMoreButtons.tsx @@ -0,0 +1,113 @@ +import React, { useMemo, useState } from 'react'; +import { ActiontechTable, ConfigProvider } from '@actiontech/dms-kit'; +import type { ActiontechTableColumn } from '@actiontech/dms-kit'; +import { Space, Switch, Tag, Typography, message } from 'antd'; +import { + EditOutlined, + DeleteOutlined, + EyeOutlined, + MoreOutlined +} from '@ant-design/icons'; + +interface RecordItem { + id: number; + name: string; + status: 'enabled' | 'disabled'; +} + +const mockData: RecordItem[] = [ + { id: 1, name: '示例数据 A', status: 'enabled' }, + { id: 2, name: '示例数据 B', status: 'disabled' }, + { id: 3, name: '示例数据 C', status: 'enabled' } +]; + +const EnableOnlyRenderMoreButtonsDemo: React.FC = () => { + const [enableOnlyRenderMoreButtons, setEnableOnlyRenderMoreButtons] = + useState(false); + + const columns: ActiontechTableColumn< + RecordItem, + Record + > = useMemo( + () => [ + { title: 'ID', dataIndex: 'id', width: 80 }, + { title: '名称', dataIndex: 'name', width: 200 }, + { + title: '状态', + dataIndex: 'status', + width: 120, + render: (status) => ( + + {status === 'enabled' ? '启用' : '禁用'} + + ) + } + ], + [] + ); + + return ( + + + + enableOnlyRenderMoreButtons + + + {enableOnlyRenderMoreButtons + ? '所有操作都在“更多”里(不自动外露按钮)' + : '当 buttons 为空时,会从 moreButtons 自动外露最多 2 个按钮'} + + + + > + rowKey="id" + dataSource={mockData} + columns={columns} + pagination={false} + enableOnlyRenderMoreButtons={enableOnlyRenderMoreButtons} + actions={{ + title: '操作', + width: 220, + fixed: 'right', + // 关键点:buttons 为空数组 + buttons: [], + moreButtons: (record) => [ + { + key: 'view', + text: '查看', + icon: , + onClick: () => message.info(`查看:${record.name}`) + }, + { + key: 'edit', + text: '编辑', + icon: , + onClick: () => message.info(`编辑:${record.name}`) + }, + { + key: 'more', + text: '更多信息', + icon: , + onClick: () => message.info(`更多信息:${record.name}`) + }, + { + key: 'delete', + text: '删除', + icon: , + confirm: () => ({ + title: `确定删除 "${record.name}" 吗?`, + onConfirm: () => message.success(`已删除:${record.name}`) + }) + } + ] + }} + /> + + + ); +}; + +export default EnableOnlyRenderMoreButtonsDemo; diff --git a/packages/dms-kit/src/components/ActiontechTable/hooks/useTableAction.tsx b/packages/dms-kit/src/components/ActiontechTable/hooks/useTableAction.tsx index 64450164d..ae526ba40 100644 --- a/packages/dms-kit/src/components/ActiontechTable/hooks/useTableAction.tsx +++ b/packages/dms-kit/src/components/ActiontechTable/hooks/useTableAction.tsx @@ -89,7 +89,8 @@ const useTableAction = () => { const renderActionInTable = useCallback( >( - actions: ActiontechTableProps['actions'] + actions: ActiontechTableProps['actions'], + enableOnlyRenderMoreButtons = false ): | ActiontechTableColumn< T, @@ -151,7 +152,11 @@ const useTableAction = () => { checkButtonPermissions(button.permissions, record) ); } - if (visibleButtons.length === 0 && visibleMoreButtons.length > 0) { + if ( + visibleButtons.length === 0 && + visibleMoreButtons.length > 0 && + !enableOnlyRenderMoreButtons + ) { //todo 文档记录. 当 visibleButtons 为 0 时,从 moreButtons 中 move 两个 button 到外层。 const maxIndex = Math.min(visibleMoreButtons.length, 2); diff --git a/packages/dms-kit/src/components/ActiontechTable/index.type.ts b/packages/dms-kit/src/components/ActiontechTable/index.type.ts index 8caf77730..a5e2f0363 100644 --- a/packages/dms-kit/src/components/ActiontechTable/index.type.ts +++ b/packages/dms-kit/src/components/ActiontechTable/index.type.ts @@ -419,4 +419,10 @@ export interface ActiontechTableProps< * 控制表格的分页器是否固定于页面底部,默认为true,固定在页面底部,注意:只有在表格有pagination时,设置isPaginationFixed才有意义 */ isPaginationFixed?: boolean; + /** + * 是否只渲染更多按钮,默认为false,如果action.buttons为空数组,会从moreButtons中移动两个按钮到buttons中 + * 如果设置为true,则不会移动 + * 为了适配移动端窄屏幕的场景,将全部操作都放入moreButtons中 + */ + enableOnlyRenderMoreButtons?: boolean; } diff --git a/packages/dms-kit/src/styleWrapper/nav.ts b/packages/dms-kit/src/styleWrapper/nav.ts index 817a9d0bc..bc4fb3af6 100644 --- a/packages/dms-kit/src/styleWrapper/nav.ts +++ b/packages/dms-kit/src/styleWrapper/nav.ts @@ -390,6 +390,12 @@ export const LayoutStyleWrapper = styled('section')` display: flex; flex-direction: column; + /* 移动端适配 */ + @media (max-width: 768px) { + width: 100% !important; + min-width: 100% !important; + } + .copyright-information { color: ${({ theme }) => theme.sharedTheme.uiToken.colorTextTertiary}; font-size: 13px; diff --git a/packages/shared/lib/hooks/index.ts b/packages/shared/lib/hooks/index.ts index 717b48b11..1528c9b21 100644 --- a/packages/shared/lib/hooks/index.ts +++ b/packages/shared/lib/hooks/index.ts @@ -1,3 +1,5 @@ export { default as usePrompt } from './usePrompt'; export { default as useBack } from './useBack'; export { default as useNotificationContext } from './useNotificationContext'; +export { default as useMedia } from './useMedia'; +export { default as useViewport } from './useViewport'; diff --git a/packages/shared/lib/hooks/useMedia/index.ts b/packages/shared/lib/hooks/useMedia/index.ts new file mode 100644 index 000000000..b18d00f55 --- /dev/null +++ b/packages/shared/lib/hooks/useMedia/index.ts @@ -0,0 +1,11 @@ +import { useMediaQuery, useTheme } from '@mui/material'; + +const useMedia = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); // 768px + const isSmallMobile = useMediaQuery(theme.breakpoints.down('sm')); // 480px + + return { isMobile, isSmallMobile }; +}; + +export default useMedia; diff --git a/packages/shared/lib/hooks/useViewport/index.ts b/packages/shared/lib/hooks/useViewport/index.ts new file mode 100644 index 000000000..cd7c4e2b3 --- /dev/null +++ b/packages/shared/lib/hooks/useViewport/index.ts @@ -0,0 +1,78 @@ +import { useEffect } from 'react'; + +export interface ViewportConfig { + width?: string; + initialScale?: number; + maximumScale?: number; + minimumScale?: number; + userScalable?: boolean; + viewportFit?: 'auto' | 'contain' | 'cover'; +} + +const defaultMobileConfig: ViewportConfig = { + width: 'device-width', + initialScale: 1.0, + maximumScale: 1.0, + minimumScale: 1.0, + userScalable: false, + viewportFit: 'cover' +}; + +const defaultDesktopConfig: ViewportConfig = { + width: 'device-width', + initialScale: 1.0, + userScalable: true +}; + +/** + * 动态设置viewport meta标签 + * @param isMobile - 是否为移动端 + * @param customConfig - 自定义配置,会覆盖默认配置 + */ +export const useViewport = ( + isMobile?: boolean, + customConfig?: ViewportConfig +) => { + useEffect(() => { + const config = + customConfig || (isMobile ? defaultMobileConfig : defaultDesktopConfig); + + let viewport = document.querySelector('meta[name="viewport"]'); + + if (!viewport) { + viewport = document.createElement('meta'); + viewport.setAttribute('name', 'viewport'); + document.head.appendChild(viewport); + } + + const contentParts: string[] = []; + + if (config.width) { + contentParts.push(`width=${config.width}`); + } + + if (config.initialScale !== undefined) { + contentParts.push(`initial-scale=${config.initialScale}`); + } + + if (config.maximumScale !== undefined) { + contentParts.push(`maximum-scale=${config.maximumScale}`); + } + + if (config.minimumScale !== undefined) { + contentParts.push(`minimum-scale=${config.minimumScale}`); + } + + if (config.userScalable !== undefined) { + contentParts.push(`user-scalable=${config.userScalable ? 'yes' : 'no'}`); + } + + if (config.viewportFit) { + contentParts.push(`viewport-fit=${config.viewportFit}`); + } + + viewport.setAttribute('content', contentParts.join(', ')); + }, [isMobile, customConfig]); +}; + +export default useViewport; diff --git a/packages/sqle/src/components/ReportDrawer/index.tsx b/packages/sqle/src/components/ReportDrawer/index.tsx index 1e39322f7..acefbe8bb 100644 --- a/packages/sqle/src/components/ReportDrawer/index.tsx +++ b/packages/sqle/src/components/ReportDrawer/index.tsx @@ -9,7 +9,8 @@ import { import { SQLRenderer, BasicTypographyEllipsis, - parse2ReactRouterPath + parse2ReactRouterPath, + useMedia } from '@actiontech/shared'; import { DetailReportDrawerProps, IAuditResultItem } from './index.type'; import { @@ -34,6 +35,7 @@ const ReportDrawer = ({ extra }: DetailReportDrawerProps) => { const { t } = useTranslation(); + const { isMobile } = useMedia(); const { sqleTheme } = useThemeStyleData(); const closeModal = () => { onClose(); @@ -69,6 +71,7 @@ const ReportDrawer = ({ onClose={closeModal} noBodyPadding size="large" + width={isMobile ? '23rem' : undefined} maskClosable extra={extra} > diff --git a/packages/sqle/src/components/SqlRewrittenDrawer/index.tsx b/packages/sqle/src/components/SqlRewrittenDrawer/index.tsx index b58872110..6d15d5b12 100644 --- a/packages/sqle/src/components/SqlRewrittenDrawer/index.tsx +++ b/packages/sqle/src/components/SqlRewrittenDrawer/index.tsx @@ -5,12 +5,14 @@ import { import { useTranslation } from 'react-i18next'; import SqlRewrittenDrawerEE from './index.ee'; import SqlRewrittenDrawerCE from './index.ce'; +import { useMedia } from '@actiontech/shared'; const SqlRewrittenDrawer: React.FC = (props) => { const { t } = useTranslation(); + const { isMobile } = useMedia(); const commonProps: SqlRewrittenDrawerWithBaseProps = { ...props, - width: 920, + width: isMobile ? '23rem' : 920, title: t('sqlRewrite.rewriteDetails'), maskClosable: false }; diff --git a/packages/sqle/src/components/SqlRewrittenDrawer/index.type.ts b/packages/sqle/src/components/SqlRewrittenDrawer/index.type.ts index 8da893b14..4a476f823 100644 --- a/packages/sqle/src/components/SqlRewrittenDrawer/index.type.ts +++ b/packages/sqle/src/components/SqlRewrittenDrawer/index.type.ts @@ -12,7 +12,7 @@ export type SqlRewrittenDrawerProps = { interface SqlRewrittenDrawerBaseProps { maskClosable: boolean; - width: number; + width: number | string; title: string; } diff --git a/packages/sqle/src/locale/zh-CN/execWorkflow.ts b/packages/sqle/src/locale/zh-CN/execWorkflow.ts index 336bce2c8..d37dc6aa2 100644 --- a/packages/sqle/src/locale/zh-CN/execWorkflow.ts +++ b/packages/sqle/src/locale/zh-CN/execWorkflow.ts @@ -159,6 +159,7 @@ export default { operator: { buttonText: '工单详情', title: '工单信息', + moreActions: '更多操作', baseInfo: { title: '基本信息', createUser: '创建人', diff --git a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultFilterContainer/index.tsx b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultFilterContainer/index.tsx index c8fff7b6e..26e880617 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultFilterContainer/index.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultFilterContainer/index.tsx @@ -9,12 +9,14 @@ import classNames from 'classnames'; import { floatToPercent } from '@actiontech/dms-kit'; import { floatRound } from '@actiontech/dms-kit'; import { useTranslation } from 'react-i18next'; +import { useMedia } from '@actiontech/shared'; const AuditResultFilterContainer = < T extends CustomSegmentedFilterBaseValue = string >( props: AuditResultFilterContainerProps ) => { const { t } = useTranslation(); + const { isMobile } = useMedia(); const { score, passRate, @@ -34,7 +36,13 @@ const AuditResultFilterContainer = < }, [auditLevel]); return ( theme.sqleTheme.execWorkflow.common.auditResultFilter.bgColor}; + &.audit-result-filter-container-mobile-wrapper { + padding: 1rem; + flex-direction: column; + + .custom-segmented-filter-wrapper { + flex-wrap: wrap; + align-items: center; + justify-content: center; + + .custom-segmented-filter-item { + padding: 0.5rem; + margin-bottom: 0.5rem; + margin-right: 0.5rem; + } + } + } + &.audit-result-filter-container-borderless { border-bottom: 0; } diff --git a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/actions.tsx b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/actions.tsx index eeb6a16fe..e760ea3c8 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/actions.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/actions.tsx @@ -9,8 +9,43 @@ import { export const AuditResultForCreateWorkflowActions = ( clickAnalyze: (sqlNum?: number) => void, onCreateWhitelist: (record?: IAuditTaskSQLResV2) => void, - handleClickSqlRewritten: (record: IAuditTaskSQLResV2) => void + handleClickSqlRewritten: (record: IAuditTaskSQLResV2) => void, + isMobile: boolean ): ActiontechTableActionsWithPermissions => { + if (isMobile) { + return { + buttons: [], + moreButtons: [ + { + key: 'jumpAnalyze', + text: t('execWorkflow.audit.table.analyze'), + onClick: (record) => { + clickAnalyze(record?.number); + } + }, + // #if [ee] + { + key: 'create-exception', + text: t('execWorkflow.audit.table.createWhitelist'), + onClick: (record) => { + onCreateWhitelist(record); + }, + permissions: + PERMISSIONS.ACTIONS.SQLE.SQL_EXEC_WORKFLOW.CREATE_WHITE_LIST + }, + // #endif + { + key: 'sqlRewriter', + text: t('sqlRewrite.actionName'), + onClick: (record) => { + handleClickSqlRewritten(record!); + }, + icon: + } + ] + }; + } + return [ { key: 'jumpAnalyze', diff --git a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.tsx b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.tsx index a57c976e7..eca1420d6 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.tsx @@ -23,6 +23,8 @@ import EventEmitter from '../../../../../utils/EventEmitter'; import EmitterKey from '../../../../../data/EmitterKey'; import SqlRewrittenDrawer from '../../../../../components/SqlRewrittenDrawer/index'; import useSqlRewrittenDrawerState from '../../../../../components/SqlRewrittenDrawer/hooks/useSqlRewrittenDrawerState'; +import { useMedia } from '@actiontech/shared'; + const AuditResultTable: React.FC = ({ noDuplicate, taskID, @@ -36,6 +38,7 @@ const AuditResultTable: React.FC = ({ supportedBackupPolicies, updateTaskAuditRuleExceptionStatus }) => { + const { isMobile } = useMedia(); const { sqlRewrittenOpen, handleCloseSqlRewrittenDrawer, @@ -167,14 +170,16 @@ const AuditResultTable: React.FC = ({ AuditResultForCreateWorkflowActions( handleClickAnalyze, onCreateWhitelist, - handleClickSqlRewritten + handleClickSqlRewritten, + isMobile ) ); }, [ parse2TableActionPermissions, handleClickAnalyze, onCreateWhitelist, - handleClickSqlRewritten + handleClickSqlRewritten, + isMobile ]); const onSwitchSqlBackupPolicy = useCallback( (sqlId?: number) => { @@ -230,6 +235,7 @@ const AuditResultTable: React.FC = ({ current: pagination.page_index ?? 1 }} actions={actions} + enableOnlyRenderMoreButtons={isMobile} /> = ({ tasks, updateTaskRecordCount, @@ -29,6 +32,7 @@ const AuditResultList: React.FC = ({ }) => { const { t } = useTranslation(); const { projectID } = useCurrentProject(); + const { isMobile } = useMedia(); const { noDuplicate, setNoDuplicate, @@ -66,8 +70,15 @@ const AuditResultList: React.FC = ({ } }, [tasks]); return ( - - + + {showTaskTab ? ( = ({ className, status, gap = 12, - sqlVersion + sqlVersion, + isMobile }) => { const { t } = useTranslation(); const { projectID } = useCurrentProject(); @@ -23,6 +24,7 @@ const BasicInfoWrapper: React.FC = ({ className={classNames(className)} gap={gap} status={status} + isMobile={isMobile} >
diff --git a/packages/sqle/src/page/SqlExecWorkflow/Common/BasicInfoWrapper/index.type.ts b/packages/sqle/src/page/SqlExecWorkflow/Common/BasicInfoWrapper/index.type.ts index 4508bacf8..d9e124ad9 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Common/BasicInfoWrapper/index.type.ts +++ b/packages/sqle/src/page/SqlExecWorkflow/Common/BasicInfoWrapper/index.type.ts @@ -8,4 +8,5 @@ export type BasicInfoWrapperProps = { className?: string; gap?: number; sqlVersion?: ISqlVersion; + isMobile?: boolean; }; diff --git a/packages/sqle/src/page/SqlExecWorkflow/Common/BasicInfoWrapper/style.ts b/packages/sqle/src/page/SqlExecWorkflow/Common/BasicInfoWrapper/style.ts index 35a0ee53e..bd1349667 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Common/BasicInfoWrapper/style.ts +++ b/packages/sqle/src/page/SqlExecWorkflow/Common/BasicInfoWrapper/style.ts @@ -4,9 +4,10 @@ import { styled } from '@mui/material/styles'; export const BasicInfoStyleWrapper = styled('div')<{ gap: number; status?: WorkflowRecordResV2StatusEnum; + isMobile?: boolean; }>` display: flex; - padding: 24px 40px; + padding: ${({ isMobile }) => (isMobile ? '1rem' : '24px 40px')}; flex-direction: column; align-items: flex-start; align-self: stretch; diff --git a/packages/sqle/src/page/SqlExecWorkflow/Common/SqlStatementFormController/SqlStatementFormItem/index.tsx b/packages/sqle/src/page/SqlExecWorkflow/Common/SqlStatementFormController/SqlStatementFormItem/index.tsx index 3f88f5634..8fe31ec49 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Common/SqlStatementFormController/SqlStatementFormItem/index.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Common/SqlStatementFormController/SqlStatementFormItem/index.tsx @@ -53,7 +53,7 @@ const SqlStatementFormItem: React.FC = ({ {t('execWorkflow.create.form.sqlInfo.uploadType')} } - className="form-item-label-mb-16" + className="form-item-label-mb-16 sql-statement-form-item-label" /> ['actions']; + columns: ActiontechTableColumn; + onCardClick: (record: IGetWorkflowTasksItemV2) => void; +} + +type TaskItemValue = IGetWorkflowTasksItemV2[keyof IGetWorkflowTasksItemV2]; + +const defaultCellRender = (value: TaskItemValue): React.ReactNode => { + if (value === null || value === undefined || value === '') { + return '-'; + } + if (Array.isArray(value)) { + if (value.length === 0) { + return '-'; + } + const allPrimitive = value.every( + (v) => typeof v === 'string' || typeof v === 'number' + ); + return allPrimitive ? value.join(', ') : `${value.length}`; + } + return value as React.ReactNode; +}; + +const MobileCardList: React.FC = ({ + dataSource, + loading, + actions, + columns, + onCardClick +}) => { + const { t } = useTranslation(); + + const renderActions = useMemo(() => { + return (record: IGetWorkflowTasksItemV2) => { + // ActiontechTableActionsWithPermissions 也可能是数组类型;这里移动端只处理 object-config 的 buttons + if (!actions || Array.isArray(actions) || !actions.buttons) { + return null; + } + + const actionButtons = actions.buttons + .filter((btn) => { + if (btn.permissions && !btn.permissions(record)) { + return false; + } + + // hidden 检查 + const buttonPropsResult = + typeof btn.buttonProps === 'function' + ? btn.buttonProps(record) + : btn.buttonProps || {}; + + return !buttonPropsResult.hidden; + }) + .map((btn) => { + const buttonPropsResult = + typeof btn.buttonProps === 'function' + ? btn.buttonProps(record) + : btn.buttonProps || {}; + + const confirmResult = + typeof btn.confirm === 'function' + ? btn.confirm(record) + : btn.confirm; + + // 有 confirm 配置时使用 Popconfirm 包裹 + if (confirmResult) { + return ( + { + e.stopPropagation(); + }} + > + + + {btn.text} + + + + ); + } + + // 无 confirm 配置时直接渲染按钮 + return ( + { + e.stopPropagation(); + (buttonPropsResult.onClick as any)?.(e); + }} + type={buttonPropsResult.type} + > + {btn.text} + + ); + }); + + return actionButtons.length > 0 ? actionButtons : null; + }; + }, [actions]); + + return ( + + + }} + renderItem={(record, index) => { + const actionButtons = renderActions(record); + + return ( + + { + onCardClick(record); + }} + > + {/* 标题:实例名 - 可点击跳转 */} +
+ {record.instance_name} +
+ + {/* 详情信息 */} + + {columns.map((col, colIndex) => { + const label = + typeof col.title === 'function' + ? col.title(undefined as any) + : col.title; + const dataIndex = col.dataIndex as + | keyof IGetWorkflowTasksItemV2 + | undefined; + const rawValue = dataIndex + ? record?.[dataIndex] + : undefined; + const content = col.render + ? (col.render as any)(rawValue, record, index ?? colIndex) + : defaultCellRender(rawValue); + + const key = String( + col.key ?? col.dataIndex ?? `col-${colIndex}` + ); + + return ( + + {content} + + ); + })} + + + {/* 操作按钮区域 */} + {actionButtons && ( + { + e.stopPropagation(); + }} + > + {actionButtons} + + )} +
+
+ ); + }} + /> +
+ ); +}; + +export default MobileCardList; diff --git a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/OverviewList/ScheduleTimeModal/index.tsx b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/OverviewList/ScheduleTimeModal/index.tsx index 2b30d2dc0..c761de5e7 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/OverviewList/ScheduleTimeModal/index.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/OverviewList/ScheduleTimeModal/index.tsx @@ -237,6 +237,7 @@ const ScheduleTimeModal: React.FC = ({ showTime={{ defaultValue: createDefaultRangeTime() }} + inputReadOnly /> diff --git a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/OverviewList/index.tsx b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/OverviewList/index.tsx index 0a49816f2..d77d6f64b 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/OverviewList/index.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/OverviewList/index.tsx @@ -16,6 +16,10 @@ import { import { ModalName } from '../../../../../../data/ModalName'; import { useDispatch } from 'react-redux'; import { IGetWorkflowTasksItemV2 } from '@actiontech/shared/lib/api/sqle/service/common'; +import MobileCardList from './MobileCardList'; +import { useCallback, useMemo } from 'react'; +import { useMedia } from '@actiontech/shared'; +import { EmptyBox } from '@actiontech/dms-kit'; const WorkflowOverviewList: React.FC = ({ workflowInfo, @@ -26,6 +30,7 @@ const WorkflowOverviewList: React.FC = ({ overviewList, overviewTableErrorMessage }) => { + const { isMobile } = useMedia(); const { username } = useCurrentUser(); const { projectName } = useCurrentProject(); const { parse2TableActionPermissions } = usePermission(); @@ -46,19 +51,51 @@ const WorkflowOverviewList: React.FC = ({ refreshOverview: refreshOverviewAction }); - const onRetryExecute = (record: IGetWorkflowTasksItemV2) => { - dispatch( - updateRetryExecuteData({ - taskId: record.task_id?.toString() ?? '' - }) - ); - dispatch( - updateSqlExecWorkflowModalStatus({ - modalName: ModalName.Sql_Exec_Workflow_Retry_Execute_Modal, - status: true - }) - ); - }; + const onRetryExecute = useCallback( + (record: IGetWorkflowTasksItemV2) => { + dispatch( + updateRetryExecuteData({ + taskId: record.task_id?.toString() ?? '' + }) + ); + dispatch( + updateSqlExecWorkflowModalStatus({ + modalName: ModalName.Sql_Exec_Workflow_Retry_Execute_Modal, + status: true + }) + ); + }, + [dispatch] + ); + + const actions = useMemo( + () => + parse2TableActionPermissions( + AuditResultOverviewListAction({ + scheduleTimeHandle, + sqlExecuteHandle, + sqlTerminateHandle, + openScheduleModalAndSetCurrentTask, + currentUsername: username, + workflowStatus: workflowInfo?.record?.status, + executable: !!workflowInfo?.record?.executable, + onRetryExecute, + workflowInfoRecord: workflowInfo?.record + }) + ), + [ + scheduleTimeHandle, + sqlExecuteHandle, + sqlTerminateHandle, + openScheduleModalAndSetCurrentTask, + username, + workflowInfo?.record, + onRetryExecute, + parse2TableActionPermissions + ] + ); + + const columns = useMemo(() => auditResultOverviewColumn(), []); return ( <> @@ -69,35 +106,37 @@ const WorkflowOverviewList: React.FC = ({ submit={scheduleTimeHandle} maintenanceTime={currentTask?.instance_maintenance_times ?? []} /> - { - return { - onClick: () => { - activeTabChangeEvent(`${record.task_id}`); - } - }; - }} - /> + + { + return { + onClick: () => { + activeTabChangeEvent(`${record.task_id}`); + } + }; + }} + /> + } + > + activeTabChangeEvent(`${record.task_id}`)} + /> + ); }; diff --git a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/OverviewList/style.ts b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/OverviewList/style.ts new file mode 100644 index 000000000..6513acf86 --- /dev/null +++ b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/OverviewList/style.ts @@ -0,0 +1,54 @@ +import { styled } from '@mui/material'; + +export const MobileCardListStyleWrapper = styled('div')` + padding: 1rem; + + .overview-list-item { + padding: 0 !important; + margin-bottom: 0.75rem; + } + + .overview-card { + width: 100%; + cursor: pointer; + } + + .overview-card .ant-card-body { + padding: 0.75rem !important; + } + + .overview-card-title { + font-weight: 600; + font-size: 0.875rem; + margin-bottom: 0.5rem; + color: ${({ theme }) => theme.sharedTheme.uiToken.colorPrimary}; + user-select: none; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .overview-card-desc .ant-descriptions-item-label { + font-size: 0.75rem; + width: 40%; + padding-right: 0.5rem; + } + + .overview-card-desc .ant-descriptions-item-content { + font-size: 0.75rem; + } + + .overview-card-actions { + margin-top: 0.75rem; + width: 100%; + } + + .ant-descriptions-item-label { + color: ${({ theme }) => theme.sharedTheme.uiToken.colorTextSecondary}; + font-weight: 500; + } + + .ant-descriptions-item-content { + color: ${({ theme }) => theme.sharedTheme.uiToken.colorText}; + } +`; diff --git a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/RetryExecuteModal/index.tsx b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/RetryExecuteModal/index.tsx index 179e7b293..f90430ef9 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/RetryExecuteModal/index.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/RetryExecuteModal/index.tsx @@ -28,10 +28,12 @@ import { ModalName } from '../../../../../../data/ModalName'; import { updateSqlExecWorkflowModalStatus } from '../../../../../../store/sqlExecWorkflow/index'; import EmitterKey from '../../../../../../data/EmitterKey'; import EventEmitter from '../../../../../../utils/EventEmitter'; +import { useMedia } from '@actiontech/shared'; const RetryExecuteModal = () => { const { t } = useTranslation(); const dispatch = useDispatch(); + const { isMobile } = useMedia(); const urlParams = useTypedParams(); @@ -170,7 +172,7 @@ const RetryExecuteModal = () => { open={visible} size="large" title={ - + {t('execWorkflow.detail.overview.table.selectRetryExecuteSql')} {t('execWorkflow.detail.overview.table.selectRetryExecuteSqlDesc')} @@ -225,6 +227,7 @@ const RetryExecuteModal = () => { dataIndex: 'exec_result', className: 'ellipsis-column-width', title: () => t('execWorkflow.audit.table.execResult'), + width: 400, render: (result) => { return result ? ( @@ -260,6 +263,7 @@ const RetryExecuteModal = () => { setAllSelectedKeys(newAllSelectedKeys); }, columnWidth: 60, + fixed: 'left', getCheckboxProps: (record) => { return { disabled: diff --git a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/TaskResultList/Common/ResultCard/FileMode.tsx b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/TaskResultList/Common/ResultCard/FileMode.tsx index 5ee00f416..316b03de0 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/TaskResultList/Common/ResultCard/FileMode.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/TaskResultList/Common/ResultCard/FileMode.tsx @@ -15,6 +15,8 @@ import { TasksResultCardStyleWrapper } from './style'; import { SqlFileOutlined } from '@actiontech/icons'; import { TypedLink, useTypedParams } from '@actiontech/shared'; import { ROUTE_PATHS } from '@actiontech/dms-kit'; +import { useMedia } from '@actiontech/shared'; +import classNames from 'classnames'; const FileMode: React.FC = ({ taskId, @@ -22,6 +24,7 @@ const FileMode: React.FC = ({ enableRetryExecute, ...props }) => { + const { isMobile } = useMedia(); const { workflowId } = useTypedParams(); const auditResult = useMemo(() => { @@ -170,7 +173,9 @@ const FileMode: React.FC = ({ ]); return ( - + { if (keys.length > 0) { diff --git a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/TaskResultList/Common/ResultCard/SqlMode.tsx b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/TaskResultList/Common/ResultCard/SqlMode.tsx index c1fb8e50a..f0706e69f 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/TaskResultList/Common/ResultCard/SqlMode.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/TaskResultList/Common/ResultCard/SqlMode.tsx @@ -44,6 +44,8 @@ import { PermissionControl, PERMISSIONS } from '@actiontech/shared/lib/features'; +import { useMedia } from '@actiontech/shared'; +import classNames from 'classnames'; const SqlMode: React.FC = ({ projectID, @@ -54,6 +56,7 @@ const SqlMode: React.FC = ({ ...props }) => { const { t } = useTranslation(); + const { isMobile } = useMedia(); const [messageApi, contextHolder] = message.useMessage(); const { sqleTheme } = useThemeStyleData(); const dispatch = useDispatch(); @@ -158,7 +161,9 @@ const SqlMode: React.FC = ({ : ''; }, [props.taskStatus, props.backup_result, t]); return ( - + {contextHolder}
@@ -176,7 +181,7 @@ const SqlMode: React.FC = ({ />
- + {/* #if [ee] */} = ({ } ]} segmentedRowExtraContent={ - + theme.sharedTheme.uiToken.colorBgBase}; margin-bottom: 20px; + &.mobile-task-result-card { + .result-card-header { + padding: 0.4rem 0.8rem; + flex-wrap: wrap; + height: auto; + + .result-card-status-wrap { + margin-left: 0.2rem; + + .result-card-status-divider { + margin: 0 0.2rem; + } + + .ant-tag { + padding: 0 0.2rem; + margin-right: 0; + } + } + + .task-result-card-button-wrap { + margin-top: 0.2rem; + } + + .ant-space { + gap: 0.5rem !important; + } + } + + .result-card-content { + padding: 0.3rem 0.6rem; + + .task-result-card-source-file-wrap { + margin-top: 0.5rem; + } + } + + .file-result-collapse-wrapper { + .ant-collapse-expand-icon { + padding-right: 0 !important; + } + } + } + & .result-card-header { height: 60px; padding: 16px 20px; diff --git a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/TaskResultList/PaginationList/index.tsx b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/TaskResultList/PaginationList/index.tsx index c0ea97ab2..e247330cf 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/TaskResultList/PaginationList/index.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/AuditExecResultPanel/TaskResultList/PaginationList/index.tsx @@ -5,18 +5,21 @@ import { WorkflowResV2ExecModeEnum } from '@actiontech/shared/lib/api/sqle/servi import FileExecuteMode from './FileExecuteMode'; import ExecModeController from '../Common/ExecModeController'; import { TasksResultListStyleWrapper } from '../style'; +import { useMedia } from '@actiontech/shared'; const PaginationList: React.FC = ({ executeMode, ...props }) => { + const { isMobile } = useMedia(); return ( { const { t } = useTranslation(); + const { isMobile } = useMedia(); + useViewport(isMobile); const dispatch = useDispatch(); const { taskId, fileId, workflowId } = useTypedParams< @@ -173,7 +178,11 @@ const SqlFileStatementOverview: React.FC = () => { }, [refresh]); return ( - + @@ -193,7 +202,10 @@ const SqlFileStatementOverview: React.FC = () => { } /> - + = ({ executeMode, ...props }) => { + const { isMobile } = useMedia(); return ( = ({ activeTabKey, taskInfos, + isMobile, ...resetProps }) => { const { t } = useTranslation(); @@ -117,10 +119,16 @@ const AuditExecResultPanel: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [resetProps.refreshWorkflow, resetProps.refreshOverviewAction]); return ( - +
{t('audit.result')}
- + { if (value === WORKFLOW_OVERVIEW_TAB_KEY) { @@ -140,22 +148,30 @@ const AuditExecResultPanel: React.FC = ({ ]} /> -