feat: 三聪头相关移动端支持逻辑 (#798)

Co-authored-by: sealday <zhanglin@daoyoucloud.com>
Co-authored-by: sealday <sealday@gmail.com>
Reviewed-on: daoyoucloud/tachybase#798
Co-authored-by: bai.zixv <bai.zixv@foxmail.com>
Co-committed-by: bai.zixv <bai.zixv@foxmail.com>
This commit is contained in:
bai.zixv 2024-05-06 12:41:25 +08:00 committed by sealday
parent 46129499be
commit 4bb491f2e3
56 changed files with 1514 additions and 76 deletions

View File

@ -15,6 +15,7 @@ import { isPatternDisabled } from '../../../../schema-settings';
import { ActionType } from '../../../../schema-settings/LinkageRules/type';
import { SchemaSettingsDefaultValue } from '../../../../schema-settings/SchemaSettingsDefaultValue';
import { useIsAllowToSetDefaultValue } from '../../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
import { css } from '@nocobase/client';
export const fieldSettingsFormItem = new SchemaSettings({
name: 'fieldSettings:FormItem',
@ -223,6 +224,47 @@ export const fieldSettingsFormItem = new SchemaSettings({
},
Component: SchemaSettingsDefaultValue,
},
{
name: 'layoutDirection',
type: 'select',
useComponentProps() {
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const { dn } = useDesignable();
const initialValue = fieldSchema['x-decorator-props']?.layoutDirection ?? 'column';
return {
title: t('Layout Direction'),
options: [
{ label: t('Row'), value: 'row' },
{ label: t('Column'), value: 'column' },
],
value: initialValue,
onChange(v) {
const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'],
};
const styleValue = {
layoutDirection: v ?? 'column',
style: {
display: 'flex',
flexDirection: `${v === 'row' ? 'row' : 'column'}`,
alignItems: 'baseline',
},
};
_.set(fieldSchema, 'x-decorator-props', styleValue);
_.set(schema, 'x-decorator-props', styleValue);
dn.emit('patch', {
schema,
});
dn.refresh();
},
};
},
},
{
name: 'pattern',
type: 'select',

View File

@ -0,0 +1,173 @@
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;
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,
children: (hasMore: boolean, failed: boolean, retry: () => void) => (
<InfiniteScrollContent hasMore={hasMore} failed={failed} retry={retry} />
),
};
export const InfiniteScroll: FC<InfiniteScrollProps> = (p) => {
const props = mergeProps(defaultProps, p);
const [failed, setFailed] = useState(false);
const doLoadMore = useLockFn(async (isRetry: boolean) => {
try {
await props.loadMore(isRetry);
} catch (e) {
setFailed(true);
throw e;
}
});
const elementRef = useRef<HTMLDivElement>(null);
// Prevent duplicated trigger of `check` function
const [flag, setFlag] = useState({});
const nextFlagRef = useRef(flag);
const [scrollParent, setScrollParent] = useState<Window | Element | null | undefined>();
const { run: check } = useThrottleFn(
async () => {
if (nextFlagRef.current !== flag) return;
if (!props.hasMore) return;
const element = elementRef.current;
if (!element) return;
if (!element.offsetParent) return;
const parent = getScrollParent(element);
setScrollParent(parent);
if (!parent) return;
const rect = element.getBoundingClientRect();
const elementTop = rect.top;
const current = isWindow(parent) ? window.innerHeight : parent.getBoundingClientRect().bottom;
if (current >= elementTop - props.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);
}
}
},
{
wait: 100,
leading: true,
trailing: true,
},
);
// Make sure to trigger `loadMore` when content changes
useEffect(() => {
check();
});
useEffect(() => {
const element = elementRef.current;
if (!element) return;
if (!scrollParent) return;
function onScroll() {
check();
}
scrollParent.addEventListener('scroll', onScroll);
return () => {
scrollParent.removeEventListener('scroll', onScroll);
};
}, [scrollParent]);
async function retry() {
setFailed(false);
try {
await doLoadMore(true);
setFlag(nextFlagRef.current);
} catch (e) {
console.log('%c Line:121 🥥 e', 'font-size:18px;color:#3f7cff;background:#2eafb0', e);
}
}
return withNativeProps(
props,
<div className={classPrefix} ref={elementRef}>
{typeof props.children === 'function' ? props.children(props.hasMore, failed, retry) : props.children}
</div>,
);
};
const InfiniteScrollContent: FC<{
hasMore: boolean;
failed: boolean;
retry: () => void;
}> = (props) => {
const { locale } = useConfig();
if (!props.hasMore) {
return <span>{locale.InfiniteScroll.noMore}</span>;
}
if (props.failed) {
return (
<span>
<span className={`${classPrefix}-failed-text`}>{locale.InfiniteScroll.failedToLoad}</span>
<a
onClick={() => {
props.retry();
}}
>
{locale.InfiniteScroll.retry}
</a>
</span>
);
}
return (
<>
<span>{locale.common.loading}</span>
<DotLoading />
</>
);
};
const keepComp = (props) => {
return <InfiniteScroll {...props} />;
};
keepComp({});

View File

@ -0,0 +1,54 @@
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();
}

View File

@ -0,0 +1,85 @@
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);
}

View File

@ -620,17 +620,33 @@ export const SchemaSettingsConnectDataBlocks: FC<SchemaSettingsConnectDataBlocks
const Content = dataBlocks.map((block) => {
const title = `${compile(block.collection.title)} #${block.uid.slice(0, 4)}`;
const onHover = () => {
// 保存当前的滚动位置
const originalScrollPosition = window.scrollY || window.pageYOffset || document.documentElement.scrollTop;
const dom = block.dom;
const designer = dom.querySelector('.general-schema-designer') as HTMLElement;
if (designer) {
designer.style.display = 'block';
}
dom.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.2)';
dom.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
setTimeout(() => {
// 使用平滑滚动滚回到原始位置
dom.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}, 1000);
setTimeout(() => {
// 使用平滑滚动滚回到原始位置
window.scrollTo({
top: originalScrollPosition,
behavior: 'smooth',
});
}, 2000);
};
const onLeave = () => {
const dom = block.dom;
const designer = dom.querySelector('.general-schema-designer') as HTMLElement;
@ -662,8 +678,9 @@ export const SchemaSettingsConnectDataBlocks: FC<SchemaSettingsConnectDataBlocks
}).catch(error);
dn.refresh();
}}
onMouseEnter={onHover}
// onMouseEnter={onHover}
onMouseLeave={onLeave}
onMouseUp={onHover}
/>
);
}

View File

