news 2025/12/25 4:57:21

新一代 Workflow 编辑器Unione Flow Editor :OA 审批流程实现案例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
新一代 Workflow 编辑器Unione Flow Editor :OA 审批流程实现案例

新一代 Workflow 编辑器Unione Flow Editor :OA 审批流程实现案例

Unione Flow Editor 是一款灵活高效的工作流可视化编辑器,支持自定义节点、流程配置与数据联动。本文通过一个完整的 OA 审批流程案例,展示其核心用法,包含编辑器主组件、自定义节点组件及流程数据结构。

一、核心组件与文件结构

案例包含三个核心文件:

  • oa-editor.vue:编辑器主组件,负责节点注册、工具栏配置及流程数据加载
  • oa-node.vue:OA 审批节点自定义组件,定义节点 UI 与展示逻辑
  • oa-data.json:示例流程数据,描述完整 OA 审批流程的节点与连接关系
二、编辑器主组件(oa-editor.vue)

该组件是流程编辑的核心,负责初始化编辑器、注册自定义节点、配置工具栏,并加载流程数据。

<template> <div class="unione-flow-editor"> <UFEditor ref="editorObj" :value="flowChart" :toolbar="toolbar" model="edit"></UFEditor> </div> </template> <script setup lang="ts"> import { UFEditor, registerNode, registerOpts } from 'unione-flow-vue' import type { UFDefine } from 'unione-flow-vue/dist/typing' import { onMounted, ref } from 'vue' import flowJsonData from './oa.json' import Custome from './node/node.vue' import OaApprovalNode from './node/oa.vue' defineOptions({ name: 'DemoIndex', }) /** * 注册自定义节点 */ registerNode({ shape: 'custom', //节点标识 component: Custome, //节点组件 icon: 'AndroidOutlined', //节点图标 width: 200, //节点宽度 height: 90, //节点高度 data: { //节点初始化数据 title: '自定义节点', body: '发起人' }, props: { // 节点属性架构 基础信息-base,高级设置-advanced,流程通知-notice,超时设置-time // 属性path -> 属性控件定义{} // 属性path:false 表示隐藏预设属性 ,eg: time:false 隐藏整个超时设置 // 属性path:{merge:'override'} 如果属性path已存在,并设置合并方式为 override 表示完全覆盖预设属性,不设置表示合并 // 属性path:{parent:'xxx.yyy'} 表示将属性path添加到xxx.yyy属性下面,作为子属性 // 属性path:{after:'xxx.yyy'} 表示将属性path添加到xxx.yyy属性后面,作为兄弟属性 'base.approve.specify': { title: '指定审批人', // 属性标题 name: 'approve.specify', // 指定属性名称,可覆盖预设名称 control: 'a-textarea', // 属性设置控件名称,vue全局注册 after: 'base.approve.handlerType', // 指定当前属性显示位置,在base.approve.handlerType后面显示 props: { // 属性设置控件属性 required: true, help: '通过选定的成员,作为审批人' }, event: { /** * 动态显示/隐藏逻辑 * @param val 属性值 * @param formValue 表单值 * @return true-显示,false-隐藏 */ visible: (val: any, formValue: any) => { // 根据业务逻辑判断是否显示 return formValue.approve?.handlerType === 'specify' }, /** * 动态设置属性标题 * @param val 属性值 * @param formValue 表单值 * @return 返回性标题 */ title: (val: any, formValue: any) => { // 根据业务动态显示属性标题 return '指定审批人' }, /** * 校验属性是否必填 * @param val 属性值 * @param formValue 表单值 * @return true-必填,false-非必填 */ required: (val: any, formValue: any) => { // 根据业务逻辑判断是否必填 return true }, /** * 监听属性值变化 * @param val 属性值 * @param formValue 表单值 */ change: (val: any, formValue: any) => { // 业务处理逻辑 }, /** * 校验指定审批人是否为空 * @param val 属性值 * @param formValue 表单值 * @returns 校验异常信息 false表示校验通过 */ validate: (val: any, formValue: any) => { if (!val) { return '指定审批人不能为空' } // 其他复杂业务逻辑 return false } } }, } }) // 注册OA审批节点类型 const oaNodeTypes = [ { shape: 'oa-initiate', title: '公文发起', icon: 'FormOutlined', data: { title: '公文发起', description: '发起新的公文审批流程', formType: 'dynamic' } }, { shape: 'oa-department', title: '部门审批', icon: 'TeamOutlined', data: { title: '部门审批', approver: '部门经理', deadline: '3个工作日', description: '部门负责人审批', formType: 'dynamic' } }, { shape: 'oa-leader', title: '分管领导审批', icon: 'UserOutlined', data: { title: '分管领导审批', approver: '分管领导', deadline: '5个工作日', description: '分管领导审批', formType: 'dynamic' } }, { shape: 'oa-general-manager', title: '总经理审批', icon: 'UserOutlined', data: { title: '总经理审批', approver: '总经理', deadline: '7个工作日', description: '总经理审批', formType: 'dynamic' } }, { shape: 'oa-finance', title: '财务审批', icon: 'AccountBookOutlined', data: { title: '财务审批', approver: '财务总监', deadline: '3个工作日', description: '财务部门审批', formType: 'dynamic' } }, { shape: 'oa-archive', title: '归档', icon: 'FileDoneOutlined', data: { title: '归档', description: '公文归档保存', formType: 'dynamic' } } ] // 注册所有OA审批节点 oaNodeTypes.forEach(type => { registerNode({ shape: type.shape, component: OaApprovalNode, icon: type.icon, width: 220, height: 113, data: type.data, props: { // 基础信息 'base.approver': { title: '审批人', control: 'a-input', props: { placeholder: '请输入审批人姓名或部门' }, event: { visible: (val: any, formValue: any) => { // 公文发起和归档节点不需要审批人 return ['oa-initiate', 'oa-archive'].indexOf(formValue.shape) === -1 }, required: (val: any, formValue: any) => { return ['oa-initiate', 'oa-archive'].indexOf(formValue.shape) === -1 } } }, 'base.deadline': { title: '审批期限', control: 'a-select', props: { options: [ { value: '1', label: '1个工作日' }, { value: '3', label: '3个工作日' }, { value: '5', label: '5个工作日' }, { value: '7', label: '7个工作日' }, { value: '14', label: '14个工作日' } ] }, event: { visible: (val: any, formValue: any) => { // 公文发起和归档节点不需要审批期限 return ['oa-initiate', 'oa-archive'].indexOf(formValue.shape) === -1 } } }, 'base.description': { title: '节点描述', control: 'a-textarea', props: { placeholder: '请输入节点描述信息', rows: 3 } }, // 高级设置 'advanced.notify.enable': { title: '启用通知', control: 'a-switch', props: { defaultChecked: true } }, 'advanced.notify.type': { title: '通知方式', control: 'a-checkbox-group', props: { options: [ { label: '邮件', value: 'email' }, { label: '短信', value: 'sms' }, { label: '系统消息', value: 'system' } ] }, event: { visible: (val: any, formValue: any) => { return formValue.advanced?.notify?.enable === true } } }, // 流程通知 'notice.approve': { title: '审批通知', control: 'a-textarea', props: { placeholder: '审批通知内容', rows: 3 }, event: { visible: (val: any, formValue: any) => { return ['oa-initiate', 'oa-archive'].indexOf(formValue.shape) === -1 } } }, 'notice.complete': { title: '完成通知', control: 'a-textarea', props: { placeholder: '流程完成通知内容', rows: 3 } } } }) }) /** * 注册节点操作 */ registerOpts({ name: 'custom', //操作名称,和节点标识保持一致 title: '自定义节点', //操作标题 icon: 'AndroidOutlined', //操作图标 color: '#1890ff', //图标颜色 // click:()=>{ //点击事件,默认:添加节点,覆盖后仅触发点击事件 // alert(22) // } }) // 注册OA审批节点操作 const oaNodeColors: Record<string, string> = { 'oa-initiate': '#1890ff', 'oa-department': '#52c41a', 'oa-leader': '#722ed1', 'oa-general-manager': '#fa8c16', 'oa-finance': '#faad14', 'oa-archive': '#8c8c8c' } oaNodeTypes.forEach(type => { registerOpts({ name: type.shape, title: type.title, icon: type.icon, color: oaNodeColors[type.shape] || '#1890ff' }) }) /** * 注册工具栏 */ const toolbar = ref<any>([ { name: 'end', index: 20 }, { widget: 'AndroidOutlined', //工具栏图标 name: 'custom', //操作名称,和节点标识保持一致 title: '自定义节点', //操作标题 location: 'left', //工具栏位置,left-左侧,right-右侧 props: { //工具栏图标属性 style: { color: '#1890ff', } }, }, // OA审批流程工具栏 { widget: 'a-divider', location: 'left', props: { type: 'vertical', } }, ...oaNodeTypes.map(type => ({ widget: type.icon, name: type.shape, title: type.title, location: 'left', props: { style: { color: oaNodeColors[type.shape] || '#1890ff', } } })) ]) /** * 编辑器对象 * 方法介绍: * toJSON: 获取流程图数据 * fromJSON: 加载流程图数据 * getNodes: 获取所有节点 * setActiveNode: 设置当前活动节点 * onActiveNode: 监听当前活动节点变化 * onActiveRoute: 监听当前活动路由变化 * on: 监听事件(event:string,callback) * trigger: 触发事件(event:string,data:any) */ const editorObj = ref() /** * 流程图表数据 */ const flowChart = ref<UFDefine>(flowJsonData) onMounted(() => { }) </script> <style lang="less" scoped> .unione-flow-editor { height: 100%; overflow: hidden; } :deep(.unione-flow-node-opts) { width: 140px; } </style>

核心功能解析

  • 节点注册:通过registerNode方法注册 OA 专属节点,指定节点标识、UI 组件、默认数据及可配置属性
  • 工具栏配置:通过toolbar定义编辑器左侧工具栏,关联 OA 节点,支持一键添加
  • 数据绑定:通过flowChart绑定流程数据,初始化时加载预设的 OA 审批流程
三、自定义 OA 节点组件(oa-node.vue)

定义 OA 节点的 UI 展示逻辑,包括节点头部(图标、标题、状态)和身体(审批人、期限等信息)。

<template> <Node class="unione-flow-node-oa-approval node-box"> <template #default="{ data, node }"> <div class="head" :class="`node-${node.shape}`"> <!-- 直接使用静态图标进行测试 --> <component :is="getNodeIcon(node.shape)" class="icon" /> <span class="title">{{ data.title }}</span> <span v-if="data.status" class="status" :class="`status-${data.status}`">{{ getStatusText(data.status) }}</span> </div> <div class="body" :class="`node-${node.shape}`"> <div v-if="data.approver" class="approver">审批人:{{ data.approver }}</div> <div v-if="data.deadline" class="deadline">截止时间:{{ data.deadline }}</div> <div v-if="data.description" class="description">{{ data.description }}</div> </div> </template> </Node> </template> <script setup lang="ts"> import { Node } from 'unione-flow-vue' import { inject } from 'vue'; import { FormOutlined, TeamOutlined, UserOutlined, AccountBookOutlined, FileDoneOutlined, CheckCircleOutlined } from '@ant-design/icons-vue' defineOptions({ name: 'UnioneFlowNodeOaApproval', }) /** * 获得流程图编辑器对象 */ const flowGraph = inject<Function>('flowGraph') /** * 节点颜色映射 */ const oaNodeColors: Record<string, string> = { 'oa-initiate': '#1890ff', 'oa-department': '#52c41a', 'oa-leader': '#722ed1', 'oa-general-manager': '#fa8c16', 'oa-finance': '#faad14', 'oa-archive': '#8c8c8c' } /** * 根据节点类型获取图标 */ const getNodeIcon = (nodeType: string) => { switch (nodeType) { case 'oa-initiate': return FormOutlined case 'oa-department': return TeamOutlined case 'oa-leader': return UserOutlined case 'oa-general-manager': return UserOutlined case 'oa-finance': return AccountBookOutlined case 'oa-archive': return FileDoneOutlined default: return FormOutlined } } /** * 获取状态文本 */ const getStatusText = (status: string) => { const statusMap: Record<string, string> = { 'pending': '待审批', 'running': '审批中', 'completed': '已完成', 'rejected': '已拒绝', 'backed': '已退回' } return statusMap[status] || '未知状态' } </script> <style lang="less" scoped> .unione-flow-node-oa-approval { width: 220px; background-color: #FFFFFF98; border-radius: 10px; .head { display: flex; flex-direction: row; padding: 5px 10px; background-image: linear-gradient(to right, #1890ff, #096dd9); box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1); border-top: solid 2px transparent; border-top-left-radius: 10px; border-top-right-radius: 10px; height: 32px; /* 添加固定高度 */ align-items: center; /* 确保内容垂直居中 */ .icon { color: #fff; width: 20px; height: 20px; margin-right: 5px; font-weight: bold; } .title { font-size: 13px; font-weight: bold; color: #fff; } } .body { height: 80px; padding: 8px 10px; box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1); border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; font-size: 12px; line-height: 1.5; overflow-y: auto; /* 当内容超出时显示垂直滚动条 */ .approver { color: #333; margin-bottom: 2px; } .deadline { color: #ff4d4f; margin-bottom: 2px; } .description { color: #666; margin-bottom: 2px; } } .head .status { font-size: 11px; padding: 2px 6px; border-radius: 10px; margin-left: 5px; } .head .status-pending { background-color: #faad14; color: #fff; } .head .status-running { background-color: #1890ff; color: #fff; } .head .status-completed { background-color: #52c41a; color: #fff; } .head .status-rejected { background-color: #f5222d; color: #fff; } .head .status-backed { background-color: #fa8c16; color: #fff; } // OA节点类型样式 .head.node-oa-initiate { background-image: linear-gradient(to right, #1890ff, #096dd9); border-left: solid 2px #1890ff; border-right: solid 2px #096dd9; } .body.node-oa-initiate { border-left: solid 2px #1890ff; border-right: solid 2px #096dd9; border-bottom: solid 2px #096dd9; } .head.node-oa-department { background-image: linear-gradient(to right, #52c41a, #389e0d); border-left: solid 2px #52c41a; border-right: solid 2px #389e0d; } .body.node-oa-department { border-left: solid 2px #52c41a; border-right: solid 2px #389e0d; border-bottom: solid 2px #389e0d; } .head.node-oa-leader { background-image: linear-gradient(to right, #722ed1, #531dab); border-left: solid 2px #722ed1; border-right: solid 2px #531dab; } .body.node-oa-leader { border-left: solid 2px #722ed1; border-right: solid 2px #531dab; border-bottom: solid 2px #531dab; } .head.node-oa-general-manager { background-image: linear-gradient(to right, #fa8c16, #d46b08); border-left: solid 2px #fa8c16; border-right: solid 2px #d46b08; } .body.node-oa-general-manager { border-left: solid 2px #fa8c16; border-right: solid 2px #d46b08; border-bottom: solid 2px #d46b08; } .head.node-oa-finance { background-image: linear-gradient(to right, #faad14, #d48806); border-left: solid 2px #faad14; border-right: solid 2px #d48806; } .body.node-oa-finance { border-left: solid 2px #faad14; border-right: solid 2px #d48806; border-bottom: solid 2px #d48806; } .head.node-oa-archive { background-image: linear-gradient(to right, #8c8c8c, #595959); border-left: solid 2px #8c8c8c; border-right: solid 2px #595959; } .body.node-oa-archive { border-left: solid 2px #8c8c8c; border-right: solid 2px #595959; border-bottom: solid 2px #595959; } } </style>

核心功能解析

  • 动态图标:通过getNodeIcon根据节点类型(如oa-department)显示对应图标
  • 状态展示:支持显示审批状态(待审批 / 审批中 / 已完成等),并通过样式区分
  • 差异化样式:不同节点类型(如部门审批、财务审批)使用专属渐变颜色,直观区分节点角色
四、流程数据结构(oa-data.json)

描述完整 OA 审批流程的节点信息与连接关系,可直接被编辑器加载。

{ "nodes": [ { "data": { "title": "开始", "sn": "start" }, "sn": "start", "types": "start", "title": "开始", "attr": { "parent": "group1", "position": { "x": 69.99999999999977, "y": 161.5 }, "size": { "width": 80, "height": 30 } } }, { "data": { "title": "公文发起", "description": "发起一个新的公文审批流程", "formType": "dynamic", "sn": "node-1" }, "sn": "node-1", "types": "oa-initiate", "title": "公文发起", "attr": { "position": { "x": 220, "y": 120 }, "size": { "width": 220, "height": 113 } } }, { "data": { "title": "部门审批", "approver": "部门经理", "deadline": "3个工作日", "description": "部门经理审批公文内容", "formType": "dynamic", "sn": "node-2" }, "sn": "node-2", "types": "oa-department", "title": "部门审批", "attr": { "position": { "x": 510, "y": 120 }, "size": { "width": 220, "height": 113 } } }, { "data": { "title": "领导审批", "approver": "部门总监", "deadline": "2个工作日", "description": "部门总监审核公文", "formType": "dynamic", "sn": "node-3" }, "sn": "node-3", "types": "oa-leader", "title": "领导审批", "attr": { "position": { "x": 810, "y": 120 }, "size": { "width": 220, "height": 113 } } }, { "data": { "title": "总经理审批", "approver": "总经理", "deadline": "5个工作日", "description": "总经理最终审批公文", "formType": "dynamic", "sn": "node-4" }, "sn": "node-4", "types": "oa-general-manager", "title": "总经理审批", "attr": { "position": { "x": 1150, "y": 120 }, "size": { "width": 220, "height": 113 } } }, { "data": { "title": "财务审批", "approver": "财务经理", "deadline": "3个工作日", "description": "财务部门审核费用相关内容", "formType": "dynamic", "sn": "node-5" }, "sn": "node-5", "types": "oa-finance", "title": "财务审批", "attr": { "position": { "x": 810, "y": 320 }, "size": { "width": 220, "height": 113 } } }, { "data": { "title": "归档", "description": "审批完成后归档公文", "formType": "dynamic", "sn": "node-6" }, "sn": "node-6", "types": "oa-archive", "title": "归档", "attr": { "position": { "x": 1150, "y": 320 }, "size": { "width": 220, "height": 113 } } }, { "data": { "title": "结束", "sn": "d89c29cc-ab3c-4895-9f4e-beaca5a78e73" }, "sn": "d89c29cc-ab3c-4895-9f4e-beaca5a78e73", "types": "end", "title": "结束", "attr": { "position": { "x": 1220, "y": 540 }, "size": { "width": 80, "height": 30 } } } ], "routes": [ { "data": { "title": "发起 -> 部门审批", "sn": "route-1" }, "sn": "route-1", "title": "发起 -> 部门审批", "attr": { "source": { "cell": "node-1", "port": "right" }, "target": { "cell": "node-2", "port": "left" } } }, { "data": { "title": "部门审批 -> 领导审批", "sn": "route-2" }, "sn": "route-2", "title": "部门审批 -> 领导审批", "attr": { "source": { "cell": "node-2", "port": "right" }, "target": { "cell": "node-3", "port": "left" } } }, { "data": { "title": "领导审批 -> 总经理审批", "sn": "route-3" }, "sn": "route-3", "title": "领导审批 -> 总经理审批", "attr": { "source": { "cell": "node-3", "port": "right" }, "target": { "cell": "node-4", "port": "left" } } }, { "data": { "title": "领导审批 -> 财务审批", "sn": "route-4" }, "sn": "route-4", "title": "领导审批 -> 财务审批", "attr": { "source": { "cell": "node-3", "port": "bottom" }, "target": { "cell": "node-5", "port": "top" } } }, { "data": { "title": "总经理审批 -> 归档", "sn": "route-5" }, "sn": "route-5", "title": "总经理审批 -> 归档", "attr": { "source": { "cell": "node-4", "port": "bottom" }, "target": { "cell": "node-6", "port": "top" } } }, { "data": { "title": "财务审批 -> 归档", "sn": "route-6" }, "sn": "route-6", "title": "财务审批 -> 归档", "attr": { "source": { "cell": "node-5", "port": "right" }, "target": { "cell": "node-6", "port": "left" } } }, { "sn": "b20aa483-961a-475b-86f8-6930b701e857", "attr": { "source": { "cell": "node-6", "port": "bottom" }, "target": { "cell": "d89c29cc-ab3c-4895-9f4e-beaca5a78e73", "port": "top" } } }, { "sn": "b8fb5a28-d066-4226-a103-5085dec4c116", "attr": { "source": { "cell": "start", "port": "right" }, "target": { "cell": "node-1", "port": "left" } } } ], "setting": { "title": "{发起用户名}的{流程名称}" } }

数据结构解析

  • nodes:数组,包含所有节点信息,每个节点通过types关联注册的节点类型
  • routes:数组,描述节点间的连接关系,通过sourcetarget指定连接的节点与端口
  • setting:流程全局配置,如流程标题
五、总结

通过 Unione Flow Editor 实现 OA 审批流程的核心优势:

  1. 高度自定义:支持自定义节点 UI、属性配置及交互逻辑
  2. 可视化编辑:通过工具栏快速添加节点,拖拽连接流程,降低使用门槛
  3. 数据驱动:流程数据与 UI 分离,便于存储、传输与二次加工

如需扩展更多节点类型(如 "人事审批"),只需新增节点配置并注册,即可快速扩展流程能力。

项目地址github:https://github.com/unione-cloud/unione-flow-editor

项目地址gitee:https://gitee.com/unione-cloud/unione-flow-editor

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/15 23:13:28

基于FLUX.1-dev的AI艺术创作平台搭建全记录

基于FLUX.1-dev的AI艺术创作平台搭建全记录 在数字艺术创作的前沿&#xff0c;我们正经历一场由生成式AI驱动的范式变革。过去几年里&#xff0c;从Stable Diffusion到DALLE系列&#xff0c;文生图模型不断刷新人们对“机器创造力”的认知边界。然而&#xff0c;真正能将创意意…

作者头像 李华
网站建设 2025/12/24 1:02:36

NCM格式转换终极指南:3步解锁网易云音乐加密文件

还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗&#xff1f;ncmdump工具正是你需要的解决方案&#xff01;这款轻量级工具能够快速将NCM加密文件转换为通用音频格式&#xff0c;让你的音乐库真正实现跨平台自由流通&#x1f3b5; 【免费下载链接】ncmdump …

作者头像 李华
网站建设 2025/12/24 20:55:27

力扣300

/* dp[n]&#xff1a;以第n个元素结尾的最大子序列的值 所以说dp[n]应该与前面的所有dp[n-1]--dp[0]都与有关&#xff0c;从里面选出一个最大的dp&#xff0c;然后 加上n的本身&#xff08;如果nums[n]大的话&#xff09; */ class Solution { public:int lengthOfLIS(vector&l…

作者头像 李华
网站建设 2025/12/15 23:11:02

3.6B活跃参数的秘密:解密GPT-OSS-20B的高效推理机制

3.6B活跃参数的秘密&#xff1a;解密GPT-OSS-20B的高效推理机制 在一台仅配备16GB内存的普通笔记本上&#xff0c;运行一个总参数达210亿的语言模型——这听起来像是天方夜谭。然而&#xff0c;GPT-OSS-20B 正是这样一款打破常规的开源模型&#xff0c;它不仅做到了&#xff0c…

作者头像 李华
网站建设 2025/12/24 19:49:35

收藏必备!智能体工程:解决大模型“上线秒变智障“的终极指南

智能体工程是通过"构建、测试、上线、观察、优化、重复"的循环迭代&#xff0c;将不稳定的大模型系统打磨成生产级可靠应用的方法论。它需要产品思维、工程能力和数据科学三种能力配合&#xff0c;与传统软件开发不同之处在于强调上线是为了学习而非完美。成功的团队…

作者头像 李华
网站建设 2025/12/25 3:04:23

必收藏!RAG知识库实战指南:AI产品经理如何构建高质量知识库?

本文详解RAG知识库构建与管理&#xff0c;强调知识库质量决定AI产品成败。阐述四大核心要素&#xff1a;内容权威性、语义完整性、结构化与元数据丰富、动态可维护性。通过银行智能客服案例展示优化效果&#xff0c;给出从最小可行知识集开始、监控检索失败率等行动建议&#x…

作者头像 李华