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,
|
||||
rowKey,
|
||||
isLinkable: false,
|
||||
needInfiniteScroll: false,
|
||||
layoutDirection: 'column',
|
||||
},
|
||||
'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',
|
||||
Component: SchemaSettingsTemplate,
|
||||
|
@ -33,6 +33,7 @@ const InternalGridCardBlockProvider = (props) => {
|
||||
service,
|
||||
resource,
|
||||
columnCount: props.columnCount,
|
||||
needInfiniteScroll: props.needInfiniteScroll,
|
||||
}}
|
||||
>
|
||||
<FormContext.Provider value={form}>
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCollection } from '@tachybase/client';
|
||||
import { ObjectField } from '@tachybase/schema';
|
||||
import { useField } from '@tachybase/schema';
|
||||
import { useField, useFieldSchema } from '@tachybase/schema';
|
||||
import { Card } from 'antd';
|
||||
import React from 'react';
|
||||
import { useCollectionParentRecordData } from '../../../data-source/collection-record/CollectionRecordProvider';
|
||||
import { RecordProvider } from '../../../record-provider';
|
||||
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useGridCardDetailUrl } from './hooks';
|
||||
|
||||
const itemCss = css`
|
||||
display: flex;
|
||||
@ -17,13 +20,28 @@ const itemCss = css`
|
||||
`;
|
||||
|
||||
export const GridCardItem = withDynamicSchemaProps((props) => {
|
||||
const collection = useCollection();
|
||||
const field = useField<ObjectField>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
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 (
|
||||
<Card
|
||||
role="button"
|
||||
aria-label="grid-card-item"
|
||||
className={css`
|
||||
/* background-color: re/d; */
|
||||
height: 100%;
|
||||
> .ant-card-body {
|
||||
padding: 24px 24px 0px;
|
||||
@ -33,8 +51,10 @@ export const GridCardItem = withDynamicSchemaProps((props) => {
|
||||
padding: 5px 0;
|
||||
}
|
||||
`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className={itemCss}>
|
||||
{/* @ts-ignore */}
|
||||
<RecordProvider record={field.value} parent={parentRecordData}>
|
||||
{props.children}
|
||||
</RecordProvider>
|
||||
|
@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css';
|
||||
import { ArrayField } from '@tachybase/schema';
|
||||
import { RecursionField, Schema, useField, useFieldSchema } from '@tachybase/schema';
|
||||
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 { SchemaComponentOptions } from '../../core';
|
||||
import { useDesigner, useProps } from '../../hooks';
|
||||
@ -12,6 +12,7 @@ import { GridCardItem } from './GridCard.Item';
|
||||
import { useGridCardActionBarProps } from './hooks';
|
||||
import { defaultColumnCount, pageSizeOptions } from './options';
|
||||
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
|
||||
import { InfiniteScroll } from '../../common/infinite-scroll/infinite-scroll';
|
||||
|
||||
const rowGutter = {
|
||||
md: 12,
|
||||
@ -59,8 +60,7 @@ const designerCss = css`
|
||||
const InternalGridCard = (props) => {
|
||||
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
||||
const { columnCount: columnCountProp, pagination } = useProps(props);
|
||||
|
||||
const { service, columnCount: _columnCount = defaultColumnCount } = useGridCardBlockContext();
|
||||
const { service, columnCount: _columnCount = defaultColumnCount, needInfiniteScroll } = useGridCardBlockContext();
|
||||
const columnCount = columnCountProp || _columnCount;
|
||||
const { run, params } = service;
|
||||
const meta = service?.data?.meta;
|
||||
@ -68,6 +68,7 @@ const InternalGridCard = (props) => {
|
||||
const field = useField<ArrayField>();
|
||||
const Designer = useDesigner();
|
||||
const [schemaMap] = useState(new Map());
|
||||
|
||||
const getSchema = useCallback(
|
||||
(key) => {
|
||||
if (!schemaMap.has(key)) {
|
||||
@ -99,6 +100,31 @@ const InternalGridCard = (props) => {
|
||||
[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 (
|
||||
<SchemaComponentOptions
|
||||
scope={{
|
||||
@ -109,7 +135,7 @@ const InternalGridCard = (props) => {
|
||||
<SortableItem className={cx('nb-card-list', designerCss)}>
|
||||
<AntdList
|
||||
pagination={
|
||||
!meta || meta.count <= meta.pageSize
|
||||
!meta || meta.count <= meta.pageSize || needInfiniteScroll
|
||||
? false
|
||||
: {
|
||||
...pagination,
|
||||
@ -120,7 +146,7 @@ const InternalGridCard = (props) => {
|
||||
pageSizeOptions,
|
||||
}
|
||||
}
|
||||
dataSource={field.value}
|
||||
dataSource={needInfiniteScroll ? data : field.value}
|
||||
grid={{
|
||||
...columnCount,
|
||||
sm: columnCount.xs,
|
||||
@ -142,6 +168,7 @@ const InternalGridCard = (props) => {
|
||||
}}
|
||||
loading={service?.loading}
|
||||
/>
|
||||
{needInfiniteScroll && <InfiniteScroll loadMore={loadMore} hasMore={hasMore} />}
|
||||
<Designer />
|
||||
</SortableItem>
|
||||
</SchemaComponentOptions>
|
||||
|
@ -10,3 +10,13 @@ export const useGridCardActionBarProps = () => {
|
||||
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 screenSizeTitleMaps = {
|
||||
xs: 'Phone device',
|
||||
|
@ -1,41 +1,20 @@
|
||||
import { getScrollParent, merge } from '@tachybase/utils/client';
|
||||
import { useLockFn, useThrottleFn } from 'ahooks';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
// import { useConfig } from '../config-provider';
|
||||
// import DotLoading from '../dot-loading';
|
||||
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;
|
||||
import { Spin } from 'antd';
|
||||
import React, { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useStyles from './style';
|
||||
|
||||
function isWindow(element: any | Window): element is Window {
|
||||
return element === window;
|
||||
}
|
||||
|
||||
const classPrefix = `adm-infinite-scroll`;
|
||||
|
||||
export type InfiniteScrollProps = {
|
||||
loadMore: (isRetry: boolean) => Promise<void>;
|
||||
hasMore: boolean;
|
||||
threshold?: number;
|
||||
children?: ReactNode | ((hasMore: boolean, failed: boolean, retry: () => void) => ReactNode);
|
||||
} & NativeProps;
|
||||
};
|
||||
|
||||
const defaultProps: Required<Pick<InfiniteScrollProps, 'threshold' | 'children'>> = {
|
||||
threshold: 250,
|
||||
@ -44,13 +23,17 @@ const defaultProps: Required<Pick<InfiniteScrollProps, 'threshold' | 'children'>
|
||||
),
|
||||
};
|
||||
|
||||
export const InfiniteScroll: FC<InfiniteScrollProps> = (p) => {
|
||||
const props = mergeProps(defaultProps, p);
|
||||
|
||||
export const InfiniteScroll = ({
|
||||
threshold = defaultProps.threshold,
|
||||
loadMore,
|
||||
children = defaultProps.children,
|
||||
hasMore,
|
||||
}: InfiniteScrollProps) => {
|
||||
const { styles } = useStyles();
|
||||
const [failed, setFailed] = useState(false);
|
||||
const doLoadMore = useLockFn(async (isRetry: boolean) => {
|
||||
try {
|
||||
await props.loadMore(isRetry);
|
||||
await loadMore(isRetry);
|
||||
} catch (e) {
|
||||
setFailed(true);
|
||||
throw e;
|
||||
@ -68,7 +51,7 @@ export const InfiniteScroll: FC<InfiniteScrollProps> = (p) => {
|
||||
const { run: check } = useThrottleFn(
|
||||
async () => {
|
||||
if (nextFlagRef.current !== flag) return;
|
||||
if (!props.hasMore) return;
|
||||
if (!hasMore) return;
|
||||
const element = elementRef.current;
|
||||
if (!element) return;
|
||||
if (!element.offsetParent) return;
|
||||
@ -78,14 +61,14 @@ export const InfiniteScroll: FC<InfiniteScrollProps> = (p) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const elementTop = rect.top;
|
||||
const current = isWindow(parent) ? window.innerHeight : parent.getBoundingClientRect().bottom;
|
||||
if (current >= elementTop - props.threshold) {
|
||||
if (current >= elementTop - threshold) {
|
||||
const nextFlag = {};
|
||||
nextFlagRef.current = nextFlag;
|
||||
try {
|
||||
await doLoadMore(false);
|
||||
setFlag(nextFlag);
|
||||
} 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);
|
||||
setFlag(nextFlagRef.current);
|
||||
} catch (e) {
|
||||
console.log('%c Line:121 🥥 e', 'font-size:18px;color:#3f7cff;background:#2eafb0', e);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return withNativeProps(
|
||||
props,
|
||||
<div className={classPrefix} ref={elementRef}>
|
||||
{typeof props.children === 'function' ? props.children(props.hasMore, failed, retry) : props.children}
|
||||
</div>,
|
||||
return (
|
||||
<div className={styles.infiniteScroll} ref={elementRef}>
|
||||
{typeof children === 'function' ? children(hasMore, failed, retry) : children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InfiniteScrollContent: FC<{
|
||||
hasMore: boolean;
|
||||
failed: boolean;
|
||||
retry: () => void;
|
||||
}> = (props) => {
|
||||
const { locale } = useConfig();
|
||||
const InfiniteScrollContent = (props: { hasMore: boolean; failed: boolean; retry: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { styles } = useStyles();
|
||||
|
||||
if (!props.hasMore) {
|
||||
return <span>{locale.InfiniteScroll.noMore}</span>;
|
||||
return <span>{t('no more')}</span>;
|
||||
}
|
||||
|
||||
if (props.failed) {
|
||||
return (
|
||||
<span>
|
||||
<span className={`${classPrefix}-failed-text`}>{locale.InfiniteScroll.failedToLoad}</span>
|
||||
<span className={`${styles.infiniteScroll}-failed-text`}>{t('failed to load')}</span>
|
||||
<a
|
||||
onClick={() => {
|
||||
props.retry();
|
||||
}}
|
||||
>
|
||||
{locale.InfiniteScroll.retry}
|
||||
{t('retry')}
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
@ -160,14 +139,8 @@ const InfiniteScrollContent: FC<{
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>{locale.common.loading}</span>
|
||||
<DotLoading />
|
||||
<span>{t('loading')}</span>
|
||||
<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 { dayjs, lodash };
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
||||
console.log('%c Line:10 🥐 data', 'font-size:18px;color:#4fff4B;background:#e41a6a', data);
|
||||
|
||||
if (loading) {
|
||||
return render();
|
||||
}
|
||||
|
@ -6,10 +6,12 @@
|
||||
"main": "dist/server/index.js",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.3.6",
|
||||
"antd-mobile": "^5.35.0"
|
||||
"antd-mobile": "^5.35.0",
|
||||
"react-router-dom": "^6.23.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tachybase/client": "0.x",
|
||||
"@tachybase/plugin-mobile-client": "0.x",
|
||||
"@tachybase/server": "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 { CustomComponentType } from '@hera/plugin-core/client';
|
||||
import { Button, Image, Modal, Toast } from 'antd-mobile';
|
||||
import React from 'react';
|
||||
import downloadImage from '../assets/download.svg';
|
||||
import { CustomComponentType } from '@hera/plugin-core/client';
|
||||
import './style.less';
|
||||
|
||||
export const ShareProduct = () => {
|
||||
return (
|
||||
<Button
|
||||
style={{
|
||||
alignSelf: 'center',
|
||||
fontSize: 16,
|
||||
}}
|
||||
color="primary"
|
||||
size="small"
|
||||
block
|
||||
onClick={showModal}
|
||||
>
|
||||
<Button className={'m-share'} color="primary" size="small" block onClick={showModal}>
|
||||
分享产品
|
||||
<ShareAltOutlined />
|
||||
</Button>
|
||||
@ -26,7 +18,8 @@ ShareProduct.displayName = 'ShareProduct';
|
||||
ShareProduct.__componentType = CustomComponentType.CUSTOM_FORM_ITEM;
|
||||
ShareProduct.__componentLabel = '移动端-三聪头-分享产品';
|
||||
|
||||
const showModal = () => {
|
||||
const showModal = (event) => {
|
||||
event.stopPropagation();
|
||||
Modal.show({
|
||||
bodyStyle: {
|
||||
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 { ProductDetail } from './pages/ProductDetail';
|
||||
import { ShareProduct } from './components/ShareProduct';
|
||||
// import { ProductDetail } from './pages/ProductDetail.1';
|
||||
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 {
|
||||
async afterAdd() {
|
||||
// await this.app.pm.add()
|
||||
await this.app.pm.add(SCCardDetail);
|
||||
}
|
||||
|
||||
async beforeLoad() {}
|
||||
@ -12,10 +15,18 @@ export class PluginSancongtouClient extends Plugin {
|
||||
// You can get and modify the app instance here
|
||||
async load() {
|
||||
this.addRoutes();
|
||||
|
||||
this.app.addComponents({
|
||||
// 自定义字段组件
|
||||
// GridCard: {
|
||||
// ...GridCard,
|
||||
// // @ts-ignore
|
||||
// // Decorator: GridCardDe,
|
||||
// },
|
||||
ShowDetail,
|
||||
ShareProduct,
|
||||
});
|
||||
console.log(this.app);
|
||||
// console.log(this.app);
|
||||
// this.app.addComponents({})
|
||||
// this.app.addScopes({})
|
||||
// this.app.addProvider()
|
||||
@ -23,9 +34,18 @@ export class PluginSancongtouClient extends Plugin {
|
||||
}
|
||||
|
||||
addRoutes() {
|
||||
this.app.router.add('sancongtou/detail', {
|
||||
path: '/mobile/products/detail',
|
||||
Component: ProductDetail,
|
||||
// this.app.router.add('sancongtou/detail', {
|
||||
// path: '/mobile/products/detail',
|
||||
// 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';
|
||||
|
@ -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.schema';
|
||||
export * from './ImageSearch.view';
|
||||
export * from './data';
|
||||
export * from './hooks';
|
||||
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 React from 'react';
|
||||
import { useActionImageSearchItemView } from '../hooks/useAction.ImageSearchItemView';
|
||||
import { useTranslation } from '../../../../../locale';
|
||||
// import { useActionImageSearchItemView } from '../hooks/useAction.ImageSearchItemView';
|
||||
|
||||
export const ImageSearchItemView = withDynamicSchemaProps(
|
||||
(props) => {
|
||||
const { collectionField, Designer, items, onSelect } = useActionImageSearchItemView(props);
|
||||
const { collectionField, Designer, items, onSelect } = useAction(props);
|
||||
|
||||
if (!collectionField) {
|
||||
return null;
|
||||
@ -25,6 +27,63 @@ export const ImageSearchItemView = withDynamicSchemaProps(
|
||||
{ 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 { srcUrl, label } = props;
|
||||
return (
|
||||
|
@ -9,6 +9,8 @@ import {
|
||||
mBlockInitializers,
|
||||
mBlockInitializers_deprecated,
|
||||
MobileFormItemInitializers,
|
||||
SwiperFieldSettings,
|
||||
SwiperPage,
|
||||
TabSearchFieldSchemaInitializer,
|
||||
TabSearchItemFieldSettings,
|
||||
} from './core/schema';
|
||||
@ -27,6 +29,7 @@ export class MobileClientPlugin extends Plugin {
|
||||
this.app.schemaInitializerManager.add(ImageSearchConfigureFields);
|
||||
this.app.schemaInitializerManager.add(TabSearchFieldSchemaInitializer);
|
||||
|
||||
this.app.schemaSettingsManager.add(SwiperFieldSettings);
|
||||
this.app.schemaSettingsManager.add(ImageSearchItemFieldSettings);
|
||||
this.app.schemaSettingsManager.add(TabSearchItemFieldSettings);
|
||||
this.app.schemaInitializerManager.add(MobileFormItemInitializers);
|
||||
@ -82,7 +85,7 @@ export class MobileClientPlugin extends Plugin {
|
||||
});
|
||||
this.app.router.add('mobile.swiper.page', {
|
||||
path: '/mobile/:name/image/:collection/:field/:fieldParams',
|
||||
Component: 'SwiperPage',
|
||||
Component: SwiperPage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1778,6 +1778,9 @@ importers:
|
||||
'@tachybase/client':
|
||||
specifier: 0.x
|
||||
version: link:../../../core/client
|
||||
'@tachybase/plugin-mobile-client':
|
||||
specifier: 0.x
|
||||
version: link:../../@tachybase/plugin-mobile-client
|
||||
'@tachybase/server':
|
||||
specifier: 0.x
|
||||
version: link:../../../core/server
|
||||
@ -1787,6 +1790,9 @@ importers:
|
||||
antd-mobile:
|
||||
specifier: ^5.35.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:
|
||||
dependencies:
|
||||
@ -10803,6 +10809,11 @@ packages:
|
||||
engines: {node: '>=14.0.0'}
|
||||
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):
|
||||
resolution: {integrity: sha512-cZFXYTxbpzYcieq/mBwSyXgqnGMHoBVh3J7MU0CCoIB4NRZxV9/TuwTBAaLMqpNhC3zTPMCgkQ5Ey07L02Xmcw==}
|
||||
peerDependencies:
|
||||
@ -26612,6 +26623,19 @@ packages:
|
||||
react-router: 6.22.3(react@18.2.0)
|
||||
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):
|
||||
resolution: {integrity: sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==}
|
||||
peerDependencies:
|
||||
@ -26663,6 +26687,16 @@ packages:
|
||||
react: 18.2.0
|
||||
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):
|
||||
resolution: {integrity: sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==}
|
||||
peerDependencies:
|
||||
|
Loading…
Reference in New Issue
Block a user