@ -0,0 +1,17 @@
import { Plugin, tval } from '@nocobase/client';
class PluginCommon extends Plugin {
async afterAdd() {}
async beforeLoad() {}
async load() {
// 扩展原移动端插件的 block 类型
this.app.schemaInitializerManager.addItem('mobilePage:addBlock', 'filterBlocks', {
title: tval('Filter blocks'),
type: 'itemGroup',
children: [],
});
}
}
export default PluginCommon;

View File

@ -1,19 +1,24 @@
import { Plugin } from '@nocobase/client';
import PluginCommon from './common/plugin';
import PluginBlock from './schma-block';
import PluginImageSearch from './schma-component/image-search/plugin';
import PluginSwiper from './schma-component/swiper';
import PluginTabSearch from './schma-component/tab-search';
import './assets/svg/index';
import PluginSwiper from './schma-component/swiper';
class PluginMobileClient extends Plugin {
async beforeLoad() {}
async load() {}
async afterAdd() {
this.pm.add(PluginCommon);
this.pm.add(PluginBlock);
this.pm.add(PluginTabSearch);
this.pm.add(PluginSwiper);
this.pm.add(PluginImageSearch);
// this.app.router.add()
}
async beforeLoad() {}
async load() {}
}
export default PluginMobileClient;

View File

@ -0,0 +1,53 @@
import { SchemaInitializer, useCollection, useCollectionManager } from '@nocobase/client';
import { tval } from '../../locale';
import { useIsMobile } from '../tab-search/components/field-item/hooks';
import { canBeOptionalField, canBeRelatedField } from '../tab-search/utils';
import { createSchemaImageSearchItem } from './search-item/ImageSearchItem.schema';
export const ImageSearchConfigureFields = new SchemaInitializer({
name: 'ImageSearchView:configureFields',
title: tval('Configure fields'),
style: { marginTop: 16 },
icon: 'SettingOutlined',
items: [
{
name: 'choicesFields',
type: 'itemGroup',
title: tval('Choices fields'),
useChildren: useChildrenChoicesFieldSchemas,
},
],
});
function useChildrenChoicesFieldSchemas() {
const collection = useCollection();
const fields = collection?.fields;
const cm = useCollectionManager();
const isMobile = useIsMobile();
const choicesFieldsSchemas = fields
.map((field) => {
const { interface: _interface, target: collectionName } = field;
const label = cm.getCollection(collectionName)?.getPrimaryKey() ?? 'id';
const isCanBeOptional = canBeOptionalField(_interface);
const isCanBeRelated = canBeRelatedField(_interface);
if (isCanBeOptional || isCanBeRelated) {
const schema = createSchemaImageSearchItem({
collection,
field,
isMobile,
label,
isCanBeOptional,
isCanBeRelated,
});
return schema;
}
return null;
})
.filter(Boolean);
return choicesFieldsSchemas;
}

View File

@ -0,0 +1,37 @@
import { DataBlockInitializer, useSchemaInitializer, useSchemaInitializerItem } from '@nocobase/client';
import React from 'react';
import { createSchemaImageSearchBlock } from './ImageSearch.schema';
interface ItemConfig {
name: string;
title: string;
icon: string;
}
export function ImageSearchInitializer(props) {
const { filterCollections } = props;
const itemConfig: ItemConfig = useSchemaInitializerItem();
const { insert } = useSchemaInitializer();
const onCreateBlockSchema = async ({ item }) => {
const { dataSource, collectionName, name } = item;
const schema = createSchemaImageSearchBlock({
dataSource,
collection: collectionName || name,
blockType: 'filter',
});
insert(schema);
};
return (
<DataBlockInitializer
{...itemConfig}
componentType={'ImageSearch'}
filter={filterCollections}
onCreateBlockSchema={onCreateBlockSchema}
/>
);
}

View File

@ -0,0 +1,6 @@
import { DataBlockProvider } from '@nocobase/client';
import React from 'react';
export const ImageSearchProvider = ({ collection, children }) => {
return <DataBlockProvider collection={collection}>{children}</DataBlockProvider>;
};

View File

@ -0,0 +1,34 @@
import { ISchema } from '@tachybase/schema';
import { uid } from '@nocobase/utils/client';
interface OptionsType {
collection: string;
dataSource: string;
blockType: string;
}
export function createSchemaImageSearchBlock(options: OptionsType): ISchema {
const { collection, dataSource, blockType } = options;
return {
type: 'void',
// TODO: 困惑, 这里不能直接用 DataBlockProvider, 因为它没有collection和dataSource属性
'x-decorator': 'ImageSearchProvider',
'x-decorator-props': {
collection,
dataSource,
blockType,
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:filterCollapse',
'x-component': 'CardItem',
'x-filter-targets': [],
properties: {
[uid()]: {
type: 'void',
'x-action': 'imageSearch',
'x-initializer': 'ImageSearchView:configureFields',
'x-component': 'ImageSearchView',
},
},
};
}

View File

@ -0,0 +1,19 @@
import { useDesigner, useSchemaInitializerRender } from '@nocobase/client';
import { RecursionField, useFieldSchema } from '@tachybase/schema';
import React from 'react';
export const ImageSearchView = () => {
const Designer = useDesigner();
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']);
return (
<>
<Designer />
<RecursionField schema={fieldSchema} onlyRenderProperties />
<React.Fragment>{render()}</React.Fragment>
</>
);
};
export default ImageSearchView;

View File

@ -0,0 +1,22 @@
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',
},
];

View File

@ -0,0 +1,61 @@
import { useCollection, useDesigner } from '@nocobase/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,
};
}

View File

