fix: improve quickedit

This commit is contained in:
chenos 2025-06-30 10:18:19 +08:00
parent 0ca5272a41
commit 9af3b50f08
6 changed files with 166 additions and 120 deletions

View File

@ -23,27 +23,28 @@ import {
import { Button, InputRef, Skeleton } from 'antd'; import { Button, InputRef, Skeleton } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import { SkeletonFallback } from '../../../components/SkeletonFallback';
import { DataBlockModel } from '../../base/BlockModel'; import { DataBlockModel } from '../../base/BlockModel';
export class QuickEditForm extends DataBlockModel { export class QuickEditForm extends DataBlockModel {
form: Form; form: Form;
fieldPath: string; fieldPath: string;
declare resource: SingleRecordResource; declare resource: SingleRecordResource;
declare collection: Collection; declare collection: Collection;
static async open(options: { static async open(options: {
target: any; target: any;
flowEngine: FlowEngine;
dataSourceKey: string; dataSourceKey: string;
collectionName: string; collectionName: string;
fieldPath: string; fieldPath: string;
filterByTk: string; filterByTk: string;
}) { }) {
const { flowEngine, dataSourceKey, collectionName, fieldPath, target, filterByTk } = options; const { target, dataSourceKey, collectionName, fieldPath, filterByTk } = options;
const model = flowEngine.createModel({ const model = this.flowEngine.createModel({
use: 'QuickEditForm', use: 'QuickEditForm',
stepParams: { stepParams: {
initial: { propsFlow: {
step1: { step1: {
dataSourceKey, dataSourceKey,
collectionName, collectionName,
@ -52,81 +53,75 @@ export class QuickEditForm extends DataBlockModel {
}, },
}, },
}) as QuickEditForm; }) as QuickEditForm;
await model.open({ target, filterByTk });
options.flowEngine.removeModel(model.uid); await this.flowEngine.context.popover.open({
target,
placement: 'rightTop',
content: (popover) => {
return (
<FlowModelRenderer
fallback={<Skeleton.Input size="small" />}
model={model}
sharedContext={{ currentView: popover }}
extraContext={{ filterByTk }}
/>
);
},
});
} }
async open({ target, filterByTk }: { target: any; filterByTk: string }) { render() {
await this.applyFlow('initial', { filterByTk }); return (
return new Promise((resolve, reject) => { <form
const inputRef = createRef<InputRef>(); style={{ minWidth: '200px' }}
const popover = this.ctx.globals.popover.open({ className="quick-edit-form"
target, onSubmit={async (e) => {
content: ( e.preventDefault();
<form await this.form.submit();
style={{ minWidth: '200px' }} await this.resource.save(
className="quick-edit-form" {
onSubmit={async (e) => { [this.fieldPath]: this.form.values[this.fieldPath],
e.preventDefault(); },
await this.form.submit(); { refresh: false },
await this.resource.save( );
{ this.ctx.shared.currentView.close();
[this.fieldPath]: this.form.values[this.fieldPath], }}
}, >
{ refresh: false }, <FlowEngineProvider engine={this.flowEngine}>
); <FormProvider form={this.form}>
popover.destroy(); <FormLayout layout={'vertical'}>
resolve(this.form.values); {this.mapSubModels('fields', (field) => {
}} return (
> <FlowModelRenderer
<FlowEngineProvider engine={this.flowEngine}> model={field}
<FormProvider form={this.form}> sharedContext={{ currentRecord: this.resource.getData() }}
<FormLayout layout={'vertical'}> fallback={<Skeleton paragraph={{ rows: 2 }} />}
{this.mapSubModels('fields', (field) => { />
return ( );
<FlowModelRenderer })}
model={field} </FormLayout>
sharedContext={{ currentRecord: this.resource.getData() }} <FormButtonGroup align="right">
fallback={<Skeleton paragraph={{ rows: 2 }} />} <Button
/> onClick={() => {
); this.ctx.shared.currentView.close();
})} }}
</FormLayout> >
<FormButtonGroup align="right"> {this.translate('Cancel')}
<Button </Button>
onClick={() => { <Button type="primary" htmlType="submit">
popover.destroy(); {this.translate('Submit')}
reject(null); // 在 close 之后 resolve </Button>
}} </FormButtonGroup>
> </FormProvider>
{this.translate('Cancel')} </FlowEngineProvider>
</Button> </form>
<Button type="primary" htmlType="submit"> );
{this.translate('Submit')}
</Button>
</FormButtonGroup>
</FormProvider>
</FlowEngineProvider>
</form>
),
onRendered: () => {
setTimeout(() => {
// 聚焦 Popover 内第一个 input 或 textarea
const el = document.querySelector(
'.quick-edit-form input, .quick-edit-form textarea, .quick-edit-form select',
) as HTMLInputElement | HTMLTextAreaElement | null;
el?.focus();
}, 200);
// inputRef.current.focus();
},
placement: 'rightTop',
});
});
} }
} }
QuickEditForm.registerFlow({ QuickEditForm.registerFlow({
key: 'initial', key: 'propsFlow',
auto: true,
steps: { steps: {
step1: { step1: {
async handler(ctx, params) { async handler(ctx, params) {

View File

@ -164,7 +164,6 @@ export class TableModel extends DataBlockModel<TableModelStructure> {
try { try {
await QuickEditForm.open({ await QuickEditForm.open({
target: ref.current, target: ref.current,
flowEngine: this.flowEngine,
dataSourceKey: this.collection.dataSourceKey, dataSourceKey: this.collection.dataSourceKey,
collectionName: this.collection.name, collectionName: this.collection.name,
fieldPath: dataIndex, fieldPath: dataIndex,

View File

@ -226,6 +226,7 @@ export class FlowEngine {
if (this.modelClasses.has(name)) { if (this.modelClasses.has(name)) {
console.warn(`FlowEngine: Model class with name '${name}' is already registered and will be overwritten.`); console.warn(`FlowEngine: Model class with name '${name}' is already registered and will be overwritten.`);
} }
(modelClass as typeof FlowModel).flowEngine = this; // 绑定 FlowEngine 实例到 Model 类
this.modelClasses.set(name, modelClass); this.modelClasses.set(name, modelClass);
} }
@ -316,7 +317,7 @@ export class FlowEngine {
if (uid && this.modelInstances.has(uid)) { if (uid && this.modelInstances.has(uid)) {
return this.modelInstances.get(uid) as T; return this.modelInstances.get(uid) as T;
} }
const modelInstance = new (ModelClass as ModelConstructor<T>)({ ...options, flowEngine: this } as any); const modelInstance = new (ModelClass as ModelConstructor<T>)({ ...options } as any);
modelInstance.onInit(options); modelInstance.onInit(options);

View File

@ -39,11 +39,13 @@ const modelMetas = new WeakMap<typeof FlowModel, FlowModelMeta>();
const modelFlows = new WeakMap<typeof FlowModel, Map<string, FlowDefinition>>(); const modelFlows = new WeakMap<typeof FlowModel, Map<string, FlowDefinition>>();
export class FlowModel<Structure extends DefaultStructure = DefaultStructure> { export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
public static flowEngine: FlowEngine;
public readonly uid: string; public readonly uid: string;
public sortIndex: number; public sortIndex: number;
public props: IModelComponentProps = {}; public props: IModelComponentProps = {};
public stepParams: StepParams = {}; public stepParams: StepParams = {};
public flowEngine: FlowEngine; // public flowEngine: FlowEngine;
public parent: ParentFlowModel<Structure>; public parent: ParentFlowModel<Structure>;
public subModels: Structure['subModels']; public subModels: Structure['subModels'];
private _options: FlowModelOptions<Structure>; private _options: FlowModelOptions<Structure>;
@ -72,9 +74,12 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
private observerDispose: () => void; private observerDispose: () => void;
constructor(options: FlowModelOptions<Structure>) { constructor(options: FlowModelOptions<Structure>) {
if (options?.flowEngine?.getModel(options.uid)) { if (!this.flowEngine) {
throw new Error('FlowModel must be initialized with a FlowEngine instance.');
}
if (this.flowEngine.getModel(options.uid)) {
// 此时 new FlowModel 并不创建新实例而是返回已存在的实例避免重复创建同一个model实例 // 此时 new FlowModel 并不创建新实例而是返回已存在的实例避免重复创建同一个model实例
return options.flowEngine.getModel(options.uid); return this.flowEngine.getModel(options.uid);
} }
if (!options.uid) { if (!options.uid) {
@ -85,7 +90,6 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
this.props = options.props || {}; this.props = options.props || {};
this.stepParams = options.stepParams || {}; this.stepParams = options.stepParams || {};
this.subModels = {}; this.subModels = {};
this.flowEngine = options.flowEngine;
this.sortIndex = options.sortIndex || 0; this.sortIndex = options.sortIndex || 0;
this._options = options; this._options = options;
@ -132,6 +136,11 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
return modelMetas.get(this); return modelMetas.get(this);
} }
get flowEngine() {
// 取静态属性 flowEngine
return (this.constructor as typeof FlowModel).flowEngine;
}
private createSubModels(subModels: Record<string, CreateSubModelOptions | CreateSubModelOptions[]>) { private createSubModels(subModels: Record<string, CreateSubModelOptions | CreateSubModelOptions[]>) {
Object.entries(subModels || {}).forEach(([key, value]) => { Object.entries(subModels || {}).forEach(([key, value]) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
@ -162,7 +171,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
* @param {FlowEngine} flowEngine FlowEngine实例 * @param {FlowEngine} flowEngine FlowEngine实例
*/ */
setFlowEngine(flowEngine: FlowEngine): void { setFlowEngine(flowEngine: FlowEngine): void {
this.flowEngine = flowEngine; // this.flowEngine = flowEngine;
} }
static define(meta: FlowModelMeta) { static define(meta: FlowModelMeta) {

View File

@ -40,8 +40,6 @@ export function useDrawer() {
// 支持 content 为函数,传递 currentDrawer // 支持 content 为函数,传递 currentDrawer
const content = typeof config.content === 'function' ? config.content(currentDrawer) : config.content; const content = typeof config.content === 'function' ? config.content(currentDrawer) : config.content;
console.log('useDrawer open', config, content);
const drawer = ( const drawer = (
<DrawerComponent <DrawerComponent
key={`drawer-${uuid}`} key={`drawer-${uuid}`}

View File

@ -13,57 +13,101 @@ import usePatchElement from './usePatchElement';
let uuid = 0; let uuid = 0;
// 独立 PopoverComponent参考 DrawerComponent 实现
const PopoverComponent = React.forwardRef<any, any>(({ afterClose, content, placement, rect, ...props }, ref) => {
const [visible, setVisible] = React.useState(true);
const [config, setConfig] = React.useState({ content, placement, rect, ...props });
React.useImperativeHandle(ref, () => ({
destroy: () => setVisible(false),
update: (newConfig: any) => setConfig((prev) => ({ ...prev, ...newConfig })),
close: (result?: any) => setVisible(false),
}));
// 关闭后触发 afterClose
React.useEffect(() => {
if (!visible) {
afterClose?.();
}
}, [visible, afterClose]);
return (
<Popover
arrow={false}
open={visible}
trigger={['click']}
destroyTooltipOnHide
content={config.content}
placement={config.placement}
getPopupContainer={() => document.body}
onOpenChange={(nextOpen) => {
setVisible(nextOpen);
if (!nextOpen) {
afterClose?.();
}
}}
{...config}
>
<span
style={{
position: 'absolute',
top: (config.rect?.top ?? 0) + window.scrollY,
left: (config.rect?.left ?? 0) + window.scrollX,
width: 0,
height: 0,
}}
/>
</Popover>
);
});
export function usePopover() { export function usePopover() {
const holderRef = React.useRef(null); const holderRef = React.useRef<any>(null);
const open = (config) => { const open = (config) => {
uuid += 1; uuid += 1;
const { target, placement = 'rightTop', content, onRendered, ...rest } = config; const { target, placement = 'rightTop', content, ...rest } = config;
const popoverRef = React.createRef<{ destroy: () => void; update: (config: any) => void }>(); const popoverRef = React.createRef<any>();
// 计算目标位置 // 计算目标位置
const rect = target?.getBoundingClientRect?.() ?? { top: 0, left: 0 }; const rect = target?.getBoundingClientRect?.() ?? { top: 0, left: 0 };
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let closeFunc: (() => void) | undefined; let closeFunc: (() => void) | undefined;
const PopoverComponent = (props) => { let resolvePromise: (value?: any) => void;
const [open, setOpen] = React.useState(true); const promise = new Promise((resolve) => {
React.useEffect(() => { resolvePromise = resolve;
onRendered?.(); });
}, []);
return (
<Popover
arrow={false}
open={open}
trigger={['click']}
destroyTooltipOnHide
onOpenChange={(open) => setOpen(open)}
content={content}
placement={placement}
getPopupContainer={() => document.body}
{...rest}
>
<span
style={{
position: 'absolute',
top: rect.top + window.scrollY,
left: rect.left + window.scrollX,
width: 0,
height: 0,
}}
/>
</Popover>
);
};
const popover = <PopoverComponent key={`popover-${uuid}`} ref={popoverRef} />; // 构造 currentPopover 实例
// eslint-disable-next-line prefer-const const currentPopover = {
destroy: () => popoverRef.current?.destroy(),
update: (newConfig) => popoverRef.current?.update(newConfig),
close: (result?: any) => {
resolvePromise?.(result);
popoverRef.current?.close(result);
},
};
const children = typeof content === 'function' ? content(currentPopover) : content;
const popover = (
<PopoverComponent
key={`popover-${uuid}`}
ref={popoverRef}
content={children}
placement={placement}
rect={rect}
afterClose={() => {
closeFunc?.();
config.onClose?.();
resolvePromise?.(config.result);
}}
{...rest}
/>
);
closeFunc = holderRef.current?.patchElement(popover); closeFunc = holderRef.current?.patchElement(popover);
return { return Object.assign(promise, currentPopover);
destroy: () => closeFunc?.(),
// update: (newConfig) => ... // 可选:实现更新逻辑
};
}; };
const api = React.useMemo(() => ({ open }), []); const api = React.useMemo(() => ({ open }), []);