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 _ from 'lodash';
import React, { createRef } from 'react';
import { SkeletonFallback } from '../../../components/SkeletonFallback';
import { DataBlockModel } from '../../base/BlockModel';
export class QuickEditForm extends DataBlockModel {
form: Form;
fieldPath: string;
declare resource: SingleRecordResource;
declare collection: Collection;
static async open(options: {
target: any;
flowEngine: FlowEngine;
dataSourceKey: string;
collectionName: string;
fieldPath: string;
filterByTk: string;
}) {
const { flowEngine, dataSourceKey, collectionName, fieldPath, target, filterByTk } = options;
const model = flowEngine.createModel({
const { target, dataSourceKey, collectionName, fieldPath, filterByTk } = options;
const model = this.flowEngine.createModel({
use: 'QuickEditForm',
stepParams: {
initial: {
propsFlow: {
step1: {
dataSourceKey,
collectionName,
@ -52,17 +53,25 @@ export class QuickEditForm extends DataBlockModel {
},
},
}) 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 }) {
await this.applyFlow('initial', { filterByTk });
return new Promise((resolve, reject) => {
const inputRef = createRef<InputRef>();
const popover = this.ctx.globals.popover.open({
target,
content: (
render() {
return (
<form
style={{ minWidth: '200px' }}
className="quick-edit-form"
@ -75,8 +84,7 @@ export class QuickEditForm extends DataBlockModel {
},
{ refresh: false },
);
popover.destroy();
resolve(this.form.values);
this.ctx.shared.currentView.close();
}}
>
<FlowEngineProvider engine={this.flowEngine}>
@ -95,8 +103,7 @@ export class QuickEditForm extends DataBlockModel {
<FormButtonGroup align="right">
<Button
onClick={() => {
popover.destroy();
reject(null); // 在 close 之后 resolve
this.ctx.shared.currentView.close();
}}
>
{this.translate('Cancel')}
@ -108,25 +115,13 @@ export class QuickEditForm extends DataBlockModel {
</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({
key: 'initial',
key: 'propsFlow',
auto: true,
steps: {
step1: {
async handler(ctx, params) {

View File

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

View File

@ -226,6 +226,7 @@ export class FlowEngine {
if (this.modelClasses.has(name)) {
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);
}
@ -316,7 +317,7 @@ export class FlowEngine {
if (uid && this.modelInstances.has(uid)) {
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);

View File

@ -39,11 +39,13 @@ const modelMetas = new WeakMap<typeof FlowModel, FlowModelMeta>();
const modelFlows = new WeakMap<typeof FlowModel, Map<string, FlowDefinition>>();
export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
public static flowEngine: FlowEngine;
public readonly uid: string;
public sortIndex: number;
public props: IModelComponentProps = {};
public stepParams: StepParams = {};
public flowEngine: FlowEngine;
// public flowEngine: FlowEngine;
public parent: ParentFlowModel<Structure>;
public subModels: Structure['subModels'];
private _options: FlowModelOptions<Structure>;
@ -72,9 +74,12 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
private observerDispose: () => void;
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实例
return options.flowEngine.getModel(options.uid);
return this.flowEngine.getModel(options.uid);
}
if (!options.uid) {
@ -85,7 +90,6 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
this.props = options.props || {};
this.stepParams = options.stepParams || {};
this.subModels = {};
this.flowEngine = options.flowEngine;
this.sortIndex = options.sortIndex || 0;
this._options = options;
@ -132,6 +136,11 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
return modelMetas.get(this);
}
get flowEngine() {
// 取静态属性 flowEngine
return (this.constructor as typeof FlowModel).flowEngine;
}
private createSubModels(subModels: Record<string, CreateSubModelOptions | CreateSubModelOptions[]>) {
Object.entries(subModels || {}).forEach(([key, value]) => {
if (Array.isArray(value)) {
@ -162,7 +171,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
* @param {FlowEngine} flowEngine FlowEngine实例
*/
setFlowEngine(flowEngine: FlowEngine): void {
this.flowEngine = flowEngine;
// this.flowEngine = flowEngine;
}
static define(meta: FlowModelMeta) {

View File

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

View File

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