@ -0,0 +1,73 @@
import { findFilterTargets, mergeFilter, useCollection, useFilterBlock } from '@nocobase/client';
import { useFieldSchema } from '@tachybase/schema';
import _ from 'lodash';
import { useMemo } from 'react';
export const useGetSelected = () => {
const fieldSchema = useFieldSchema();
const collection = useCollection();
const { getDataBlocks } = useFilterBlock();
const collectionField = useMemo(() => collection?.getField(fieldSchema.name as any), [collection, fieldSchema.name]);
const onSelected = (value, filterKey) => {
const { targets, uid } = findFilterTargets(fieldSchema);
getDataBlocks().forEach((block) => {
const target = targets.find((target) => target.uid === block.uid);
if (!target) return;
const key = `${uid}${fieldSchema.name}`;
const param = block.service.params?.[0] || {};
// 保留原有的 filter
let storedFilter = block.service.params?.[1]?.filters || {};
if (value.length) {
storedFilter[key] = {
[filterKey]: value,
};
} else {
if (block.dataLoadingMode === 'manual') {
return block.clearData();
}
delete storedFilter[key];
}
const mergedFilter = mergeFilter([...Object.values(storedFilter), block.defaultFilter]);
if (value.length === 1 && value[0] === 'all') {
const currentKey = Object.keys(storedFilter[key]);
if (mergedFilter['$and']) {
currentKey.forEach((currentValue) => {
mergedFilter['$and'].forEach((value, index) => {
if (value[currentValue]) {
mergedFilter['$and'].splice(index, 1);
}
});
});
mergedFilter['$and'] = mergedFilter['$and'].filter(Boolean);
} else {
delete mergedFilter[currentKey[0]];
}
storedFilter[key] = {};
}
storedFilter = _.omitBy(
storedFilter,
(value) => value === null || value === undefined || (_.isObject(value) && _.isEmpty(value)),
);
return block.doFilter(
{
...param,
page: 1,
filter: mergedFilter,
},
{ filters: storedFilter },
);
});
};
return {
onSelected,
};
};
export const useIsMobile = () => {
const fieldSchema = useFieldSchema();
const isMobile = Object.values(fieldSchema.root.properties).some((value) => value['x-component'] === 'MContainer');
return isMobile;
};

View File

@ -0,0 +1,40 @@
import { Plugin } from '@nocobase/client';
import { ImageSearchView } from './ImageSearch.view';
import { ImageSearchInitializer } from './ImageSearch.initializer';
import { ImageSearchConfigureFields } from './ImageSearch.configure';
import { ImageSearchProvider } from './ImageSearch.provider';
import { ImageSearchItemIntializer } from './search-item/ImageSearchItem.intializer';
import { ImageSearchItemToolbar } from './search-item/ImageSearchItem.toolbar';
import { ImageSearchItemView } from './search-item/ImageSearchItem.view';
import { ImageSearchItemFieldSettings } from './search-item/ImageSearchItem.setting';
import { usePropsOptionalImageSearchItemField } from './search-item/useProps.Optional';
import { usePropsRelatedImageSearchItemField } from './search-item/useProps.Related';
class PluginImageSearch extends Plugin {
async load() {
this.app.addScopes({
usePropsOptionalImageSearchItemField: usePropsOptionalImageSearchItemField,
usePropsRelatedImageSearchItemField: usePropsRelatedImageSearchItemField,
});
this.app.addComponents({
ImageSearchView: ImageSearchView,
ImageSearchInitializer: ImageSearchInitializer,
ImageSearchProvider: ImageSearchProvider,
ImageSearchItemIntializer: ImageSearchItemIntializer,
ImageSearchItemToolbar: ImageSearchItemToolbar,
ImageSearchItemView: ImageSearchItemView,
});
this.app.schemaInitializerManager.add(ImageSearchConfigureFields);
this.schemaSettingsManager.add(ImageSearchItemFieldSettings);
this.app.schemaInitializerManager.addItem('mobilePage:addBlock', 'filterBlocks.imageSearch', {
name: 'imageSearch',
title: 'imageSearch',
icon: 'tab-search',
Component: 'ImageSearchInitializer',
});
}
}
export default PluginImageSearch;

View File

@ -0,0 +1,28 @@
import {
SchemaInitializerSwitch,
useCurrentSchema,
useSchemaInitializer,
useSchemaInitializerItem,
} from '@nocobase/client';
import { merge } from '@tachybase/schema';
import React from 'react';
export const ImageSearchItemIntializer = () => {
const itemConfig = useSchemaInitializerItem();
const { insert } = useSchemaInitializer();
const { schema: oldSchema, title } = itemConfig;
const { exists, remove } = useCurrentSchema(oldSchema.name, 'name', itemConfig.find, itemConfig.remove);
const onClick = () => {
if (exists) {
return remove();
}
const { schema: latestSchema } = itemConfig;
const newSchema = merge(oldSchema || {}, latestSchema || {});
// itemConfig?.schemaInitialize?.(newSchema);
insert(newSchema);
};
return <SchemaInitializerSwitch checked={exists} title={title} onClick={onClick} />;
};

View File

@ -0,0 +1,45 @@
import { SchemaInitializerItemType } from '@nocobase/client';
interface OptionsType {
label?: string;
field?: any;
collection?: any;
isMobile?: boolean;
isCanBeOptional?: boolean;
isCanBeRelated?: boolean;
}
export function createSchemaImageSearchItem(options: OptionsType): SchemaInitializerItemType {
const { field, isMobile, label, isCanBeOptional, isCanBeRelated, collection } = options;
const { key, name, uiSchema, interface: _interface } = field;
const title = uiSchema?.title;
const indexOfUseProps = [isCanBeOptional, isCanBeRelated].indexOf(true);
return {
name: key,
title: title,
Component: 'ImageSearchItemIntializer',
schema: {
name: `${name}-choice`,
fieldName: name,
title: title,
type: 'void',
'x-toolbar': 'ImageSearchItemToolbar',
'x-settings': 'fieldSettings:component:ImageSearchItem',
'x-component': 'ImageSearchItemView',
'x-component-props': {
fieldNames: {
label,
// 固定字段, 用于取数据表对外展示的图片字段
imageShow: 'image_show',
},
interface: _interface,
collectionName: collection?.name,
correlation: name,
},
'x-use-component-props': ['usePropsOptionalImageSearchItemField', 'usePropsRelatedImageSearchItemField'][
indexOfUseProps
],
properties: {},
},
};
}

View File

