mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 09:17:23 +00:00
refactor(mobile): Improve mobile component usability (#5590)
* refactor: mobile component * refactor: mobile datepicker * refactor: datepicker * refactor: selectpicker * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: style improve * fix: bug * refactor: locale improve * fix: locale * fix: bug * chore: datetime picker * fix: test * fix: test * refactor: make test stable * refactor: make test stable * fix: bug * fix: bug * fix: bug * refactor: locale improve * fix: bug
This commit is contained in:
parent
73438d505c
commit
fd6b63d24f
@ -2,9 +2,7 @@
|
||||
"version": "1.4.0-alpha.9",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"npmClientArgs": [
|
||||
"--ignore-engines"
|
||||
],
|
||||
"npmClientArgs": ["--ignore-engines"],
|
||||
"command": {
|
||||
"version": {
|
||||
"forcePublish": true,
|
||||
|
@ -106,13 +106,13 @@ test.describe('association fields', () => {
|
||||
await expect(page.getByLabel('Root')).toBeVisible();
|
||||
|
||||
await page.getByTestId('select-object-multiple').click();
|
||||
await page.getByRole('option', { name: 'Member' }).click();
|
||||
await page.getByTitle('Member').locator('div').click();
|
||||
// 再次点击,关闭下拉框。
|
||||
await page.getByTestId('select-object-multiple').click();
|
||||
|
||||
await expect(page.getByLabel('Admin')).toBeVisible();
|
||||
await expect(page.getByTestId('select-object-multiple').getByLabel('Admin')).toBeVisible();
|
||||
await expect(page.getByLabel('Member')).toBeHidden();
|
||||
await expect(page.getByLabel('Root')).toBeVisible();
|
||||
await expect(page.getByTestId('select-object-multiple').getByLabel('Root')).toBeVisible();
|
||||
|
||||
await page.getByLabel('schema-initializer-Grid-form:configureFields-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Nickname' }).click();
|
||||
@ -120,8 +120,8 @@ test.describe('association fields', () => {
|
||||
await page.mouse.move(200, 0);
|
||||
|
||||
await page.waitForTimeout(200);
|
||||
await expect(page.getByLabel('Admin')).toBeVisible();
|
||||
await expect(page.getByTestId('select-object-multiple').getByLabel('Admin')).toBeVisible();
|
||||
await expect(page.getByLabel('Member')).toBeHidden();
|
||||
await expect(page.getByLabel('Root')).toBeVisible();
|
||||
await expect(page.getByTestId('select-object-multiple').getByLabel('Root')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -21,6 +21,7 @@ test.describe('hide column', () => {
|
||||
.hover();
|
||||
await page.getByRole('menuitem', { name: 'Hide column question-circle' }).click();
|
||||
await page.mouse.move(500, 0);
|
||||
await page.getByLabel('block-item-CardItem-users-form').click();
|
||||
|
||||
// 2. Sub table: hide column
|
||||
await page.getByRole('button', { name: 'Role name' }).hover();
|
||||
|
@ -8,3 +8,4 @@
|
||||
*/
|
||||
|
||||
export * from './DatePicker';
|
||||
export * from './util';
|
||||
|
@ -77,7 +77,7 @@ export const moment2str = (value?: Dayjs | null, options: Moment2strOptions = {}
|
||||
const handleChangeOnFilter = (value, picker, showTime) => {
|
||||
const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : getPickerFormat(picker);
|
||||
if (value) {
|
||||
return value.format(format);
|
||||
return dayjs(value).format(format);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
@ -96,7 +96,7 @@ test.describe('form item & create form', () => {
|
||||
.getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-oneToOneBelongsTo')
|
||||
.getByTestId(/select-object/)
|
||||
.click();
|
||||
await page.getByRole('option', { name: String(records[0].id), exact: true }).click();
|
||||
await page.getByTitle(String(records[0].id), { exact: true }).click();
|
||||
},
|
||||
expectReadonly: async () => {
|
||||
await expect(
|
||||
@ -165,7 +165,7 @@ test.describe('form item & create form', () => {
|
||||
.getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-oneToOneBelongsTo')
|
||||
.getByTestId('select-object-single')
|
||||
.click();
|
||||
await expect(page.getByRole('option', { name: String(records[0].id), exact: true })).toBeVisible();
|
||||
await expect(page.getByTitle(String(records[0].id), { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('option')).toHaveCount(1);
|
||||
});
|
||||
|
||||
|
@ -155,7 +155,7 @@ test.describe('form item & edit form', () => {
|
||||
.getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-oneToOneBelongsTo')
|
||||
.getByTestId(/select-object/)
|
||||
.click();
|
||||
await expect(page.getByRole('option', { name: String(recordsOfUser[0].id), exact: true })).toBeVisible();
|
||||
await expect(page.getByTitle(String(recordsOfUser[0].id), { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('option')).toHaveCount(2);
|
||||
});
|
||||
|
||||
|
@ -332,7 +332,7 @@ export class CollectionSettings {
|
||||
|
||||
private async ['File storage'](value: string) {
|
||||
await this.page.getByLabel('block-item-RemoteSelect-collections-File storage').getByTestId('select-single').click();
|
||||
await this.page.getByRole('option', { name: value }).click();
|
||||
await this.page.getByTitle(value).click();
|
||||
}
|
||||
|
||||
private async ['Collection display name'](value: string) {
|
||||
|
@ -32,7 +32,7 @@ export const MobileTabBar: FC<MobileTabBarProps> & {
|
||||
Page: typeof MobileTabBarPage;
|
||||
Link: typeof MobileTabBarLink;
|
||||
} = ({ enableTabBar = true }) => {
|
||||
const { styles } = useStyles();
|
||||
const { styles } = useStyles() as any;
|
||||
const { designable } = useDesignable();
|
||||
const { routeList, activeTabBarItem, resource, refresh } = useMobileRoutes();
|
||||
const validRouteList = routeList.filter((item) => item.schemaUid || isInnerLink(item.options?.url));
|
||||
|
@ -7,9 +7,25 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { RemoteSchemaComponent } from '@nocobase/client';
|
||||
import { RemoteSchemaComponent, AssociatedFields } from '@nocobase/client';
|
||||
import React, { useCallback } from 'react';
|
||||
import { Outlet, useParams } from 'react-router-dom';
|
||||
import { Button as MobileButton, Form as MobileForm, List as MobileList, Dialog as MobileDialog } from 'antd-mobile';
|
||||
import { MobilePicker } from './components/MobilePicker';
|
||||
import { MobileDateTimePicker } from './components/MobileDatePicker';
|
||||
|
||||
const mobileComponents = {
|
||||
Button: MobileButton,
|
||||
Select: MobilePicker,
|
||||
DatePicker: MobileDateTimePicker,
|
||||
UnixTimestamp: MobileDateTimePicker,
|
||||
Modal: MobileDialog,
|
||||
AssociatedFields: (
|
||||
<div contentEditable="false">
|
||||
<AssociatedFields />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const MobilePage = () => {
|
||||
const { pageSchemaUid } = useParams<{ pageSchemaUid: string }>();
|
||||
@ -37,6 +53,7 @@ export const MobilePage = () => {
|
||||
NotFoundPage={'MobileNotFoundPage'}
|
||||
memoized={false}
|
||||
onPageNotFind={onPageNotFind}
|
||||
components={mobileComponents}
|
||||
/>
|
||||
{/* 用于渲染子页面 */}
|
||||
<Outlet />
|
||||
|
@ -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 };
|
@ -24,5 +24,6 @@
|
||||
"Settings": "Settings",
|
||||
"Mobile menu": "Mobile menu",
|
||||
"No accessible pages found": "No accessible pages found",
|
||||
"This might be due to permission configuration issues": "This might be due to permission configuration issues"
|
||||
"This might be due to permission configuration issues": "This might be due to permission configuration issues",
|
||||
"Select time":"Select time"
|
||||
}
|
||||
|
@ -22,5 +22,6 @@
|
||||
"Desktop data blocks": "デスクトップデータブロック",
|
||||
"Other desktop blocks": "他のデスクトップブロック",
|
||||
"Settings": "設定",
|
||||
"Fill": "塗りつぶし"
|
||||
"Fill": "塗りつぶし",
|
||||
"Select time":"時間の選択"
|
||||
}
|
@ -25,5 +25,9 @@
|
||||
"Settings": "设置",
|
||||
"Mobile menu": "移动端菜单",
|
||||
"No accessible pages found": "没有找到你可以访问的页面",
|
||||
"This might be due to permission configuration issues": "这可能是权限配置的问题"
|
||||
"This might be due to permission configuration issues": "这可能是权限配置的问题",
|
||||
"Select time": "选择时间",
|
||||
"Clear": "清空",
|
||||
"Confirm": "确认",
|
||||
"Cancel": "取消"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user