feat: infinite scroll and linkable card items
Co-authored-by: sealday <zhanglin@daoyoucloud.com> Reviewed-on: daoyoucloud/tachybase#997 Co-authored-by: bai.zixv <bai.zixv@foxmail.com> Co-committed-by: bai.zixv <bai.zixv@foxmail.com>
This commit is contained in:
parent
519bcc703e
commit
f0e51e1e89
@ -30,6 +30,8 @@ export const createGridCardBlockUISchema = (options: {
|
|||||||
},
|
},
|
||||||
runWhenParamsChanged: true,
|
runWhenParamsChanged: true,
|
||||||
rowKey,
|
rowKey,
|
||||||
|
isLinkable: false,
|
||||||
|
needInfiniteScroll: false,
|
||||||
layoutDirection: 'column',
|
layoutDirection: 'column',
|
||||||
},
|
},
|
||||||
'x-component': 'BlockItem',
|
'x-component': 'BlockItem',
|
||||||
|
@ -277,6 +277,54 @@ export const gridCardBlockSettings = new SchemaSettings({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'setLinkable',
|
||||||
|
type: 'switch',
|
||||||
|
useComponentProps() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const field = useField();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
return {
|
||||||
|
title: t('Set Linkable'),
|
||||||
|
checked: fieldSchema['x-decorator-props']?.['isLinkable'] ?? false,
|
||||||
|
onChange: (isLinkable) => {
|
||||||
|
_.set(fieldSchema, 'x-decorator-props.isLinkable', isLinkable);
|
||||||
|
field.decoratorProps.isLinkable = isLinkable;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'setInfiniteScroll',
|
||||||
|
type: 'switch',
|
||||||
|
useComponentProps() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const field = useField();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
return {
|
||||||
|
title: t('Set InfiniteScroll'),
|
||||||
|
checked: fieldSchema['x-decorator-props']?.['needInfiniteScroll'],
|
||||||
|
onChange: (needInfiniteScroll) => {
|
||||||
|
_.set(fieldSchema, 'x-decorator-props.needInfiniteScroll', needInfiniteScroll);
|
||||||
|
field.decoratorProps.needInfiniteScroll = needInfiniteScroll;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'ConvertReferenceToDuplicate',
|
name: 'ConvertReferenceToDuplicate',
|
||||||
Component: SchemaSettingsTemplate,
|
Component: SchemaSettingsTemplate,
|
||||||
|
@ -33,6 +33,7 @@ const InternalGridCardBlockProvider = (props) => {
|
|||||||
service,
|
service,
|
||||||
resource,
|
resource,
|
||||||
columnCount: props.columnCount,
|
columnCount: props.columnCount,
|
||||||
|
needInfiniteScroll: props.needInfiniteScroll,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormContext.Provider value={form}>
|
<FormContext.Provider value={form}>
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
import { useCollection } from '@tachybase/client';
|
||||||
import { ObjectField } from '@tachybase/schema';
|
import { ObjectField } from '@tachybase/schema';
|
||||||
import { useField } from '@tachybase/schema';
|
import { useField, useFieldSchema } from '@tachybase/schema';
|
||||||
import { Card } from 'antd';
|
import { Card } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useCollectionParentRecordData } from '../../../data-source/collection-record/CollectionRecordProvider';
|
import { useCollectionParentRecordData } from '../../../data-source/collection-record/CollectionRecordProvider';
|
||||||
import { RecordProvider } from '../../../record-provider';
|
import { RecordProvider } from '../../../record-provider';
|
||||||
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
|
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useGridCardDetailUrl } from './hooks';
|
||||||
|
|
||||||
const itemCss = css`
|
const itemCss = css`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -17,13 +20,28 @@ const itemCss = css`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const GridCardItem = withDynamicSchemaProps((props) => {
|
export const GridCardItem = withDynamicSchemaProps((props) => {
|
||||||
|
const collection = useCollection();
|
||||||
const field = useField<ObjectField>();
|
const field = useField<ObjectField>();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
const parentRecordData = useCollectionParentRecordData();
|
const parentRecordData = useCollectionParentRecordData();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const detailUrl = useGridCardDetailUrl({ collection, field, fieldSchema });
|
||||||
|
// XXX: 实现的有些丑陋, 需要想想有没有更好的办法
|
||||||
|
const handleClick = () => {
|
||||||
|
// 1. 依赖schema的层级,不合适
|
||||||
|
const parentSchema = fieldSchema?.parent?.parent;
|
||||||
|
const isLinkable = parentSchema?.['x-decorator-props']?.isLinkable;
|
||||||
|
if (isLinkable) {
|
||||||
|
// 2. 固定的链接格式写法,不合适; 需要想个能配置的办法
|
||||||
|
navigate(detailUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
role="button"
|
role="button"
|
||||||
aria-label="grid-card-item"
|
aria-label="grid-card-item"
|
||||||
className={css`
|
className={css`
|
||||||
|
/* background-color: re/d; */
|
||||||
height: 100%;
|
height: 100%;
|
||||||
> .ant-card-body {
|
> .ant-card-body {
|
||||||
padding: 24px 24px 0px;
|
padding: 24px 24px 0px;
|
||||||
@ -33,8 +51,10 @@ export const GridCardItem = withDynamicSchemaProps((props) => {
|
|||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<div className={itemCss}>
|
<div className={itemCss}>
|
||||||
|
{/* @ts-ignore */}
|
||||||
<RecordProvider record={field.value} parent={parentRecordData}>
|
<RecordProvider record={field.value} parent={parentRecordData}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</RecordProvider>
|
</RecordProvider>
|
||||||
|
@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css';
|
|||||||
import { ArrayField } from '@tachybase/schema';
|
import { ArrayField } from '@tachybase/schema';
|
||||||
import { RecursionField, Schema, useField, useFieldSchema } from '@tachybase/schema';
|
import { RecursionField, Schema, useField, useFieldSchema } from '@tachybase/schema';
|
||||||
import { List as AntdList, Col, PaginationProps } from 'antd';
|
import { List as AntdList, Col, PaginationProps } from 'antd';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { SortableItem } from '../../common';
|
import { SortableItem } from '../../common';
|
||||||
import { SchemaComponentOptions } from '../../core';
|
import { SchemaComponentOptions } from '../../core';
|
||||||
import { useDesigner, useProps } from '../../hooks';
|
import { useDesigner, useProps } from '../../hooks';
|
||||||
@ -12,6 +12,7 @@ import { GridCardItem } from './GridCard.Item';
|
|||||||
import { useGridCardActionBarProps } from './hooks';
|
import { useGridCardActionBarProps } from './hooks';
|
||||||
import { defaultColumnCount, pageSizeOptions } from './options';
|
import { defaultColumnCount, pageSizeOptions } from './options';
|
||||||
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
|
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
|
||||||
|
import { InfiniteScroll } from '../../common/infinite-scroll/infinite-scroll';
|
||||||
|
|
||||||
const rowGutter = {
|
const rowGutter = {
|
||||||
md: 12,
|
md: 12,
|
||||||
@ -59,8 +60,7 @@ const designerCss = css`
|
|||||||
const InternalGridCard = (props) => {
|
const InternalGridCard = (props) => {
|
||||||
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
||||||
const { columnCount: columnCountProp, pagination } = useProps(props);
|
const { columnCount: columnCountProp, pagination } = useProps(props);
|
||||||
|
const { service, columnCount: _columnCount = defaultColumnCount, needInfiniteScroll } = useGridCardBlockContext();
|
||||||
const { service, columnCount: _columnCount = defaultColumnCount } = useGridCardBlockContext();
|
|
||||||
const columnCount = columnCountProp || _columnCount;
|
const columnCount = columnCountProp || _columnCount;
|
||||||
const { run, params } = service;
|
const { run, params } = service;
|
||||||
const meta = service?.data?.meta;
|
const meta = service?.data?.meta;
|
||||||
@ -68,6 +68,7 @@ const InternalGridCard = (props) => {
|
|||||||
const field = useField<ArrayField>();
|
const field = useField<ArrayField>();
|
||||||
const Designer = useDesigner();
|
const Designer = useDesigner();
|
||||||
const [schemaMap] = useState(new Map());
|
const [schemaMap] = useState(new Map());
|
||||||
|
|
||||||
const getSchema = useCallback(
|
const getSchema = useCallback(
|
||||||
(key) => {
|
(key) => {
|
||||||
if (!schemaMap.has(key)) {
|
if (!schemaMap.has(key)) {
|
||||||
@ -99,6 +100,31 @@ const InternalGridCard = (props) => {
|
|||||||
[run, params],
|
[run, params],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* 以下为无限滚动逻辑 */
|
||||||
|
// XXX: 需要仔细梳理这里的逻辑, 目前的实现有点效果问题
|
||||||
|
const [data, setData] = useState(field.value || []);
|
||||||
|
const [hasMore, setHasMore] = useState(true);
|
||||||
|
|
||||||
|
const loadMore = async () => {
|
||||||
|
const { count, pageSize = 10, page = 1 } = meta || {};
|
||||||
|
await onPaginationChange(page + 1, pageSize);
|
||||||
|
setHasMore(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { count, pageSize = 10, page = 1 } = meta || {};
|
||||||
|
const hasMore = count > page * pageSize;
|
||||||
|
|
||||||
|
setData((val = []) => {
|
||||||
|
const currentVal = field.value || [];
|
||||||
|
return [...val, ...currentVal];
|
||||||
|
});
|
||||||
|
|
||||||
|
setHasMore(hasMore);
|
||||||
|
}, [meta?.page]);
|
||||||
|
|
||||||
|
/* 以上为无限滚动逻辑 */
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SchemaComponentOptions
|
<SchemaComponentOptions
|
||||||
scope={{
|
scope={{
|
||||||
@ -109,7 +135,7 @@ const InternalGridCard = (props) => {
|
|||||||
<SortableItem className={cx('nb-card-list', designerCss)}>
|
<SortableItem className={cx('nb-card-list', designerCss)}>
|
||||||
<AntdList
|
<AntdList
|
||||||
pagination={
|
pagination={
|
||||||
!meta || meta.count <= meta.pageSize
|
!meta || meta.count <= meta.pageSize || needInfiniteScroll
|
||||||
? false
|
? false
|
||||||
: {
|
: {
|
||||||
...pagination,
|
...pagination,
|
||||||
@ -120,7 +146,7 @@ const InternalGridCard = (props) => {
|
|||||||
pageSizeOptions,
|
pageSizeOptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataSource={field.value}
|
dataSource={needInfiniteScroll ? data : field.value}
|
||||||
grid={{
|
grid={{
|
||||||
...columnCount,
|
...columnCount,
|
||||||
sm: columnCount.xs,
|
sm: columnCount.xs,
|
||||||
@ -142,6 +168,7 @@ const InternalGridCard = (props) => {
|
|||||||
}}
|
}}
|
||||||
loading={service?.loading}
|
loading={service?.loading}
|
||||||
/>
|
/>
|
||||||
|
{needInfiniteScroll && <InfiniteScroll loadMore={loadMore} hasMore={hasMore} />}
|
||||||
<Designer />
|
<Designer />
|
||||||
</SortableItem>
|
</SortableItem>
|
||||||
</SchemaComponentOptions>
|
</SchemaComponentOptions>
|
||||||
|
@ -10,3 +10,13 @@ export const useGridCardActionBarProps = () => {
|
|||||||
spaceProps,
|
spaceProps,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGridCardDetailUrl = ({ collection, field, fieldSchema }) => {
|
||||||
|
const name = 'detail';
|
||||||
|
const { datasource = 'main', name: collectionName } = collection;
|
||||||
|
const { record } = field;
|
||||||
|
const primaryKey = 'id';
|
||||||
|
const primaryKeyValue = record[primaryKey];
|
||||||
|
|
||||||
|
return `/mobile/${name}/${datasource}/${collectionName}/${primaryKeyValue}`;
|
||||||
|
};
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
import React, { useContext } from 'react';
|
|
||||||
import type { FC, ReactNode } from 'react';
|
|
||||||
|
|
||||||
// TODO: 待继续
|
|
||||||
// import { Locale } from '../../locales/base';
|
|
||||||
// import zhCN from '../../locales/zh-CN';
|
|
||||||
type Locale = {};
|
|
||||||
const zhCN = {};
|
|
||||||
|
|
||||||
type Config = {
|
|
||||||
locale: Locale;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultConfigRef: {
|
|
||||||
current: Config;
|
|
||||||
} = {
|
|
||||||
current: {
|
|
||||||
locale: zhCN,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function setDefaultConfig(config: Config) {
|
|
||||||
defaultConfigRef.current = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDefaultConfig() {
|
|
||||||
return defaultConfigRef.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConfigContext = React.createContext<Config | null>(null);
|
|
||||||
|
|
||||||
export type ConfigProviderProps = Config & {
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ConfigProvider: FC<ConfigProviderProps> = (props) => {
|
|
||||||
const { children, ...config } = props;
|
|
||||||
const parentConfig = useConfig();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigContext.Provider
|
|
||||||
value={{
|
|
||||||
...parentConfig,
|
|
||||||
...config,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ConfigContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useConfig() {
|
|
||||||
return useContext(ConfigContext) ?? getDefaultConfig();
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import type { CSSProperties, ReactElement } from 'react';
|
|
||||||
import React, { AriaAttributes } from 'react';
|
|
||||||
|
|
||||||
const canUseDom = !!(
|
|
||||||
typeof window !== 'undefined' &&
|
|
||||||
typeof document !== 'undefined' &&
|
|
||||||
window.document &&
|
|
||||||
window.document.createElement
|
|
||||||
);
|
|
||||||
|
|
||||||
type ScrollElement = HTMLElement | Window;
|
|
||||||
|
|
||||||
const defaultRoot = canUseDom ? window : undefined;
|
|
||||||
|
|
||||||
const overflowStylePatterns = ['scroll', 'auto', 'overlay'];
|
|
||||||
|
|
||||||
function isElement(node: Element) {
|
|
||||||
const ELEMENT_NODE_TYPE = 1;
|
|
||||||
return node.nodeType === ELEMENT_NODE_TYPE;
|
|
||||||
}
|
|
||||||
export function getScrollParent(
|
|
||||||
el: Element,
|
|
||||||
root: ScrollElement | null | undefined = defaultRoot,
|
|
||||||
): Window | Element | null | undefined {
|
|
||||||
let node = el;
|
|
||||||
|
|
||||||
while (node && node !== root && isElement(node)) {
|
|
||||||
if (node === document.body) {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
const { overflowY } = window.getComputedStyle(node);
|
|
||||||
if (overflowStylePatterns.includes(overflowY) && node.scrollHeight > node.clientHeight) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
node = node.parentNode as Element;
|
|
||||||
}
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mergeProps<A, B>(a: A, b: B): B & A;
|
|
||||||
export function mergeProps<A, B, C>(a: A, b: B, c: C): C & B & A;
|
|
||||||
export function mergeProps(...items: any[]) {
|
|
||||||
const ret: any = {};
|
|
||||||
items.forEach((item) => {
|
|
||||||
Object.keys(item).forEach((key) => {
|
|
||||||
if (item[key] !== undefined) {
|
|
||||||
ret[key] = item[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NativeProps<S extends string = never> = {
|
|
||||||
className?: string;
|
|
||||||
style?: CSSProperties & Partial<Record<S, string>>;
|
|
||||||
tabIndex?: number;
|
|
||||||
} & AriaAttributes;
|
|
||||||
|
|
||||||
export function withNativeProps<P extends NativeProps>(props: P, element: ReactElement) {
|
|
||||||
const p = {
|
|
||||||
...element.props,
|
|
||||||
};
|
|
||||||
if (props.className) {
|
|
||||||
p.className = classNames(element.props.className, props.className);
|
|
||||||
}
|
|
||||||
if (props.style) {
|
|
||||||
p.style = {
|
|
||||||
...p.style,
|
|
||||||
...props.style,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (props.tabIndex !== undefined) {
|
|
||||||
p.tabIndex = props.tabIndex;
|
|
||||||
}
|
|
||||||
for (const key in props) {
|
|
||||||
// if (!props.hasOwnProperty(key)) continue;
|
|
||||||
if (!Object.hasOwn(props, key)) continue;
|
|
||||||
if (key.startsWith('data-') || key.startsWith('aria-')) {
|
|
||||||
p[key] = props[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return React.cloneElement(element, p);
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
export const pageSizeOptions = [12, 24, 60, 96, 120];
|
export const pageSizeOptions = [5, 6, 10, 12, 24, 60, 96, 120];
|
||||||
export const gridSizes = ['xs', 'md', 'lg', 'xxl'];
|
export const gridSizes = ['xs', 'md', 'lg', 'xxl'];
|
||||||
export const screenSizeTitleMaps = {
|
export const screenSizeTitleMaps = {
|
||||||
xs: 'Phone device',
|
xs: 'Phone device',
|
||||||
|
@ -1,41 +1,20 @@
|
|||||||
|
import { getScrollParent, merge } from '@tachybase/utils/client';
|
||||||
import { useLockFn, useThrottleFn } from 'ahooks';
|
import { useLockFn, useThrottleFn } from 'ahooks';
|
||||||
import type { FC, ReactNode } from 'react';
|
import { Spin } from 'antd';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { ReactNode, useEffect, useRef, useState } from 'react';
|
||||||
// import { useConfig } from '../config-provider';
|
import { useTranslation } from 'react-i18next';
|
||||||
// import DotLoading from '../dot-loading';
|
import useStyles from './style';
|
||||||
import { NativeProps, getScrollParent, mergeProps, withNativeProps } from './utils';
|
|
||||||
|
|
||||||
// TODO: 待继续
|
|
||||||
const useConfig = () => {
|
|
||||||
return {
|
|
||||||
locale: {
|
|
||||||
InfiniteScroll: {
|
|
||||||
retry: false,
|
|
||||||
noMore: false,
|
|
||||||
failedToLoad: '范德萨发',
|
|
||||||
},
|
|
||||||
common: {
|
|
||||||
loading: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: 待继续
|
|
||||||
const DotLoading = () => null;
|
|
||||||
|
|
||||||
function isWindow(element: any | Window): element is Window {
|
function isWindow(element: any | Window): element is Window {
|
||||||
return element === window;
|
return element === window;
|
||||||
}
|
}
|
||||||
|
|
||||||
const classPrefix = `adm-infinite-scroll`;
|
|
||||||
|
|
||||||
export type InfiniteScrollProps = {
|
export type InfiniteScrollProps = {
|
||||||
loadMore: (isRetry: boolean) => Promise<void>;
|
loadMore: (isRetry: boolean) => Promise<void>;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
children?: ReactNode | ((hasMore: boolean, failed: boolean, retry: () => void) => ReactNode);
|
children?: ReactNode | ((hasMore: boolean, failed: boolean, retry: () => void) => ReactNode);
|
||||||
} & NativeProps;
|
};
|
||||||
|
|
||||||
const defaultProps: Required<Pick<InfiniteScrollProps, 'threshold' | 'children'>> = {
|
const defaultProps: Required<Pick<InfiniteScrollProps, 'threshold' | 'children'>> = {
|
||||||
threshold: 250,
|
threshold: 250,
|
||||||
@ -44,13 +23,17 @@ const defaultProps: Required<Pick<InfiniteScrollProps, 'threshold' | 'children'>
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InfiniteScroll: FC<InfiniteScrollProps> = (p) => {
|
export const InfiniteScroll = ({
|
||||||
const props = mergeProps(defaultProps, p);
|
threshold = defaultProps.threshold,
|
||||||
|
loadMore,
|
||||||
|
children = defaultProps.children,
|
||||||
|
hasMore,
|
||||||
|
}: InfiniteScrollProps) => {
|
||||||
|
const { styles } = useStyles();
|
||||||
const [failed, setFailed] = useState(false);
|
const [failed, setFailed] = useState(false);
|
||||||
const doLoadMore = useLockFn(async (isRetry: boolean) => {
|
const doLoadMore = useLockFn(async (isRetry: boolean) => {
|
||||||
try {
|
try {
|
||||||
await props.loadMore(isRetry);
|
await loadMore(isRetry);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setFailed(true);
|
setFailed(true);
|
||||||
throw e;
|
throw e;
|
||||||
@ -68,7 +51,7 @@ export const InfiniteScroll: FC<InfiniteScrollProps> = (p) => {
|
|||||||
const { run: check } = useThrottleFn(
|
const { run: check } = useThrottleFn(
|
||||||
async () => {
|
async () => {
|
||||||
if (nextFlagRef.current !== flag) return;
|
if (nextFlagRef.current !== flag) return;
|
||||||
if (!props.hasMore) return;
|
if (!hasMore) return;
|
||||||
const element = elementRef.current;
|
const element = elementRef.current;
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
if (!element.offsetParent) return;
|
if (!element.offsetParent) return;
|
||||||
@ -78,14 +61,14 @@ export const InfiniteScroll: FC<InfiniteScrollProps> = (p) => {
|
|||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
const elementTop = rect.top;
|
const elementTop = rect.top;
|
||||||
const current = isWindow(parent) ? window.innerHeight : parent.getBoundingClientRect().bottom;
|
const current = isWindow(parent) ? window.innerHeight : parent.getBoundingClientRect().bottom;
|
||||||
if (current >= elementTop - props.threshold) {
|
if (current >= elementTop - threshold) {
|
||||||
const nextFlag = {};
|
const nextFlag = {};
|
||||||
nextFlagRef.current = nextFlag;
|
nextFlagRef.current = nextFlag;
|
||||||
try {
|
try {
|
||||||
await doLoadMore(false);
|
await doLoadMore(false);
|
||||||
setFlag(nextFlag);
|
setFlag(nextFlag);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('%c Line:88 🍯 e', 'font-size:18px;color:#2eafb0;background:#3f7cff', e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -120,39 +103,35 @@ export const InfiniteScroll: FC<InfiniteScrollProps> = (p) => {
|
|||||||
await doLoadMore(true);
|
await doLoadMore(true);
|
||||||
setFlag(nextFlagRef.current);
|
setFlag(nextFlagRef.current);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('%c Line:121 🥥 e', 'font-size:18px;color:#3f7cff;background:#2eafb0', e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return withNativeProps(
|
return (
|
||||||
props,
|
<div className={styles.infiniteScroll} ref={elementRef}>
|
||||||
<div className={classPrefix} ref={elementRef}>
|
{typeof children === 'function' ? children(hasMore, failed, retry) : children}
|
||||||
{typeof props.children === 'function' ? props.children(props.hasMore, failed, retry) : props.children}
|
</div>
|
||||||
</div>,
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const InfiniteScrollContent: FC<{
|
const InfiniteScrollContent = (props: { hasMore: boolean; failed: boolean; retry: () => void }) => {
|
||||||
hasMore: boolean;
|
const { t } = useTranslation();
|
||||||
failed: boolean;
|
const { styles } = useStyles();
|
||||||
retry: () => void;
|
|
||||||
}> = (props) => {
|
|
||||||
const { locale } = useConfig();
|
|
||||||
|
|
||||||
if (!props.hasMore) {
|
if (!props.hasMore) {
|
||||||
return <span>{locale.InfiniteScroll.noMore}</span>;
|
return <span>{t('no more')}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.failed) {
|
if (props.failed) {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<span className={`${classPrefix}-failed-text`}>{locale.InfiniteScroll.failedToLoad}</span>
|
<span className={`${styles.infiniteScroll}-failed-text`}>{t('failed to load')}</span>
|
||||||
<a
|
<a
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.retry();
|
props.retry();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{locale.InfiniteScroll.retry}
|
{t('retry')}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -160,14 +139,8 @@ const InfiniteScrollContent: FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span>{locale.common.loading}</span>
|
<span>{t('loading')}</span>
|
||||||
<DotLoading />
|
<Spin />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const keepComp = (props) => {
|
|
||||||
return <InfiniteScroll {...props} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
keepComp({});
|
|
@ -0,0 +1,21 @@
|
|||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ token, css }) => {
|
||||||
|
return {
|
||||||
|
infiniteScroll: css`
|
||||||
|
color: ${token.colorTextDescription};
|
||||||
|
padding: 18px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: ${token.fontSize};
|
||||||
|
|
||||||
|
&-failed-text {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export default useStyles;
|
@ -19,3 +19,4 @@ export * from './uid';
|
|||||||
export * from './url';
|
export * from './url';
|
||||||
export { dayjs, lodash };
|
export { dayjs, lodash };
|
||||||
export * from './parseHTML';
|
export * from './parseHTML';
|
||||||
|
export * from './dom';
|
||||||
|
35
packages/core/utils/src/dom.ts
Normal file
35
packages/core/utils/src/dom.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
export const canUseDom = !!(
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
typeof document !== 'undefined' &&
|
||||||
|
window.document &&
|
||||||
|
window.document.createElement
|
||||||
|
);
|
||||||
|
|
||||||
|
type ScrollElement = HTMLElement | Window;
|
||||||
|
|
||||||
|
const defaultRoot = canUseDom ? window : undefined;
|
||||||
|
|
||||||
|
const overflowStylePatterns = ['scroll', 'auto', 'overlay'];
|
||||||
|
|
||||||
|
function isElement(node: Element) {
|
||||||
|
const ELEMENT_NODE_TYPE = 1;
|
||||||
|
return node.nodeType === ELEMENT_NODE_TYPE;
|
||||||
|
}
|
||||||
|
export function getScrollParent(
|
||||||
|
el: Element,
|
||||||
|
root: ScrollElement | null | undefined = defaultRoot,
|
||||||
|
): Window | Element | null | undefined {
|
||||||
|
let node = el;
|
||||||
|
|
||||||
|
while (node && node !== root && isElement(node)) {
|
||||||
|
if (node === document.body) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
const { overflowY } = window.getComputedStyle(node);
|
||||||
|
if (overflowStylePatterns.includes(overflowY) && node.scrollHeight > node.clientHeight) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
node = node.parentNode as Element;
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
@ -12,6 +12,9 @@ export const HomePage: React.FC<{}> = () => {
|
|||||||
'appends[]': 'pictures',
|
'appends[]': 'pictures',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('%c Line:10 🥐 data', 'font-size:18px;color:#4fff4B;background:#e41a6a', data);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return render();
|
return render();
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,12 @@
|
|||||||
"main": "dist/server/index.js",
|
"main": "dist/server/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.3.6",
|
"@ant-design/icons": "^5.3.6",
|
||||||
"antd-mobile": "^5.35.0"
|
"antd-mobile": "^5.35.0",
|
||||||
|
"react-router-dom": "^6.23.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tachybase/client": "0.x",
|
"@tachybase/client": "0.x",
|
||||||
|
"@tachybase/plugin-mobile-client": "0.x",
|
||||||
"@tachybase/server": "0.x",
|
"@tachybase/server": "0.x",
|
||||||
"@tachybase/test": "0.x"
|
"@tachybase/test": "0.x"
|
||||||
},
|
},
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
import { Button } from 'antd-mobile';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export const TestComponent = () => {
|
|
||||||
const handleClick = () => {};
|
|
||||||
return (
|
|
||||||
<Button style={{ alignSelf: 'center', fontSize: '5rem' }} block color="primary" size="large" onClick={handleClick}>
|
|
||||||
Hello
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,21 +1,13 @@
|
|||||||
import { Modal, Toast, Image, Button } from 'antd-mobile';
|
|
||||||
import { ShareAltOutlined } from '@ant-design/icons';
|
import { ShareAltOutlined } from '@ant-design/icons';
|
||||||
|
import { CustomComponentType } from '@hera/plugin-core/client';
|
||||||
|
import { Button, Image, Modal, Toast } from 'antd-mobile';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import downloadImage from '../assets/download.svg';
|
import downloadImage from '../assets/download.svg';
|
||||||
import { CustomComponentType } from '@hera/plugin-core/client';
|
import './style.less';
|
||||||
|
|
||||||
export const ShareProduct = () => {
|
export const ShareProduct = () => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button className={'m-share'} color="primary" size="small" block onClick={showModal}>
|
||||||
style={{
|
|
||||||
alignSelf: 'center',
|
|
||||||
fontSize: 16,
|
|
||||||
}}
|
|
||||||
color="primary"
|
|
||||||
size="small"
|
|
||||||
block
|
|
||||||
onClick={showModal}
|
|
||||||
>
|
|
||||||
分享产品
|
分享产品
|
||||||
<ShareAltOutlined />
|
<ShareAltOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
@ -26,7 +18,8 @@ ShareProduct.displayName = 'ShareProduct';
|
|||||||
ShareProduct.__componentType = CustomComponentType.CUSTOM_FORM_ITEM;
|
ShareProduct.__componentType = CustomComponentType.CUSTOM_FORM_ITEM;
|
||||||
ShareProduct.__componentLabel = '移动端-三聪头-分享产品';
|
ShareProduct.__componentLabel = '移动端-三聪头-分享产品';
|
||||||
|
|
||||||
const showModal = () => {
|
const showModal = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
Modal.show({
|
Modal.show({
|
||||||
bodyStyle: {
|
bodyStyle: {
|
||||||
background: 'transparent',
|
background: 'transparent',
|
@ -0,0 +1,23 @@
|
|||||||
|
import { CustomComponentType } from '@hera/plugin-core/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
import './style.less';
|
||||||
|
import { getPathProductDetail } from '../utils/path';
|
||||||
|
|
||||||
|
// @deprecated
|
||||||
|
export const ShowDetail = () => {
|
||||||
|
// const navigate = useNavigate();
|
||||||
|
// const handleClick = () => {
|
||||||
|
// navigate('/mobile/');
|
||||||
|
// };
|
||||||
|
return (
|
||||||
|
<Link className={'m-detail'} to={getPathProductDetail({ dataSource: 'main', collection: 'cards', id: '15' })}>
|
||||||
|
显示详情
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
Ø;
|
||||||
|
};
|
||||||
|
|
||||||
|
ShowDetail.displayName = 'ShowDetail';
|
||||||
|
ShowDetail.__componentType = CustomComponentType.CUSTOM_FORM_ITEM;
|
||||||
|
ShowDetail.__componentLabel = '移动端-三聪头-显示详情';
|
@ -0,0 +1,11 @@
|
|||||||
|
.m-detail {
|
||||||
|
/* background-color: red; */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-share {
|
||||||
|
align-self: 'center';
|
||||||
|
font-size: 16;
|
||||||
|
}
|
@ -1,10 +1,13 @@
|
|||||||
import { Plugin } from '@tachybase/client';
|
import { Plugin } from '@tachybase/client';
|
||||||
import { ProductDetail } from './pages/ProductDetail';
|
// import { ProductDetail } from './pages/ProductDetail.1';
|
||||||
import { ShareProduct } from './components/ShareProduct';
|
import { ShareProduct } from './custom-components/ShareProduct';
|
||||||
|
import { ShowDetail } from './custom-components/ShowDetail';
|
||||||
|
import { CardDetailSC, SCCardDetail } from './pages/CardDetail.schema';
|
||||||
|
import { getPathProductDetail } from './utils/path';
|
||||||
|
|
||||||
export class PluginSancongtouClient extends Plugin {
|
export class PluginSancongtouClient extends Plugin {
|
||||||
async afterAdd() {
|
async afterAdd() {
|
||||||
// await this.app.pm.add()
|
await this.app.pm.add(SCCardDetail);
|
||||||
}
|
}
|
||||||
|
|
||||||
async beforeLoad() {}
|
async beforeLoad() {}
|
||||||
@ -12,10 +15,18 @@ export class PluginSancongtouClient extends Plugin {
|
|||||||
// You can get and modify the app instance here
|
// You can get and modify the app instance here
|
||||||
async load() {
|
async load() {
|
||||||
this.addRoutes();
|
this.addRoutes();
|
||||||
|
|
||||||
this.app.addComponents({
|
this.app.addComponents({
|
||||||
|
// 自定义字段组件
|
||||||
|
// GridCard: {
|
||||||
|
// ...GridCard,
|
||||||
|
// // @ts-ignore
|
||||||
|
// // Decorator: GridCardDe,
|
||||||
|
// },
|
||||||
|
ShowDetail,
|
||||||
ShareProduct,
|
ShareProduct,
|
||||||
});
|
});
|
||||||
console.log(this.app);
|
// console.log(this.app);
|
||||||
// this.app.addComponents({})
|
// this.app.addComponents({})
|
||||||
// this.app.addScopes({})
|
// this.app.addScopes({})
|
||||||
// this.app.addProvider()
|
// this.app.addProvider()
|
||||||
@ -23,9 +34,18 @@ export class PluginSancongtouClient extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addRoutes() {
|
addRoutes() {
|
||||||
this.app.router.add('sancongtou/detail', {
|
// this.app.router.add('sancongtou/detail', {
|
||||||
path: '/mobile/products/detail',
|
// path: '/mobile/products/detail',
|
||||||
Component: ProductDetail,
|
// Component: ProductDetail,
|
||||||
|
// });
|
||||||
|
this.app.router.add('mobile.productDetail', {
|
||||||
|
path: getPathProductDetail({
|
||||||
|
name: ':name',
|
||||||
|
dataSource: ':dataSource',
|
||||||
|
collection: ':collection',
|
||||||
|
id: ':id',
|
||||||
|
}),
|
||||||
|
Component: CardDetailSC,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
import { DataBlockProvider, useCollectionManager } from '@tachybase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
// import { ProductDetail } from './ProductDetail';
|
||||||
|
|
||||||
|
// THINK: provider 主要作用是定位于, 连接数据库, 不做其他的逻辑
|
||||||
|
export const CardDetailProvider = (props) => {
|
||||||
|
const params = useParams();
|
||||||
|
const { collection: collectionName } = params;
|
||||||
|
const cm = useCollectionManager();
|
||||||
|
const collection = cm.getCollection(collectionName);
|
||||||
|
const primaryKey = collection.getPrimaryKey();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataBlockProvider
|
||||||
|
dataSource="main"
|
||||||
|
collection={collection}
|
||||||
|
action="get"
|
||||||
|
params={{
|
||||||
|
appends: ['main_images'],
|
||||||
|
filterByTk: params[primaryKey],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</DataBlockProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,78 @@
|
|||||||
|
import { Plugin, SchemaComponent, useCollectionManager } from '@tachybase/client';
|
||||||
|
import { MPage } from '@tachybase/plugin-mobile-client/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { CardDetailProvider } from './CardDetail.provider';
|
||||||
|
import { CardDetailView } from './CardDetail.view';
|
||||||
|
import { useScopeCardDetail } from './CardDetail.scope';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
// THINK: plugin的注册,应该每个组件自己管自己的, 提高内聚性, 就近原则;
|
||||||
|
// 或者单独写一个轻量的给组件用的 plugin, 用于注册
|
||||||
|
// 这里暂时用 SC 的前缀 表明这一用途, schema component
|
||||||
|
// 对于那些零散的组件, 都在共同的文件夹根目录声明
|
||||||
|
// 尝试, 注册部分直接放在 schema 文件里, 会不会更好一些?
|
||||||
|
// 那么把转化部分也放进来, 会不会更好? 尝试一下
|
||||||
|
// 注册部分和转化部分, 都是样板代码, 心智模式无需关注, 只需关注 schema 的声明实现
|
||||||
|
// 声明部分, 是数据和视图的桥梁, 数据通过 context, provider 提供. 在 component 里消费
|
||||||
|
|
||||||
|
// 声明部分
|
||||||
|
// THINK: 代码逻辑部分写在这里, 会明显导致卡顿甚至卡死, 需要思考为什么? 目前还是将 provider 外移
|
||||||
|
const useSchema = () => {
|
||||||
|
// const params = useParams();
|
||||||
|
// const { collection: collectionName } = params;
|
||||||
|
// const cm = useCollectionManager();
|
||||||
|
// const collection = cm.getCollection(collectionName);
|
||||||
|
// const primaryKey = collection.getPrimaryKey();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'void',
|
||||||
|
'x-designer': 'MPage.Designer',
|
||||||
|
'x-component': 'MPage',
|
||||||
|
'x-component-props': {},
|
||||||
|
properties: {
|
||||||
|
DetailView: {
|
||||||
|
type: 'void',
|
||||||
|
// 'x-decorator-props': {
|
||||||
|
// dataSource: 'main',
|
||||||
|
// collection: collection,
|
||||||
|
// action: 'get',
|
||||||
|
// params: {
|
||||||
|
// appends: ['main_images'],
|
||||||
|
// filterByTk: params[primaryKey],
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
'x-decorator': 'page.provider.CardDetailProvider',
|
||||||
|
'x-use-component-props': 'useScopeCardDetail',
|
||||||
|
'x-component': 'page.view.CardDetailView',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 注册部分
|
||||||
|
export class SCCardDetail extends Plugin {
|
||||||
|
async load() {
|
||||||
|
this.app.addComponents({
|
||||||
|
'page.provider.CardDetailProvider': CardDetailProvider,
|
||||||
|
'page.view.CardDetailView': CardDetailView,
|
||||||
|
});
|
||||||
|
// THINK: 这里不能声明成形如: page.scope.useScopeCardDetail 的形式, 底层没有做处理.
|
||||||
|
// 也没想好这里的标准应该是什么? 也许需要归类?
|
||||||
|
this.app.addScopes({
|
||||||
|
useScopeCardDetail: useScopeCardDetail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转化部分
|
||||||
|
export const CardDetailSC = () => {
|
||||||
|
const schema = useSchema();
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
components={{
|
||||||
|
MPage,
|
||||||
|
}}
|
||||||
|
schema={schema}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
import { useCollectionRecordData } from '@tachybase/client';
|
||||||
|
|
||||||
|
export const useScopeCardDetail = () => {
|
||||||
|
const data = useCollectionRecordData();
|
||||||
|
const { ['main_images']: mainImages = [] } = data || {};
|
||||||
|
const images = mainImages.map((image) => image.url);
|
||||||
|
|
||||||
|
return {
|
||||||
|
images,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,37 @@
|
|||||||
|
import { withDynamicSchemaProps } from '@tachybase/client';
|
||||||
|
import { Image, NavBar, Skeleton } from 'antd-mobile';
|
||||||
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import './style.less';
|
||||||
|
|
||||||
|
// TODO: 需要提升图片加载速度, 现在是数据库请求然后用 url 去云端拿图片,导致体验有延迟
|
||||||
|
export const CardDetailView = withDynamicSchemaProps(
|
||||||
|
(props) => {
|
||||||
|
const { images = [] } = props;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const onBack = () => {
|
||||||
|
navigate(-1);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="m-product-container">
|
||||||
|
<NavBar back="返回" onBack={onBack}>
|
||||||
|
产品详情
|
||||||
|
</NavBar>
|
||||||
|
<div className={'m-share-image'}>
|
||||||
|
<ImageWithSkeletonCustom imageSrc={images[0]} />
|
||||||
|
<ImageWithSkeletonCustom imageSrc={images[1]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ displayName: 'CardDetailView' },
|
||||||
|
);
|
||||||
|
|
||||||
|
const ImageWithSkeletonCustom = (props) => {
|
||||||
|
const { imageSrc } = props;
|
||||||
|
if (imageSrc) {
|
||||||
|
return <Image src={imageSrc} width="100%" />;
|
||||||
|
} else {
|
||||||
|
return <Skeleton animated className={'customSkeleton'} />;
|
||||||
|
}
|
||||||
|
};
|
@ -1,49 +0,0 @@
|
|||||||
import { NavBar, Image } from 'antd-mobile';
|
|
||||||
import React from 'react';
|
|
||||||
import detailHeaderImpage from '../assets/detail_lt_header.png';
|
|
||||||
import detailContentImage from '../assets/detail_lt_content.png';
|
|
||||||
import { css } from '@tachybase/client';
|
|
||||||
|
|
||||||
export const ProductDetail = () => {
|
|
||||||
const onBack = () => {
|
|
||||||
const history = window.history;
|
|
||||||
history.back();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<NavBar
|
|
||||||
className={css`
|
|
||||||
.adm-nav-bar-left,
|
|
||||||
.adm-nav-bar-title {
|
|
||||||
font-size: 2vw;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
zIndex: 1,
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
width: '100%',
|
|
||||||
height: '5%',
|
|
||||||
}}
|
|
||||||
back="返回"
|
|
||||||
onBack={onBack}
|
|
||||||
>
|
|
||||||
产品详情
|
|
||||||
</NavBar>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
alignItems: 'center',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Image src={detailHeaderImpage} width="100%" />
|
|
||||||
<Image src={detailContentImage} width="100%" />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -0,0 +1,19 @@
|
|||||||
|
.m-product-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f3f3f3;
|
||||||
|
}
|
||||||
|
.m-share-image {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
clip-path: inset(0 10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.customSkeleton {
|
||||||
|
--width: 100%;
|
||||||
|
--height: 50%;
|
||||||
|
--border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
|||||||
|
interface ProductDetailParams {
|
||||||
|
name?: string;
|
||||||
|
dataSource?: string;
|
||||||
|
collection: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
export function getPathProductDetail(params: ProductDetailParams) {
|
||||||
|
const { name = 'detail', dataSource = 'main', collection, id } = params;
|
||||||
|
return `/mobile/${name}/${dataSource}/${collection}/${id}`;
|
||||||
|
}
|
@ -1,22 +0,0 @@
|
|||||||
export const images = [
|
|
||||||
{
|
|
||||||
label: '全部商品',
|
|
||||||
imageUrl: 'https://courier.dzkbdd.cn/static/jx-h5/assets/operator_all_select-884b60ce.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '中国电信',
|
|
||||||
imageUrl: 'https://courier.dzkbdd.cn/static/jx-h5/assets/operator_dx-7df8411f.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '中国移动',
|
|
||||||
imageUrl: 'https://courier.dzkbdd.cn/static/jx-h5/assets/operator_yd-7b994595.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '中国联通',
|
|
||||||
imageUrl: 'https://courier.dzkbdd.cn/static/jx-h5/assets/operator_lt-d9888233.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '中国广电',
|
|
||||||
imageUrl: 'https://courier.dzkbdd.cn/static/jx-h5/assets/operator_gd-cc48f3e6.png',
|
|
||||||
},
|
|
||||||
];
|
|
@ -1,2 +1 @@
|
|||||||
export * from './useAction.ImageSearchItemView';
|
|
||||||
export * from './useSelect';
|
export * from './useSelect';
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
import { useCollection, useDesigner } from '@tachybase/client';
|
|
||||||
import { useFieldSchema } from '@tachybase/schema';
|
|
||||||
import React from 'react';
|
|
||||||
import { useTranslation } from '../../../../../locale';
|
|
||||||
|
|
||||||
export function useActionImageSearchItemView(props) {
|
|
||||||
const { list, onSelected, valueKey: _valueKey, labelKey: _labelKey, filterKey } = props;
|
|
||||||
const fieldSchema = useFieldSchema();
|
|
||||||
const collection = useCollection();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const collectionField = React.useMemo(
|
|
||||||
() => collection?.getField(fieldSchema['fieldName'] as any),
|
|
||||||
[collection, fieldSchema['fieldName']],
|
|
||||||
);
|
|
||||||
const Designer = useDesigner();
|
|
||||||
const valueKey = _valueKey || collectionField?.targetKey || 'id';
|
|
||||||
const labelKey = _labelKey || fieldSchema['x-component-props']?.fieldNames?.label || valueKey;
|
|
||||||
|
|
||||||
const onSelect = (itemKey) => {
|
|
||||||
const key = itemKey.keyPath?.[0] || itemKey;
|
|
||||||
onSelected([key], filterKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fieldNames = {
|
|
||||||
title: labelKey || valueKey,
|
|
||||||
key: valueKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
const itemsData = (list || []).map((item) => {
|
|
||||||
const { type, ['image_show']: imageObj, [fieldNames.title]: label, [fieldNames.key]: key } = item;
|
|
||||||
|
|
||||||
// TODO: 需要更好的处理url方式
|
|
||||||
const origin = location?.origin || '';
|
|
||||||
const sourceUrl = imageObj?.[0]?.url;
|
|
||||||
let imageUrl = sourceUrl ?? '';
|
|
||||||
if (!imageUrl.includes('http')) {
|
|
||||||
imageUrl = `${origin}${sourceUrl}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'all') {
|
|
||||||
return {
|
|
||||||
label: t('AllProducts'),
|
|
||||||
key: 'all',
|
|
||||||
imageUrl: imageUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
label,
|
|
||||||
key,
|
|
||||||
imageUrl: imageUrl,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
collectionField,
|
|
||||||
Designer,
|
|
||||||
items: itemsData,
|
|
||||||
onSelect,
|
|
||||||
};
|
|
||||||
}
|
|
@ -3,6 +3,5 @@ export * from './ImageSearch.initializer';
|
|||||||
export * from './ImageSearch.provider';
|
export * from './ImageSearch.provider';
|
||||||
export * from './ImageSearch.schema';
|
export * from './ImageSearch.schema';
|
||||||
export * from './ImageSearch.view';
|
export * from './ImageSearch.view';
|
||||||
export * from './data';
|
|
||||||
export * from './hooks';
|
export * from './hooks';
|
||||||
export * from './search-item';
|
export * from './search-item';
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { SortableItem, css, withDynamicSchemaProps } from '@tachybase/client';
|
import { SortableItem, css, useCollection, useDesigner, withDynamicSchemaProps } from '@tachybase/client';
|
||||||
|
import { useFieldSchema } from '@tachybase/schema';
|
||||||
import { Image, JumboTabs } from 'antd-mobile';
|
import { Image, JumboTabs } from 'antd-mobile';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useActionImageSearchItemView } from '../hooks/useAction.ImageSearchItemView';
|
import { useTranslation } from '../../../../../locale';
|
||||||
|
// import { useActionImageSearchItemView } from '../hooks/useAction.ImageSearchItemView';
|
||||||
|
|
||||||
export const ImageSearchItemView = withDynamicSchemaProps(
|
export const ImageSearchItemView = withDynamicSchemaProps(
|
||||||
(props) => {
|
(props) => {
|
||||||
const { collectionField, Designer, items, onSelect } = useActionImageSearchItemView(props);
|
const { collectionField, Designer, items, onSelect } = useAction(props);
|
||||||
|
|
||||||
if (!collectionField) {
|
if (!collectionField) {
|
||||||
return null;
|
return null;
|
||||||
@ -25,6 +27,63 @@ export const ImageSearchItemView = withDynamicSchemaProps(
|
|||||||
{ displayName: 'ImageSearchItemView' },
|
{ displayName: 'ImageSearchItemView' },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function useAction(props) {
|
||||||
|
const { list, onSelected, valueKey: _valueKey, labelKey: _labelKey, filterKey } = props;
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const collection = useCollection();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const collectionField = React.useMemo(
|
||||||
|
() => collection?.getField(fieldSchema['fieldName'] as any),
|
||||||
|
[collection, fieldSchema['fieldName']],
|
||||||
|
);
|
||||||
|
const Designer = useDesigner();
|
||||||
|
const valueKey = _valueKey || collectionField?.targetKey || 'id';
|
||||||
|
const labelKey = _labelKey || fieldSchema['x-component-props']?.fieldNames?.label || valueKey;
|
||||||
|
|
||||||
|
const onSelect = (itemKey) => {
|
||||||
|
const key = itemKey.keyPath?.[0] || itemKey;
|
||||||
|
onSelected([key], filterKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldNames = {
|
||||||
|
title: labelKey || valueKey,
|
||||||
|
key: valueKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemsData = (list || []).map((item) => {
|
||||||
|
const { type, ['image_show']: imageObj, [fieldNames.title]: label, [fieldNames.key]: key } = item;
|
||||||
|
|
||||||
|
// TODO: 需要更好的处理url方式
|
||||||
|
const origin = location?.origin || '';
|
||||||
|
const sourceUrl = imageObj?.[0]?.url;
|
||||||
|
let imageUrl = sourceUrl ?? '';
|
||||||
|
if (!imageUrl.includes('http')) {
|
||||||
|
imageUrl = `${origin}${sourceUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'all') {
|
||||||
|
return {
|
||||||
|
label: t('AllProducts'),
|
||||||
|
key: 'all',
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
key,
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
collectionField,
|
||||||
|
Designer,
|
||||||
|
items: itemsData,
|
||||||
|
onSelect,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const ImageDescription = (props) => {
|
const ImageDescription = (props) => {
|
||||||
const { srcUrl, label } = props;
|
const { srcUrl, label } = props;
|
||||||
return (
|
return (
|
||||||
|
@ -9,6 +9,8 @@ import {
|
|||||||
mBlockInitializers,
|
mBlockInitializers,
|
||||||
mBlockInitializers_deprecated,
|
mBlockInitializers_deprecated,
|
||||||
MobileFormItemInitializers,
|
MobileFormItemInitializers,
|
||||||
|
SwiperFieldSettings,
|
||||||
|
SwiperPage,
|
||||||
TabSearchFieldSchemaInitializer,
|
TabSearchFieldSchemaInitializer,
|
||||||
TabSearchItemFieldSettings,
|
TabSearchItemFieldSettings,
|
||||||
} from './core/schema';
|
} from './core/schema';
|
||||||
@ -27,6 +29,7 @@ export class MobileClientPlugin extends Plugin {
|
|||||||
this.app.schemaInitializerManager.add(ImageSearchConfigureFields);
|
this.app.schemaInitializerManager.add(ImageSearchConfigureFields);
|
||||||
this.app.schemaInitializerManager.add(TabSearchFieldSchemaInitializer);
|
this.app.schemaInitializerManager.add(TabSearchFieldSchemaInitializer);
|
||||||
|
|
||||||
|
this.app.schemaSettingsManager.add(SwiperFieldSettings);
|
||||||
this.app.schemaSettingsManager.add(ImageSearchItemFieldSettings);
|
this.app.schemaSettingsManager.add(ImageSearchItemFieldSettings);
|
||||||
this.app.schemaSettingsManager.add(TabSearchItemFieldSettings);
|
this.app.schemaSettingsManager.add(TabSearchItemFieldSettings);
|
||||||
this.app.schemaInitializerManager.add(MobileFormItemInitializers);
|
this.app.schemaInitializerManager.add(MobileFormItemInitializers);
|
||||||
@ -82,7 +85,7 @@ export class MobileClientPlugin extends Plugin {
|
|||||||
});
|
});
|
||||||
this.app.router.add('mobile.swiper.page', {
|
this.app.router.add('mobile.swiper.page', {
|
||||||
path: '/mobile/:name/image/:collection/:field/:fieldParams',
|
path: '/mobile/:name/image/:collection/:field/:fieldParams',
|
||||||
Component: 'SwiperPage',
|
Component: SwiperPage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1778,6 +1778,9 @@ importers:
|
|||||||
'@tachybase/client':
|
'@tachybase/client':
|
||||||
specifier: 0.x
|
specifier: 0.x
|
||||||
version: link:../../../core/client
|
version: link:../../../core/client
|
||||||
|
'@tachybase/plugin-mobile-client':
|
||||||
|
specifier: 0.x
|
||||||
|
version: link:../../@tachybase/plugin-mobile-client
|
||||||
'@tachybase/server':
|
'@tachybase/server':
|
||||||
specifier: 0.x
|
specifier: 0.x
|
||||||
version: link:../../../core/server
|
version: link:../../../core/server
|
||||||
@ -1787,6 +1790,9 @@ importers:
|
|||||||
antd-mobile:
|
antd-mobile:
|
||||||
specifier: ^5.35.0
|
specifier: ^5.35.0
|
||||||
version: 5.35.0(react-dom@18.2.0)(react@18.2.0)
|
version: 5.35.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
react-router-dom:
|
||||||
|
specifier: ^6.23.1
|
||||||
|
version: 6.23.1(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
|
||||||
packages/plugins/@nocobase/plugin-acl:
|
packages/plugins/@nocobase/plugin-acl:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -10803,6 +10809,11 @@ packages:
|
|||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@remix-run/router@1.16.1:
|
||||||
|
resolution: {integrity: sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@restart/hooks@0.4.15(react@18.2.0):
|
/@restart/hooks@0.4.15(react@18.2.0):
|
||||||
resolution: {integrity: sha512-cZFXYTxbpzYcieq/mBwSyXgqnGMHoBVh3J7MU0CCoIB4NRZxV9/TuwTBAaLMqpNhC3zTPMCgkQ5Ey07L02Xmcw==}
|
resolution: {integrity: sha512-cZFXYTxbpzYcieq/mBwSyXgqnGMHoBVh3J7MU0CCoIB4NRZxV9/TuwTBAaLMqpNhC3zTPMCgkQ5Ey07L02Xmcw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -26612,6 +26623,19 @@ packages:
|
|||||||
react-router: 6.22.3(react@18.2.0)
|
react-router: 6.22.3(react@18.2.0)
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/react-router-dom@6.23.1(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8'
|
||||||
|
react-dom: '>=16.8'
|
||||||
|
dependencies:
|
||||||
|
'@remix-run/router': 1.16.1
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
react-router: 6.23.1(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-router-dom@6.3.0(react-dom@18.1.0)(react@18.1.0):
|
/react-router-dom@6.3.0(react-dom@18.1.0)(react@18.1.0):
|
||||||
resolution: {integrity: sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==}
|
resolution: {integrity: sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -26663,6 +26687,16 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/react-router@6.23.1(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8'
|
||||||
|
dependencies:
|
||||||
|
'@remix-run/router': 1.16.1
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-router@6.3.0(react@18.1.0):
|
/react-router@6.3.0(react@18.1.0):
|
||||||
resolution: {integrity: sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==}
|
resolution: {integrity: sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
Loading…
Reference in New Issue
Block a user