mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 03:02:19 +08:00
Merge branch 'next' into develop
This commit is contained in:
commit
92e87ee1da
@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { APIClient as APIClientSDK, getSubAppName } from '@nocobase/sdk';
|
||||
import { APIClient as APIClientSDK } from '@nocobase/sdk';
|
||||
import { Result } from 'ahooks/es/useRequest/src/types';
|
||||
import { notification } from 'antd';
|
||||
import React from 'react';
|
||||
@ -91,10 +91,10 @@ export class APIClient extends APIClientSDK {
|
||||
interceptors() {
|
||||
this.axios.interceptors.request.use((config) => {
|
||||
config.headers['X-With-ACL-Meta'] = true;
|
||||
const appName = this.app ? getSubAppName(this.app.getPublicPath()) : null;
|
||||
if (appName) {
|
||||
config.headers['X-App'] = appName;
|
||||
}
|
||||
const headers = this.getHeaders();
|
||||
Object.keys(headers).forEach((key) => {
|
||||
config.headers[key] = headers[key];
|
||||
});
|
||||
return config;
|
||||
});
|
||||
super.interceptors();
|
||||
|
@ -83,7 +83,16 @@ export const FormItem: any = withDynamicSchemaProps(
|
||||
|
||||
return (
|
||||
<CollectionFieldProvider allowNull={true}>
|
||||
<BlockItem className={'nb-form-item'}>
|
||||
<BlockItem
|
||||
className={cx(
|
||||
'nb-form-item',
|
||||
css`
|
||||
.ant-formily-item-layout-horizontal .ant-formily-item-control {
|
||||
max-width: ${showTitle === false ? '100% !important' : null};
|
||||
}
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<ACLCollectionFieldProvider>
|
||||
<Item className={className} {...props} extra={extra} wrapperStyle={wrapperStyle} />
|
||||
</ACLCollectionFieldProvider>
|
||||
|
@ -723,6 +723,8 @@ const InternalNocoBaseTable = React.memo(
|
||||
field,
|
||||
...others
|
||||
} = props;
|
||||
const { token } = useToken();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
@ -747,10 +749,10 @@ const InternalNocoBaseTable = React.memo(
|
||||
padding: 16px 8px;
|
||||
}
|
||||
.ant-table-middle .ant-table-cell {
|
||||
padding: 12px 8px;
|
||||
padding: 12px ${token.paddingXS}px;
|
||||
}
|
||||
.ant-table-small .ant-table-cell {
|
||||
padding: 8px 8px;
|
||||
padding: 8px ${token.paddingXS}px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,8 +71,8 @@ interface Resource {
|
||||
}
|
||||
|
||||
interface ExtendedAgent extends SuperAgentTest {
|
||||
login: (user: any) => ExtendedAgent;
|
||||
loginUsingId: (userId: number) => ExtendedAgent;
|
||||
login: (user: any, roleName?: string) => ExtendedAgent;
|
||||
loginUsingId: (userId: number, roleName?: string) => ExtendedAgent;
|
||||
resource: (name: string, resourceOf?: any) => Resource;
|
||||
}
|
||||
|
||||
@ -124,13 +124,14 @@ export class MockServer extends Application {
|
||||
const proxy = new Proxy(agent, {
|
||||
get(target, method: string, receiver) {
|
||||
if (['login', 'loginUsingId'].includes(method)) {
|
||||
return (userOrId: any) => {
|
||||
return (userOrId: any, roleName?: string) => {
|
||||
return proxy
|
||||
.auth(
|
||||
jwt.sign(
|
||||
{
|
||||
userId: typeof userOrId === 'number' ? userOrId : userOrId?.id,
|
||||
temp: true,
|
||||
roleName,
|
||||
},
|
||||
process.env.APP_KEY,
|
||||
{
|
||||
|
@ -92,6 +92,7 @@ const InternalIcons = () => {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
.ant-list-item-meta-title button {
|
||||
font-size: 14px;
|
||||
|
@ -47,6 +47,10 @@ export const useStyles = genStyleHook('nb-mobile', (token) => {
|
||||
'.nb-action-panel': {
|
||||
paddingTop: '10px',
|
||||
},
|
||||
'.ant-list-item': {
|
||||
paddingTop: '8px',
|
||||
paddingBottom: '8px',
|
||||
},
|
||||
'.nb-action-panel .ant-avatar-circle': {
|
||||
width: '48px !important',
|
||||
height: '48px !important',
|
||||
|
@ -22,6 +22,9 @@ import {
|
||||
useRequest,
|
||||
ACLCustomContext,
|
||||
VariablesProvider,
|
||||
AssociationField,
|
||||
Action,
|
||||
DatePicker,
|
||||
} from '@nocobase/client';
|
||||
import { css } from '@emotion/css';
|
||||
import { isDesktop } from 'react-device-detect';
|
||||
@ -31,6 +34,10 @@ import React, { createContext, useContext, useEffect, useMemo, useState } from '
|
||||
import { useParams } from 'react-router';
|
||||
import { usePublicSubmitActionProps } from '../hooks';
|
||||
import { UnEnabledFormPlaceholder, UnFoundFormPlaceholder } from './UnEnabledFormPlaceholder';
|
||||
|
||||
import { Button as MobileButton, Dialog as MobileDialog } from 'antd-mobile';
|
||||
import { MobilePicker } from './components/MobilePicker';
|
||||
import { MobileDateTimePicker } from './components/MobileDatePicker';
|
||||
class PublicDataSource extends DataSource {
|
||||
async getDataSource() {
|
||||
return {};
|
||||
@ -110,10 +117,43 @@ const PublicFormMessageProvider = ({ children }) => {
|
||||
</PublicFormMessageContext.Provider>
|
||||
);
|
||||
};
|
||||
function isMobile() {
|
||||
return window.matchMedia('(max-width: 768px)').matches;
|
||||
}
|
||||
|
||||
const AssociationFieldMobile = (props) => {
|
||||
return <AssociationField {...props} popupMatchSelectWidth={true} />;
|
||||
};
|
||||
|
||||
AssociationFieldMobile.SubTable = AssociationField.SubTable;
|
||||
AssociationFieldMobile.Nester = AssociationField.Nester;
|
||||
AssociationFieldMobile.AddNewer = Action.Container;
|
||||
AssociationFieldMobile.Selector = Action.Container;
|
||||
AssociationFieldMobile.Viewer = Action.Container;
|
||||
AssociationFieldMobile.InternalSelect = AssociationField.InternalSelect;
|
||||
AssociationFieldMobile.ReadPretty = AssociationField.ReadPretty;
|
||||
AssociationFieldMobile.FileSelector = AssociationField.FileSelector;
|
||||
|
||||
const DatePickerMobile = (props) => {
|
||||
return <MobileDateTimePicker {...props} />;
|
||||
};
|
||||
DatePickerMobile.FilterWithPicker = DatePicker.FilterWithPicker;
|
||||
DatePickerMobile.RangePicker = DatePicker.RangePicker;
|
||||
|
||||
const mobileComponents = {
|
||||
Button: MobileButton,
|
||||
Select: (props) => {
|
||||
return <MobilePicker {...props} />;
|
||||
},
|
||||
DatePicker: DatePickerMobile,
|
||||
UnixTimestamp: MobileDateTimePicker,
|
||||
Modal: MobileDialog,
|
||||
AssociationField: AssociationFieldMobile,
|
||||
};
|
||||
function InternalPublicForm() {
|
||||
const params = useParams();
|
||||
const apiClient = useAPIClient();
|
||||
const isMobileMedia = isMobile();
|
||||
const { error, data, loading, run } = useRequest<any>(
|
||||
{
|
||||
url: `publicForms:getMeta/${params.name}`,
|
||||
@ -189,6 +229,7 @@ function InternalPublicForm() {
|
||||
if (!data?.data) {
|
||||
return <UnEnabledFormPlaceholder />;
|
||||
}
|
||||
const components = isMobileMedia ? mobileComponents : {};
|
||||
return (
|
||||
<ACLCustomContext.Provider value={{ allowAll: true }}>
|
||||
<PublicAPIClientProvider>
|
||||
@ -217,7 +258,7 @@ function InternalPublicForm() {
|
||||
scope={{
|
||||
useCreateActionProps: usePublicSubmitActionProps,
|
||||
}}
|
||||
components={{ PublicFormMessageProvider: PublicFormMessageProvider }}
|
||||
components={{ PublicFormMessageProvider: PublicFormMessageProvider, ...components }}
|
||||
/>
|
||||
</SchemaComponentContext.Provider>
|
||||
</VariablesProvider>
|
||||
|
@ -0,0 +1,91 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { DatePicker } from 'antd-mobile';
|
||||
import { mapDatePicker, DatePicker as NBDatePicker } from '@nocobase/client';
|
||||
import { connect, mapProps, mapReadPretty } from '@formily/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const MobileDateTimePicker = connect(
|
||||
(props) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
dateFormat = 'YYYY-MM-DD',
|
||||
timeFormat = 'HH:mm',
|
||||
showTime = false,
|
||||
picker,
|
||||
...rest
|
||||
} = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
// 性能优化:使用 useCallback 缓存函数
|
||||
const handleConfirm = useCallback(
|
||||
(value) => {
|
||||
setVisible(false);
|
||||
const selectedDateTime = new Date(value);
|
||||
onChange(selectedDateTime);
|
||||
},
|
||||
[showTime, onChange],
|
||||
);
|
||||
|
||||
// 清空选择的日期和时间
|
||||
const handleClear = useCallback(() => {
|
||||
setVisible(false);
|
||||
onChange(null);
|
||||
}, [onChange]);
|
||||
|
||||
const labelRenderer = useCallback((type: string, data: number) => {
|
||||
switch (type) {
|
||||
case 'year':
|
||||
return data;
|
||||
case 'quarter':
|
||||
return data;
|
||||
default:
|
||||
return data;
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<div contentEditable="false" onClick={() => setVisible(true)}>
|
||||
<NBDatePicker
|
||||
onClick={() => setVisible(true)}
|
||||
value={value}
|
||||
picker={picker}
|
||||
{...rest}
|
||||
popupStyle={{ display: 'none' }}
|
||||
style={{ pointerEvents: 'none', width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
<DatePicker
|
||||
cancelText={t('Cancel')}
|
||||
confirmText={t('Confirm')}
|
||||
visible={visible}
|
||||
title={<a onClick={handleClear}>{t('Clear')}</a>}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
precision={showTime ? 'second' : picker === 'date' ? 'day' : picker}
|
||||
renderLabel={labelRenderer}
|
||||
min={new Date(1000, 0, 1)}
|
||||
max={new Date(9999, 11, 31)}
|
||||
onConfirm={(val) => {
|
||||
handleConfirm(val);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
mapProps(mapDatePicker()),
|
||||
mapReadPretty(NBDatePicker.ReadPretty),
|
||||
);
|
||||
|
||||
export { MobileDateTimePicker };
|
@ -0,0 +1,112 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, CheckList, Popup, SearchBar } from 'antd-mobile';
|
||||
import { connect, mapProps } from '@formily/react';
|
||||
import { Select } from '@nocobase/client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const MobilePicker = connect(
|
||||
(props) => {
|
||||
const { value, onChange, options = [], mode } = props;
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [selected, setSelected] = useState(value || []);
|
||||
const [searchText, setSearchText] = useState(null);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
if (searchText) {
|
||||
return options.filter((item) => item.label.toLowerCase().includes(searchText.toLowerCase()));
|
||||
}
|
||||
return options;
|
||||
}, [options, searchText]);
|
||||
|
||||
const handleConfirm = () => {
|
||||
onChange(selected);
|
||||
setVisible(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
!visible && setSearchText(null);
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div contentEditable="false" onClick={() => setVisible(true)}>
|
||||
<Select
|
||||
placeholder={t('Select')}
|
||||
value={value}
|
||||
dropdownStyle={{ display: 'none' }}
|
||||
multiple={mode === 'multiple'}
|
||||
onClear={() => {
|
||||
setVisible(false);
|
||||
onChange(null);
|
||||
setSelected(null);
|
||||
}}
|
||||
onFocus={(e) => e.preventDefault()}
|
||||
style={{ pointerEvents: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
<Popup
|
||||
visible={visible}
|
||||
onMaskClick={() => {
|
||||
setVisible(false);
|
||||
if (!value || value?.length === 0) {
|
||||
setSelected([]);
|
||||
}
|
||||
}}
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{ margin: '10px' }}>
|
||||
<SearchBar
|
||||
placeholder={t('search')}
|
||||
value={searchText}
|
||||
onChange={(v) => setSearchText(v)}
|
||||
showCancelButton
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: '60vh',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
<CheckList
|
||||
multiple={mode === 'multiple'}
|
||||
value={Array.isArray(selected) ? selected : [selected] || []}
|
||||
onChange={(val) => {
|
||||
if (mode === 'multiple') {
|
||||
setSelected(val);
|
||||
} else {
|
||||
setSelected(val[0]);
|
||||
onChange(val[0]);
|
||||
setVisible(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{filteredItems.map((item) => (
|
||||
<CheckList.Item key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</CheckList.Item>
|
||||
))}
|
||||
</CheckList>
|
||||
</div>
|
||||
{mode === 'multiple' && (
|
||||
<Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
|
||||
{t('Confirm')}
|
||||
</Button>
|
||||
)}
|
||||
</Popup>
|
||||
</>
|
||||
);
|
||||
},
|
||||
mapProps({ dataSource: 'options' }),
|
||||
);
|
||||
|
||||
export { MobilePicker };
|
Loading…
x
Reference in New Issue
Block a user