@ -0,0 +1,161 @@
import {
SchemaSettings,
SchemaSettingsDataScope,
useCollection,
useCollectionManager,
useCompile,
useDesignable,
useFormBlockContext,
} from '@nocobase/client';
import { useField, useFieldSchema } from '@tachybase/schema';
import _ from 'lodash';
import { useTranslation } from '../../../locale';
import { isTabSearchCollapsibleInputItem } from '../../tab-search/utils';
export const ImageSearchItemFieldSettings = new SchemaSettings({
name: 'fieldSettings:component:ImageSearchItem',
items: [
{
name: 'decoratorOptions',
type: 'itemGroup',
hideIfNoChildren: true,
useComponentProps() {
const { t } = useTranslation();
return {
title: t('Generic properties'),
};
},
useChildren() {
return [
{
name: 'setTheDataScope',
Component: SchemaSettingsDataScope,
useComponentProps() {
const fieldSchema = useFieldSchema();
const { form } = useFormBlockContext();
const field = useField();
const cm = useCollectionManager();
const c = useCollection();
const collectionField =
c.getField(fieldSchema['name']) || cm.getCollectionField(fieldSchema['x-collection-field']);
const { dn } = useDesignable();
return {
collectionName: collectionField?.target,
defaultFilter: fieldSchema?.['x-component-props']?.params?.filter || {},
form: form,
onSubmit: ({ filter }) => {
_.set(field.componentProps, 'params', {
...field.componentProps?.params,
filter,
});
fieldSchema['x-component-props']['params'] = field.componentProps.params;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-component-props': fieldSchema['x-component-props'],
},
});
},
};
},
useVisible() {
const fieldSchema = useFieldSchema();
return !isTabSearchCollapsibleInputItem(fieldSchema['x-component']);
},
},
{
name: 'titleField',
type: 'select',
useComponentProps() {
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const cm = useCollectionManager();
const c = useCollection();
const fieldCollection = fieldSchema['x-component-props']?.['collectionName'];
const correlation = fieldSchema['x-component-props']?.['correlation'];
const collectionField =
c.getField(fieldSchema['fieldName']) ||
cm.getCollectionField(fieldSchema['x-collection-field']) ||
cm.getCollection(fieldCollection + '.' + correlation);
const compile = useCompile();
const { dn } = useDesignable();
const targetFields = collectionField?.target ? cm.getCollectionFields(collectionField?.target) : [];
const options = targetFields
.filter((field) => !field?.target && field.type !== 'boolean')
.map((field) => ({
value: field?.name,
label: compile(field?.uiSchema?.title) || field?.name,
}));
const onTitleFieldChange = (label) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
const fieldNames = {
...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'],
...fieldSchema['x-component-props']?.['fieldNames'],
label,
};
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props']['fieldNames'] = fieldNames;
schema['x-component-props'] = fieldSchema['x-component-props'];
dn.emit('patch', {
schema,
});
dn.refresh();
};
return {
key: 'title-field',
title: t('Title field'),
options: options,
value: fieldSchema['x-component-props']?.fieldNames?.label,
onChange: onTitleFieldChange,
};
},
useVisible() {
const fieldSchema = useFieldSchema();
return (
isTabSearchCollapsibleInputItem(fieldSchema['x-component']) ||
fieldSchema['x-component-props']?.['correlation']
);
},
},
];
},
},
{
name: 'divider',
type: 'divider',
},
{
name: 'delete',
type: 'remove',
sort: 100,
useComponentProps() {
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const { dn } = useDesignable();
return {
removeParentsIfNoChildren: true,
confirm: {
title: t('Delete field'),
},
breakRemoveOn: (s) => {
if (isTabSearchCollapsibleInputItem(fieldSchema['x-component'])) {
Object.values(s.properties).forEach((value) => {
if (isTabSearchCollapsibleInputItem(value['x-component']) && value.name !== fieldSchema.name) {
delete s.properties[value.name];
}
});
dn.emit('patch', { schema: s });
}
return s['x-component'] === 'TabSearch';
},
};
},
},
],
});

View File

@ -0,0 +1,6 @@
import { SchemaToolbar } from '@nocobase/client';
import React from 'react';
export const ImageSearchItemToolbar = (props) => {
return <SchemaToolbar draggable showBorder showBackground initializer={false} {...props} />;
};

View File

@ -0,0 +1,54 @@
import { SortableItem, css, withDynamicSchemaProps } from '@nocobase/client';
import { Image, JumboTabs } from 'antd-mobile';
import React from 'react';
import { useActionImageSearchItemView } from '../hooks/useAction.ImageSearchItemView';
export const ImageSearchItemView = withDynamicSchemaProps(
(props) => {
const { collectionField, Designer, items, onSelect } = useActionImageSearchItemView(props);
if (!collectionField) {
return null;
}
return (
<SortableItem>
<Designer />
<JumboTabs onChange={onSelect}>
{items.map(({ key, label, imageUrl }) => (
<JumboTabs.Tab key={key} title={''} description={<ImageDescription srcUrl={imageUrl} label={label} />} />
))}
</JumboTabs>
</SortableItem>
);
},
{ displayName: 'ImageSearchItemView' },
);
const ImageDescription = (props) => {
const { srcUrl, label } = props;
return (
<div
className={css`
display: 'flex';
flex-direction: 'column';
width: '100%';
height: '100%';
:active {
background-color: transparent;
}
`}
>
<Image src={srcUrl} width={100} height={100} fit="fill" />
<p
className={css`
font-weight: 400;
font-size: 17.6px;
line-height: 1;
`}
>
{label}
</p>
</div>
);
};

View File

@ -0,0 +1,53 @@
import { useMemo } from 'react';
import { useCollection, useCollectionManager } from '@nocobase/client';
import { useFieldSchema } from '@tachybase/schema';
import _ from 'lodash';
import { useTabSearchCollapsibleInputItem } from '../../tab-search/components/field-item/hooks';
import { canBeOptionalField } from '../../tab-search/utils';
export const usePropsOptionalImageSearchItemField = () => {
const fieldSchema = useFieldSchema();
const collection = useCollection();
const cm = useCollectionManager();
const optionalFieldList = getOptionalFieldList({ collection });
const fieldName = fieldSchema['fieldName'];
const collectionField = useMemo(() => collection?.getField(fieldName), [collection, fieldName]);
const { onSelected } = useTabSearchCollapsibleInputItem();
if (!collection) {
return {};
}
const result = { list: null, valueKey: '', labelKey: '', filterKey: '' };
result.valueKey = collectionField?.target ? cm.getCollection(collectionField.target)?.getPrimaryKey() : 'id';
result.labelKey = fieldSchema['x-component-props']?.fieldNames?.label || result.valueKey;
const fieldInterface = fieldSchema['x-component-props'].interface;
if (canBeOptionalField(fieldInterface)) {
const field = optionalFieldList.find((field) => field.name === fieldSchema['fieldName']);
const operatorMap = {
select: '$in',
multipleSelect: '$anyOf',
checkboxGroup: '$anyOf',
radioGroup: '$in',
};
const _list = field?.uiSchema?.enum || [];
result.valueKey = 'value';
result.labelKey = 'label';
result.list = _list;
result.filterKey = `${field.name}.${operatorMap[field.interface]}`;
}
return {
list: result.list,
valueKey: result.valueKey,
labelKey: result.labelKey,
onSelected,
filterKey: result.filterKey,
};
};
const getOptionalFieldList = ({ collection }) => {
const currentFields = collection?.fields ?? [];
return currentFields.filter((field) => canBeOptionalField(field.interface) && field.uiSchema.enum);
};

View File

