本文档定义了DrPlayer中用于渲染T4 Action交互功能的自定义页面组件技术规范。这些组件将根据后端返回的Action JSON配置动态生成相应的UI界面,实现灵活的用户交互体验。
ActionRenderer (主渲染器)
├── ActionDialog (弹窗容器)
│ ├── InputAction (单项输入组件)
│ ├── EditAction (多行编辑组件)
│ ├── MultiInputAction (多项输入组件)
│ ├── MenuAction (单选菜单组件)
│ ├── SelectAction (多选菜单组件)
│ ├── MsgBoxAction (消息弹窗组件)
│ ├── WebViewAction (网页视图组件)
│ └── HelpAction (帮助页面组件)
├── ActionButton (动作按钮组件)
└── ActionToast (提示消息组件)
Action JSON → ActionRenderer → 具体Action组件 → 用户交互 → 回调处理 → API调用
职责:
- 解析Action JSON配置
- 根据type字段路由到对应的组件
- 管理组件的生命周期
- 处理专项动作
Props接口:
interface ActionRendererProps {
actionData: string | ActionConfig; // Action JSON字符串或对象
onAction: (action: string, value: any) => Promise<string>; // 动作回调
onClose?: () => void; // 关闭回调
}
interface ActionConfig {
actionId: string;
type: ActionType;
[key: string]: any;
}
type ActionType = 'input' | 'edit' | 'multiInput' | 'multiInputX' | 'menu' | 'select' | 'msgbox' | 'webview' | 'help';核心方法:
class ActionRenderer {
// 解析Action配置
parseActionConfig(data: string | ActionConfig): ActionConfig;
// 渲染对应组件
renderActionComponent(config: ActionConfig): React.ReactNode;
// 处理专项动作
handleSpecialAction(actionId: string, config: ActionConfig): void;
// 执行动作回调
executeAction(action: string, value: any): Promise<void>;
}职责:
- 提供统一的弹窗容器
- 处理弹窗的显示/隐藏逻辑
- 管理弹窗的尺寸和位置
- 处理背景遮罩和点击外部关闭
Props接口:
interface ActionDialogProps {
visible: boolean;
title?: string;
width?: number;
height?: number;
bottom?: number;
canceledOnTouchOutside?: boolean;
dimAmount?: number;
onClose: () => void;
children: React.ReactNode;
}样式规范:
.action-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
z-index: 1000;
}
.action-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
}职责:
- 渲染单项输入界面
- 处理输入验证
- 支持快速选择选项
- 显示图片和二维码
- 处理图片点击坐标
Props接口:
interface InputActionProps {
config: InputActionConfig;
onSubmit: (value: any) => void;
onCancel?: () => void;
}
interface InputActionConfig {
actionId: string;
id: string;
title: string;
tip: string;
value?: string;
msg?: string;
width?: number;
button?: number;
selectData?: string;
imageUrl?: string;
imageHeight?: number;
imageClickCoord?: boolean;
qrcode?: string;
qrcodeSize?: string;
timeout?: number;
keep?: boolean;
help?: string;
}核心功能:
class InputAction {
// 解析快速选择数据
parseSelectData(selectData: string): SelectOption[];
// 处理图片点击坐标
handleImageClick(event: MouseEvent): void;
// 生成二维码
generateQRCode(url: string, size: string): string;
// 验证输入值
validateInput(value: string): boolean;
// 处理超时
handleTimeout(timeout: number): void;
}职责:
- 渲染多项输入界面
- 支持不同类型的输入控件
- 处理文件选择、日期选择等特殊输入
- 验证所有输入项
Props接口:
interface MultiInputActionProps {
config: MultiInputActionConfig;
onSubmit: (values: Record<string, any>) => void;
onCancel?: () => void;
}
interface MultiInputActionConfig {
actionId: string;
type: 'multiInput' | 'multiInputX';
title: string;
width?: number;
height?: number;
msg?: string;
button?: number;
input: InputItem[];
}
interface InputItem {
id: string;
name: string;
tip: string;
value?: string;
selectData?: string;
inputType?: number;
multiLine?: number;
validation?: string;
help?: string;
quickSelect?: boolean;
onlyQuickSelect?: boolean;
multiSelect?: boolean;
selectWidth?: number;
selectColumn?: number;
}特殊输入类型处理:
class MultiInputAction {
// 文件夹选择
handleFolderSelect(itemId: string): void;
// 文件选择
handleFileSelect(itemId: string): void;
// 日期选择
handleDateSelect(itemId: string): void;
// 图像选择并转换为Base64
handleImageSelect(itemId: string): void;
// 多选处理
handleMultiSelect(itemId: string, options: string[]): void;
// 输入验证
validateItem(item: InputItem, value: string): boolean;
}职责:
- 渲染单选菜单界面
- 支持多列布局
- 处理选项选择
Props接口:
interface MenuActionProps {
config: MenuActionConfig;
onSelect: (action: string) => void;
onCancel?: () => void;
}
interface MenuActionConfig {
actionId: string;
title: string;
width?: number;
column?: number;
option: MenuOption[];
selectedIndex?: number;
}
interface MenuOption {
name: string;
action: string;
}职责:
- 渲染多选菜单界面
- 管理选中状态
- 支持全选/反选功能
Props接口:
interface SelectActionProps {
config: SelectActionConfig;
onSubmit: (selectedActions: string[]) => void;
onCancel?: () => void;
}
interface SelectActionConfig {
actionId: string;
title: string;
width?: number;
column?: number;
option: SelectOption[];
}
interface SelectOption {
name: string;
action: string;
selected?: boolean;
}职责:
- 显示消息内容
- 支持HTML格式消息
- 显示图片
Props接口:
interface MsgBoxActionProps {
config: MsgBoxActionConfig;
onClose: () => void;
}
interface MsgBoxActionConfig {
actionId: string;
title: string;
msg?: string;
htmlMsg?: string;
imageUrl?: string;
}职责:
- 嵌入网页内容
- 处理网页交互
- 管理缩放和滚动
Props接口:
interface WebViewActionProps {
config: WebViewActionConfig;
onClose: () => void;
}
interface WebViewActionConfig {
actionId: string;
url: string;
height?: number;
textZoom?: number;
}interface ActionState {
currentAction: ActionConfig | null;
isVisible: boolean;
isLoading: boolean;
error: string | null;
history: ActionConfig[]; // 动作历史,支持返回
}class ActionStateManager {
private state: ActionState;
private listeners: ((state: ActionState) => void)[];
// 显示动作
showAction(config: ActionConfig): void;
// 隐藏动作
hideAction(): void;
// 执行动作
executeAction(action: string, value: any): Promise<void>;
// 返回上一个动作
goBack(): void;
// 清空历史
clearHistory(): void;
// 订阅状态变化
subscribe(listener: (state: ActionState) => void): () => void;
}:root {
/* 颜色 */
--action-primary: #1890ff;
--action-success: #52c41a;
--action-warning: #faad14;
--action-error: #f5222d;
--action-text: #262626;
--action-text-secondary: #8c8c8c;
--action-border: #d9d9d9;
--action-background: #ffffff;
--action-background-light: #fafafa;
/* 尺寸 */
--action-border-radius: 6px;
--action-padding: 16px;
--action-margin: 8px;
/* 阴影 */
--action-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
--action-shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.15);
}/* 输入框 */
.action-input {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--action-border);
border-radius: var(--action-border-radius);
font-size: 14px;
transition: border-color 0.3s;
}
.action-input:focus {
border-color: var(--action-primary);
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
/* 按钮 */
.action-button {
padding: 8px 16px;
border: 1px solid var(--action-border);
border-radius: var(--action-border-radius);
background: var(--action-background);
cursor: pointer;
transition: all 0.3s;
}
.action-button-primary {
background: var(--action-primary);
border-color: var(--action-primary);
color: white;
}
.action-button:hover {
box-shadow: var(--action-shadow-hover);
}
/* 选项 */
.action-option {
padding: 12px;
border: 1px solid var(--action-border);
border-radius: var(--action-border-radius);
cursor: pointer;
transition: all 0.3s;
}
.action-option:hover {
background: var(--action-background-light);
border-color: var(--action-primary);
}
.action-option-selected {
background: var(--action-primary);
border-color: var(--action-primary);
color: white;
}interface ActionAPI {
// 执行动作
executeAction(action: string, value: any): Promise<ActionResponse>;
}
interface ActionResponse {
success: boolean;
data?: any;
action?: ActionConfig; // 新的动作配置
toast?: string; // 提示消息
error?: string;
}interface FileAPI {
// 选择文件夹
selectFolder(): Promise<string>;
// 选择文件
selectFile(accept?: string): Promise<File>;
// 选择图片并转换为Base64
selectImageAsBase64(): Promise<string>;
}enum ActionErrorType {
PARSE_ERROR = 'PARSE_ERROR',
VALIDATION_ERROR = 'VALIDATION_ERROR',
NETWORK_ERROR = 'NETWORK_ERROR',
TIMEOUT_ERROR = 'TIMEOUT_ERROR',
USER_CANCEL = 'USER_CANCEL'
}
interface ActionError {
type: ActionErrorType;
message: string;
details?: any;
}class ActionErrorHandler {
// 处理解析错误
handleParseError(error: Error): void;
// 处理验证错误
handleValidationError(field: string, message: string): void;
// 处理网络错误
handleNetworkError(error: Error): void;
// 处理超时错误
handleTimeoutError(): void;
// 显示错误提示
showError(error: ActionError): void;
}// 动态导入组件
const InputAction = lazy(() => import('./InputAction'));
const MultiInputAction = lazy(() => import('./MultiInputAction'));
// ...
// 使用Suspense包装
<Suspense fallback={<Loading />}>
<ActionRenderer />
</Suspense>对于大量选项的菜单组件,使用虚拟滚动优化性能:
import { FixedSizeList as List } from 'react-window';
const VirtualMenuAction = ({ options, onSelect }) => {
const Row = ({ index, style }) => (
<div style={style} onClick={() => onSelect(options[index])}>
{options[index].name}
</div>
);
return (
<List
height={300}
itemCount={options.length}
itemSize={40}
>
{Row}
</List>
);
};对于输入组件,使用防抖优化性能:
import { debounce } from 'lodash';
const debouncedValidation = debounce((value: string) => {
validateInput(value);
}, 300);describe('ActionRenderer', () => {
test('should parse action config correctly', () => {
const config = '{"actionId":"test","type":"input"}';
const result = ActionRenderer.parseActionConfig(config);
expect(result.actionId).toBe('test');
expect(result.type).toBe('input');
});
test('should handle invalid JSON gracefully', () => {
const config = 'invalid json';
expect(() => ActionRenderer.parseActionConfig(config)).toThrow();
});
});describe('Action Integration', () => {
test('should complete input action flow', async () => {
const mockOnAction = jest.fn().mockResolvedValue('success');
const { getByTestId } = render(
<ActionRenderer
actionData='{"actionId":"test","type":"input","tip":"test"}'
onAction={mockOnAction}
/>
);
const input = getByTestId('action-input');
fireEvent.change(input, { target: { value: 'test value' } });
const submitButton = getByTestId('action-submit');
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockOnAction).toHaveBeenCalledWith('test', { test: 'test value' });
});
});
});// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
'action-components': [
'./src/components/actions/InputAction',
'./src/components/actions/MultiInputAction',
// ...
]
}
}
}
}
};// 路由级别的代码分割
const ActionPage = lazy(() => import('./pages/ActionPage'));
// 组件级别的代码分割
const ActionRenderer = lazy(() => import('./components/ActionRenderer'));通过以上技术规范,开发团队可以构建出功能完整、性能优良、用户体验良好的Action交互组件系统。