@ -0,0 +1,87 @@
// 关系字段类型
import { useCollection, useCollectionManager, useDataSourceHeaders, useRequest } from '@nocobase/client';
import { useField, useFieldSchema } from '@tachybase/schema';
import { useEffect, useMemo } from 'react';
import { canBeRelatedField } from '../../tab-search/utils';
import { useGetSelected } from '../hooks/useSelect';
interface requestResultType {
data: { [key: string]: any }[];
}
export const usePropsRelatedImageSearchItemField = () => {
const fieldSchema = useFieldSchema();
const collection = useCollection();
const field = useField();
// TODO: 这里需要替换成获取 source id 的方式,不过我们暂时没有用多数据源,就不着急
const blockProps = { dataSource: 'main' };
const headers = useDataSourceHeaders(blockProps?.dataSource);
const cm = useCollectionManager();
const collectionField = useMemo(
() => collection?.getField(fieldSchema['fieldName'] as any),
[collection, fieldSchema['fieldName']],
);
const collectionFieldName = collectionField?.name;
const fieldInterface = fieldSchema['x-component-props'].interface;
const { onSelected } = useGetSelected();
const result = { list: null, valueKey: '', labelKey: '', filterKey: '' };
result.valueKey = collectionField?.target ? cm.getCollection(collectionField.target)?.getPrimaryKey() : 'id';
result.labelKey = fieldSchema['x-component-props']?.fieldNames?.label || result.valueKey;
const imageShow = fieldSchema['x-component-props']?.fieldNames?.imageShow || 'imageShow';
const { data } = useRequestRelatedField({ headers, collectionField, result, imageShow, field, fieldInterface });
result.filterKey = `${collectionFieldName}.${result.valueKey}.$in`;
result.list = data?.data || [];
if (!collectionField) {
return;
}
return {
list: result.list,
valueKey: result.valueKey,
labelKey: result.labelKey,
filterKey: result.filterKey,
onSelected,
};
};
const useRequestRelatedField = ({ headers, collectionField, result, imageShow, field, fieldInterface }) => {
const { data, run } = useRequest<requestResultType>(
{
headers,
resource: collectionField?.target,
action: 'list',
params: {
fields: [result.labelKey, result.valueKey, imageShow, 'type'],
pageSize: 200,
page: 1,
...field.componentProps?.params,
},
},
{
manual: true,
debounceWait: 300,
},
);
useEffect(() => {
if (canBeRelatedField(fieldInterface) && collectionField?.target) {
run();
}
}, [
result.labelKey,
result.valueKey,
JSON.stringify(field.componentProps?.params || {}),
canBeRelatedField(fieldInterface),
]);
return { data };
};

View File

@ -2,10 +2,10 @@ import { useMemo, useRef, useState } from 'react';
import { useFieldSchema } from '@tachybase/schema';
import { useCollection, useCollectionManager, useDesignable, useDesigner } from '@nocobase/client';
import {
canBeCalculatedField,
canBeDataField,
canBeRelatedField,
changFormat,
convertFormat,
isTabSearchCollapsibleInputItem,
} from '../../utils';
import { useTabSearchCollapsibleInputItem } from './hooks';
@ -17,6 +17,7 @@ export const useTabSearchCollapsibleInputItemAction = (props) => {
const collection = useCollection();
const cm = useCollectionManager();
const { dn } = useDesignable();
const [needSort, setNeedSort] = useState(false);
const collectionField = useMemo(
() => collection?.getField(fieldSchema['fieldName'] as any),
[collection, fieldSchema['fieldName']],
@ -44,6 +45,11 @@ export const useTabSearchCollapsibleInputItemAction = (props) => {
const onSelect = (value) => {
let time;
let filterKey = `${customLabelKey}.$includes`;
const canBeCal = canBeCalculatedField(fieldInterface);
if (canBeCal) {
filterKey = `${customLabelKey}.$notEmpty`;
}
if (canBeDataField(fieldInterface)) {
time = value.split('&').map((value) => JSON.parse(value));
filterKey = `${customLabelKey}.$dateBetween`;
@ -53,7 +59,12 @@ export const useTabSearchCollapsibleInputItemAction = (props) => {
const correlation = fieldSchema['x-component-props']['correlation'];
filterKey = `${correlation}.${label}.$includes`;
}
onSelected(time || [value], filterKey);
onSelected(time || [value], filterKey, {
canBeCalculatedField: canBeCal,
customLabelKey,
needSort,
});
};
const onSelectChange = (label) => {
@ -107,5 +118,7 @@ export const useTabSearchCollapsibleInputItemAction = (props) => {
customLabelKey,
fieldInterface,
onDateClick,
needSort,
setNeedSort,
};
};

View File

@ -1,9 +1,10 @@
import { SortableItem, withDynamicSchemaProps } from '@nocobase/client';
import { Grid } from 'antd-mobile';
import { SortableItem, css, withDynamicSchemaProps } from '@nocobase/client';
import { Checkbox, Grid, Switch } from 'antd-mobile';
import React from 'react';
import { useTabSearchCollapsibleInputItemAction } from './TabSearchCollapsibleInputItemAction';
import { IButton, IDatePicker, IInput, ISelect } from './TabSearchCollapsibleInputMItemChild';
import { canBeDataField } from '../../utils';
import { useTranslation } from '../../../../locale';
export const TabSearchCollapsibleInputMItem = withDynamicSchemaProps(
(props) => {
@ -12,13 +13,16 @@ export const TabSearchCollapsibleInputMItem = withDynamicSchemaProps(
Designer,
options,
value,
needSort,
onSelectChange,
onInputChange,
onButtonClick,
customLabelKey,
fieldInterface,
onDateClick,
setNeedSort,
} = useTabSearchCollapsibleInputItemAction(props);
const { t } = useTranslation();
if (!collectionField) {
return null;
@ -26,17 +30,46 @@ export const TabSearchCollapsibleInputMItem = withDynamicSchemaProps(
return (
<SortableItem>
<Designer />
<Grid columns={4} style={{ backgroundColor: '#f9f9f9', borderRadius: '5px', margin: '5px 0 0 0' }}>
<ISelect options={options} onChange={onSelectChange} customLabelKey={customLabelKey} />
{canBeDataField(fieldInterface) ? (
<IDatePicker value={value} onChange={onDateClick} onInputChange={onInputChange} options={options} />
) : (
<>
<IInput options={options} value={value} onChange={onInputChange} />
<IButton onClick={onButtonClick} />
</>
)}
</Grid>
<div
className={css`
display: flex;
flex-direction: row;
align-items: center;
`}
>
<Grid
columns={4}
style={{
flex: 1,
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-start',
padding: '10px',
backgroundColor: '#f9f9f9',
borderRadius: '5px',
margin: '5px 0 0 0',
}}
>
<ISelect options={options} onChange={onSelectChange} customLabelKey={customLabelKey} />
{canBeDataField(fieldInterface) ? (
<IDatePicker value={value} onChange={onDateClick} onInputChange={onInputChange} options={options} />
) : (
<>
<IInput options={options} value={value} onChange={onInputChange} />
<IButton onClick={onButtonClick} />
</>
)}
</Grid>
<Checkbox
className={css`
margin-right: 10px;
`}
checked={needSort}
onChange={setNeedSort}
>
{t('sort')}
</Checkbox>
</div>
</SortableItem>
);
},

View File

@ -102,7 +102,7 @@ export const IInput = (props) => {
const { options, value, onChange } = props;
const { t } = useTranslation();
return (
<Grid.Item span={options.length > 1 ? 2 : 3}>
<Grid.Item style={{ flex: 1 }} span={options.length > 1 ? 2 : 3}>
<Input
placeholder={t('Please enter search content')}
value={value}

View File

@ -1,15 +1,18 @@
import { findFilterTargets, mergeFilter, useApp, useCollection, useFilterBlock } from '@nocobase/client';
import { findFilterTargets, mergeFilter, useCollection, useFilterBlock } from '@nocobase/client';
import { useFieldSchema } from '@tachybase/schema';
import { useMemo } from 'react';
import _ from 'lodash';
export const useTabSearchCollapsibleInputItem = () => {
const fieldSchema = useFieldSchema();
const collection = useCollection();
const { getDataBlocks } = useFilterBlock();
const collectionField = useMemo(() => collection?.getField(fieldSchema.name as any), [collection, fieldSchema.name]);
const onSelected = (value, filterKey) => {
const onSelected = (
value,
filterKey,
options = { canBeCalculatedField: false, customLabelKey: '', needSort: false },
) => {
const { canBeCalculatedField, customLabelKey, needSort = true } = options;
const { targets, uid } = findFilterTargets(fieldSchema);
getDataBlocks().forEach((block) => {
const target = targets.find((target) => target.uid === block.uid);
@ -22,6 +25,12 @@ export const useTabSearchCollapsibleInputItem = () => {
storedFilter[key] = {
[filterKey]: value,
};
if (canBeCalculatedField) {
storedFilter[key] = {
[filterKey]: Number(value?.[0] ?? 0),
};
}
} else {
if (block.dataLoadingMode === 'manual') {
return block.clearData();
@ -50,14 +59,21 @@ export const useTabSearchCollapsibleInputItem = () => {
(value) => value === null || value === undefined || (_.isObject(value) && _.isEmpty(value)),
);
return block.doFilter(
{
...param,
page: 1,
filter: mergedFilter,
},
{ filters: storedFilter },
);
let params1 = {
...param,
page: 1,
filter: mergedFilter,
};
if (needSort) {
params1 = {
...params1,
['sort[]']: `-${customLabelKey}`,
};
} else {
params1 = _.omit(params1, ['sort[]']);
}
return block.doFilter(params1, { filters: storedFilter });
});
};
@ -68,6 +84,6 @@ export const useTabSearchCollapsibleInputItem = () => {
export const useIsMobile = () => {
const fieldSchema = useFieldSchema();
const isMobile = Object.values(fieldSchema.root.properties).find((value) => value['x-component'] === 'MContainer');
const isMobile = Object.values(fieldSchema.root.properties).some((value) => value['x-component'] === 'MContainer');
return isMobile;
};

View File

@ -5,7 +5,6 @@ import { TabSearchBlockInitializer } from './initializer/TabSearchBlockInitializ
import { TabSearchFieldItem } from './components/field-item/TabSearchFieldItem';
import { TabSearchCollapsibleInputItem } from './components/field-item/TabSearchCollapsibleInputItem';
import { TabSearchFieldSchemaInitializerGadget } from './initializer/TabSearchFieldSchemaInitializerGadget';
import { tval } from '../../locale';
import { useTabSearchFieldItemRelatedProps } from './components/field-item/TabSerachFieldItemRelatedProps';
import { useTabSearchFieldItemProps } from './components/field-item/TabSearchFieldItemProps';
import { TabSearchFieldSchemaInitializer } from './initializer/TabSearchFieldSchemaInitializer';
@ -32,16 +31,10 @@ class PluginTabSearch extends Plugin {
});
this.app.schemaInitializerManager.add(TabSearchFieldSchemaInitializer);
this.schemaSettingsManager.add(TabSearchItemFieldSettings);
this.app.schemaInitializerManager.addItem('mobilePage:addBlock', 'filterBlocks', {
title: tval('Filter blocks'),
type: 'itemGroup',
children: [
{
name: 'tabSearch',
title: 'tabSearch',
Component: 'TabSearchBlockInitializer',
},
],
this.app.schemaInitializerManager.addItem('mobilePage:addBlock', 'filterBlocks.tabSearch', {
name: 'tabSearch',
title: 'tabSearch',
Component: 'TabSearchBlockInitializer',
});
this.app.schemaInitializerManager.addItem('page:addBlock', 'filterBlocks.tabSearch', {
name: 'tabSearch',

View File

@ -10,8 +10,6 @@ const canBeSearchFields = [
'phone',
'email',
'url',
'integer',
'number',
'percent',
'password',
'color',
@ -20,6 +18,8 @@ const canBeSearchFields = [
'sequence',
];
const canBeCalculatedFields = ['integer', 'number'];
export const isTabSearchCollapsibleInputItem = (component: string) =>
component === 'TabSearchCollapsibleInputItem' || component === 'TabSearchCollapsibleInputMItem';
@ -27,6 +27,7 @@ export const canBeOptionalField = (_interface: string) => canBeOptionalFields.in
export const canBeRelatedField = (_interface: string) => canBeRelatedFields.includes(_interface);
export const canBeDataField = (_interface: string) => canBeDataFields.includes(_interface);
export const canBeSearchField = (_interface: string) => canBeSearchFields.includes(_interface);
export const canBeCalculatedField = (_interface: string) => canBeCalculatedFields.includes(_interface);
export const convertFormat = (currentDate) => {
return dayjs(currentDate).format('YYYY-MM-DD');

View File

@ -11,5 +11,7 @@
"Configure fields": "Configure fields",
"Configure columns": "Configure columns",
"Add block": "Add block",
"all": "all"
"all": "all",
"AllProducts": "AllProducts",
"sort":"sort"
}

View File

@ -11,5 +11,7 @@
"Configure fields": "配置字段",
"Configure columns": "配置字段",
"Add block": "创建区块",
"all": "全部"
"all": "全部",
"AllProducts": "全部商品",
"sort": "排序"
}

View File

@ -0,0 +1,2 @@
/node_modules
/src

View File

@ -0,0 +1 @@
# @hera/plugin-sancongtou

View File

@ -0,0 +1,2 @@
export * from './dist/client';
export { default } from './dist/client';

View File

@ -0,0 +1 @@
module.exports = require('./dist/client/index.js');

View File

@ -0,0 +1,18 @@
{
"name": "@hera/plugin-sancongtou",
"displayName": "Mobile client: Sancongtou Customization",
"version": "0.21.24",
"description": "Provide mobile client customization for the sancongtou project.",
"main": "dist/server/index.js",
"dependencies": {
"@ant-design/icons": "^5.3.6",
"antd-mobile": "^5.35.0"
},
"peerDependencies": {
"@nocobase/client": "0.x",
"@nocobase/server": "0.x",
"@nocobase/test": "0.x"
},
"description.zh-CN": "提供移动端: 定制化的三聪头项目页面,.",
"displayName.zh-CN": "移动端: 三聪头,定制化页面配置"
}

View File

@ -0,0 +1,2 @@
export * from './dist/server';
export { default } from './dist/server';

View File

@ -0,0 +1 @@
module.exports = require('./dist/server/index.js');

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 369 KiB

View File

@ -0,0 +1,63 @@
import { Modal, Toast, Image, Button } from 'antd-mobile';
import { ShareAltOutlined } from '@ant-design/icons';
import React from 'react';
import downloadImage from '../assets/download.svg';
import { CustomComponentType } from '@hera/plugin-core/client';
export const ShareProduct = () => {
return (
<Button
style={{
alignSelf: 'center',
fontSize: 16,
}}
color="primary"
size="small"
block
onClick={showModal}
>
<ShareAltOutlined />
</Button>
);
};
ShareProduct.displayName = 'ShareProduct';
ShareProduct.__componentType = CustomComponentType.CUSTOM_FORM_ITEM;
ShareProduct.__componentLabel = '移动端-三聪头-分享产品';
const showModal = () => {
Modal.show({
bodyStyle: {
background: 'transparent',
},
content: <ImageShow />,
closeOnMaskClick: true,
actions: [
{
key: 'share',
text: '复制推广链接',
primary: true,
},
],
onAction: async () => {
await copyHrefToClipboard();
Toast.show('复制成功!');
},
});
};
const ImageShow = () => {
return <Image style={{ flex: 1 }} src={downloadImage} width="100%" fit="scale-down" />;
};
async function copyHrefToClipboard() {
const location = window.location;
const href = location.href;
try {
await navigator.clipboard.writeText(href);
console.log('文本已复制到剪贴板', href);
} catch (err) {
console.error('无法复制文本: ', err);
}
}

View File

@ -0,0 +1,11 @@
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>
);
};

View File

@ -0,0 +1,33 @@
import { Plugin } from '@nocobase/client';
import { ProductDetail } from './pages/ProductDetail';
import { ShareProduct } from './components/ShareProduct';
export class PluginSancongtouClient extends Plugin {
async afterAdd() {
// await this.app.pm.add()
}
async beforeLoad() {}
// You can get and modify the app instance here
async load() {
this.addRoutes();
this.app.addComponents({
ShareProduct,
});
console.log(this.app);
// this.app.addComponents({})
// this.app.addScopes({})
// this.app.addProvider()
// this.app.addProviders()
}
addRoutes() {
this.app.router.add('sancongtou/detail', {
path: '/mobile/products/detail',
Component: ProductDetail,
});
}
}
export default PluginSancongtouClient;

View File

@ -0,0 +1,49 @@
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 '@nocobase/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>
</>
);
};

View File

@ -0,0 +1,2 @@
export * from './server';
export { default } from './server';

View File

@ -0,0 +1 @@
export { default } from './plugin';

View File

@ -0,0 +1,19 @@
import { Plugin } from '@nocobase/server';
export class PluginSancongtouServer extends Plugin {
async afterAdd() {}
async beforeLoad() {}
async load() {}
async install() {}
async afterEnable() {}
async afterDisable() {}
async remove() {}
}
export default PluginSancongtouServer;

View File

@ -22,7 +22,7 @@ const useStyles = createStyles(({ token, css }) => {
& > .general-schema-designer {
--nb-designer-top: ${token.marginMD}px;
}
position: absolute;
position: fixed;
background: ${token.colorBgContainer};
width: 100%;
bottom: 0;

View File

@ -1764,6 +1764,24 @@ importers:
specifier: ^6.26.0
version: 6.35.2
packages/plugins/@hera/plugin-sancongtou:
dependencies:
'@ant-design/icons':
specifier: ^5.3.6
version: 5.3.6(react-dom@18.2.0)(react@18.2.0)
'@nocobase/client':
specifier: 0.x
version: link:../../../core/client
'@nocobase/server':
specifier: 0.x
version: link:../../../core/server
'@nocobase/test':
specifier: 0.x
version: link:../../../core/test
antd-mobile:
specifier: ^5.35.0
version: 5.35.0(react-dom@18.2.0)(react@18.2.0)
packages/plugins/@nocobase/plugin-acl:
dependencies:
'@nocobase/acl':
@ -8708,7 +8726,6 @@ packages:
resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==}
dependencies:
'@floating-ui/utils': 0.2.1
dev: true
/@floating-ui/dom@0.4.5:
resolution: {integrity: sha512-b+prvQgJt8pieaKYMSJBXHxX/DYwdLsAWxKYqnO5dO2V4oo/TYBZJAUQCVNjTWWsrs6o4VDrNcP9+E70HAhJdw==}
@ -8720,7 +8737,6 @@ packages:
dependencies:
'@floating-ui/core': 1.6.0
'@floating-ui/utils': 0.2.1
dev: true
/@floating-ui/react-dom-interactions@0.3.1(@types/react@18.2.75)(react-dom@18.1.0)(react@18.1.0):
resolution: {integrity: sha512-tP2KEh7EHJr5hokSBHcPGojb+AorDNUf0NYfZGg/M+FsMvCOOsSEeEF0O1NDfETIzDnpbHnCs0DuvCFhSMSStg==}
@ -8780,7 +8796,6 @@ packages:
/@floating-ui/utils@0.2.1:
resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==}
dev: true
/@formily/antd-v5@1.2.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(antd@5.16.1)(react-dom@18.2.0)(react-is@18.3.1)(react@18.2.0)(typescript@4.9.5):
resolution: {integrity: sha512-6K1fV7pwvy7j5KoO/5dEe5llosann7211I/bH/WJkLQtDSDK+XLFUQ4/lU7CdNpwHMrspzXS3lF82EM3hGyM8w==}
@ -10211,7 +10226,6 @@ packages:
'@react-spring/shared': 9.6.1(react@18.2.0)
'@react-spring/types': 9.6.1
react: 18.2.0
dev: true
/@react-spring/core@9.6.1(react@18.2.0):
resolution: {integrity: sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==}
@ -10223,11 +10237,9 @@ packages:
'@react-spring/shared': 9.6.1(react@18.2.0)
'@react-spring/types': 9.6.1
react: 18.2.0
dev: true
/@react-spring/rafz@9.6.1:
resolution: {integrity: sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==}
dev: true
/@react-spring/shared@9.6.1(react@18.2.0):
resolution: {integrity: sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==}
@ -10237,11 +10249,9 @@ packages:
'@react-spring/rafz': 9.6.1
'@react-spring/types': 9.6.1
react: 18.2.0
dev: true
/@react-spring/types@9.6.1:
resolution: {integrity: sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==}
dev: true
/@react-spring/web@9.6.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-X2zR6q2Z+FjsWfGAmAXlQaoUHbPmfuCaXpuM6TcwXPpLE1ZD4A1eys/wpXboFQmDkjnrlTmKvpVna1MjWpZ5Hw==}
@ -10255,7 +10265,6 @@ packages:
'@react-spring/types': 9.6.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: true
/@redis/bloom@1.2.0(@redis/client@1.5.14):
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
@ -13114,7 +13123,6 @@ packages:
/@use-gesture/core@10.3.0:
resolution: {integrity: sha512-rh+6MND31zfHcy9VU3dOZCqGY511lvGcfyJenN4cWZe0u1BH6brBpBddLVXhF2r4BMqWbvxfsbL7D287thJU2A==}
dev: true
/@use-gesture/react@10.3.0(react@18.2.0):
resolution: {integrity: sha512-3zc+Ve99z4usVP6l9knYVbVnZgfqhKah7sIG+PS2w+vpig2v2OLct05vs+ZXMzwxdNCMka8B+8WlOo0z6Pn6DA==}
@ -13123,7 +13131,6 @@ packages:
dependencies:
'@use-gesture/core': 10.3.0
react: 18.2.0
dev: true
/@vercel/ncc@0.36.1:
resolution: {integrity: sha512-S4cL7Taa9yb5qbv+6wLgiKVZ03Qfkc4jGRuiUQMQ8HGBD5pcNRnHeYM33zBvJE4/zJGjJJ8GScB+WmTsn9mORw==}
@ -13140,7 +13147,7 @@ packages:
'@babel/plugin-transform-react-jsx-self': 7.24.1(@babel/core@7.24.4)
'@babel/plugin-transform-react-jsx-source': 7.24.1(@babel/core@7.24.4)
react-refresh: 0.14.0
vite: 4.5.2(@types/node@20.12.7)(less@4.1.3)
vite: 4.5.2(@types/node@20.12.2)(less@4.1.3)
transitivePeerDependencies:
- supports-color
@ -13453,7 +13460,6 @@ packages:
resize-observer-polyfill: 1.5.1
screenfull: 5.2.0
tslib: 2.6.2
dev: true
/ahooks@3.7.8(react@18.2.0):
resolution: {integrity: sha512-e/NMlQWoCjaUtncNFIZk3FG1ImSkV/JhScQSkTqnftakRwdfZWSw6zzoWSG9OMYqPNs2MguDYBUFFC6THelWXA==}
@ -13683,11 +13689,9 @@ packages:
/antd-mobile-icons@0.3.0:
resolution: {integrity: sha512-rqINQpJWZWrva9moCd1Ye695MZYWmqLPE+bY8d2xLRy7iSQwPsinCdZYjpUPp2zL/LnKYSyXxP2ut2A+DC+whQ==}
dev: true
/antd-mobile-v5-count@1.0.1:
resolution: {integrity: sha512-YGsiEDCPUDz3SzfXi6gLZn/HpeSMW+jgPc4qiYUr1fSopg3hkUie2TnooJdExgfiETHefH3Ggs58He0OVfegLA==}
dev: true
/antd-mobile@5.35.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UYr8T2Tlw3eXdyDYpWrPu54RWDcon4biI5VWHtMp9XxmSP5tzypRxoseh1ZGE28boQq9FKpSCC6FZ9rN0HXnQg==}
@ -13716,7 +13720,6 @@ packages:
use-sync-external-store: 1.2.0(react@18.2.0)
transitivePeerDependencies:
- react-dom
dev: true
/antd-style@3.4.5(@types/react@18.2.79)(antd@5.16.1)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-6aC4P9XyuVy0O7eZ+HZXd8GbbFX9HgzsXsJ341ihJhgqrfsQZNx8lDQvS2kCV6ao99QqtyTDphK9gWOgV2bHEw==}
@ -22753,7 +22756,6 @@ packages:
/nano-memoize@3.0.16:
resolution: {integrity: sha512-JyK96AKVGAwVeMj3MoMhaSXaUNqgMbCRSQB3trUV8tYZfWEzqUBKdK1qJpfuNXgKeHOx1jv/IEYTM659ly7zUA==}
dev: true
/nanoid@3.3.4:
resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
@ -25224,7 +25226,6 @@ packages:
rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: true
/rc-field-form@1.44.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-el7w87fyDUsca63Y/s8qJcq9kNkf/J5h+iTdqG5WsSHLH0e6Usl7QuYSmSVzJMgtp40mOVZIY/W/QP9zwrp1FA==}
@ -26945,7 +26946,6 @@ packages:
/runes2@1.1.4:
resolution: {integrity: sha512-LNPnEDPOOU4ehF71m5JoQyzT2yxwD6ZreFJ7MxZUAoMKNMY1XrAo60H1CUoX5ncSm0rIuKlqn9JZNRrRkNou2g==}
dev: true
/rw@1.3.3:
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
@ -27809,7 +27809,6 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: true
/static-extend@0.1.2:
resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==}
@ -30017,7 +30016,6 @@ packages:
rollup: 3.29.4
optionalDependencies:
fsevents: 2.3.3
dev: false
/vite@4.5.2(@types/node@20.12.7)(less@4.1.3):
resolution: {integrity: sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==}
@ -30054,6 +30052,7 @@ packages:
rollup: 3.29.4
optionalDependencies:
fsevents: 2.3.3
dev: true
/vite@5.1.5(@types/node@20.12.7)(sass@1.75.0):
resolution: {integrity: sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==}