feat: 移动端 uniapp 流程表单嵌入页面
This commit is contained in:
@@ -109,6 +109,21 @@ const coreRoutes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 用于 bpm 移动端流程表单 web-view 的嵌入
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
component: () => import('#/views/bpm/form/mobile/index.vue'),
|
||||||
|
meta: {
|
||||||
|
hideInBreadcrumb: true,
|
||||||
|
hideInMenu: true,
|
||||||
|
hideInTab: true,
|
||||||
|
ignoreAccess: true,
|
||||||
|
title: '移动端流程表单展示',
|
||||||
|
},
|
||||||
|
name: 'BpmMobileFormPreview',
|
||||||
|
path: '/bpm/mobile/form-preview',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export { coreRoutes, fallbackNotFoundRoute };
|
export { coreRoutes, fallbackNotFoundRoute };
|
||||||
|
|||||||
405
apps/web-antd/src/views/bpm/form/mobile/index.vue
Normal file
405
apps/web-antd/src/views/bpm/form/mobile/index.vue
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 移动端流程表单展示页面 - Ant Design Vue 版本
|
||||||
|
* 使用 @form-create/ant-design-vue 渲染表单
|
||||||
|
* 用于 UniApp 通过 iframe/webview 嵌入
|
||||||
|
*
|
||||||
|
* URL 参数说明:
|
||||||
|
* - type: 环境类型(必填)'miniapp' 小程序(微信/支付宝/百度等) | 'h5' H5
|
||||||
|
* - processInstanceId: 流程实例ID(查看已有流程时使用)
|
||||||
|
* - taskId: 任务ID(可选)
|
||||||
|
* - activityId: 活动节点ID(可选)
|
||||||
|
* - token: 访问令牌(用于 API 认证)
|
||||||
|
*/
|
||||||
|
import { computed, nextTick, onMounted, ref, toRaw } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
import { BpmFieldPermissionType, BpmModelFormType } from '@vben/constants';
|
||||||
|
import { updatePreferences } from '@vben/preferences';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { Button, Empty, Spin } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getApprovalDetail } from '#/api/bpm/processInstance';
|
||||||
|
import { setConfAndFields2 } from '#/components/form-create';
|
||||||
|
|
||||||
|
type EnvType = 'h5' | 'miniapp'; // 环境类型
|
||||||
|
|
||||||
|
// UniApp WebView 类型声明
|
||||||
|
interface UniWebView {
|
||||||
|
postMessage: (options: { data: any }) => void;
|
||||||
|
getEnv: (callback: (res: any) => void) => void;
|
||||||
|
navigateTo: (options: {
|
||||||
|
fail?: () => void;
|
||||||
|
success?: () => void;
|
||||||
|
url: string;
|
||||||
|
}) => void;
|
||||||
|
navigateBack: (options?: { delta?: number }) => void;
|
||||||
|
switchTab: (options: { url: string }) => void;
|
||||||
|
reLaunch: (options: { url: string }) => void;
|
||||||
|
redirectTo: (options: { url: string }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
uni?: UniWebView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ name: 'BpmMobileFormPreview' });
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
|
||||||
|
const envType = ref<EnvType>('h5'); // 环境类型
|
||||||
|
const loading = ref(true); // 页面加载状态
|
||||||
|
const error = ref<null | string>(null);
|
||||||
|
|
||||||
|
const processInstance = ref<any>(null);
|
||||||
|
const processDefinition = ref<any>(null);
|
||||||
|
|
||||||
|
const detailForm = ref<{
|
||||||
|
option: any;
|
||||||
|
rule: any[];
|
||||||
|
value: Record<string, any>;
|
||||||
|
}>({
|
||||||
|
option: {},
|
||||||
|
rule: [],
|
||||||
|
value: {},
|
||||||
|
}); // 流程实例的表单详情
|
||||||
|
const fApi = ref<any>(null); // form-create API 引用
|
||||||
|
const fieldPermissions = ref<Record<string, string>>({}); // 字段权限
|
||||||
|
|
||||||
|
// 是否有表单内容
|
||||||
|
const hasFormContent = computed(() => {
|
||||||
|
return detailForm.value.rule && detailForm.value.rule.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 Token
|
||||||
|
* 从 URL 参数获取 token 并设置到 store
|
||||||
|
*/
|
||||||
|
function initToken() {
|
||||||
|
const token = route.query.token as string;
|
||||||
|
if (token) {
|
||||||
|
accessStore.setAccessToken(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证并初始化环境类型
|
||||||
|
*/
|
||||||
|
function initEnvType(): boolean {
|
||||||
|
const type = route.query.type as string;
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
error.value = '缺少必填参数: type';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type !== 'h5' && type !== 'miniapp') {
|
||||||
|
error.value = 'type 参数值无效,必须是 h5 或 miniapp';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
envType.value = type as EnvType;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取审批详情
|
||||||
|
*/
|
||||||
|
async function getDetail() {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const processInstanceId = route.query.processInstanceId as string;
|
||||||
|
const taskId = route.query.taskId as string;
|
||||||
|
const activityId = route.query.activityId as string;
|
||||||
|
|
||||||
|
if (!processInstanceId) {
|
||||||
|
throw new Error('缺少流程实例ID参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await getApprovalDetail({
|
||||||
|
processInstanceId,
|
||||||
|
taskId,
|
||||||
|
activityId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
throw new Error('查询不到审批详情信息');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.processDefinition || !data.processInstance) {
|
||||||
|
throw new Error('查询不到流程信息');
|
||||||
|
}
|
||||||
|
|
||||||
|
processInstance.value = data.processInstance;
|
||||||
|
processDefinition.value = data.processDefinition;
|
||||||
|
|
||||||
|
// 设置普通表单信息
|
||||||
|
if (data.processDefinition.formType === BpmModelFormType.NORMAL) {
|
||||||
|
if (detailForm.value.rule?.length > 0) {
|
||||||
|
// 避免刷新 form-create 显示不了
|
||||||
|
detailForm.value.value = processInstance.value.formVariables;
|
||||||
|
} else {
|
||||||
|
setConfAndFields2(
|
||||||
|
detailForm,
|
||||||
|
processDefinition.value.formConf,
|
||||||
|
processDefinition.value.formFields,
|
||||||
|
processInstance.value.formVariables,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
fApi.value?.btn.show(false);
|
||||||
|
fApi.value?.resetBtn.show(false);
|
||||||
|
fApi.value?.disabled(true);
|
||||||
|
// 设置表单字段权限
|
||||||
|
if (data.formFieldsPermission) {
|
||||||
|
Object.keys(data.formFieldsPermission).forEach((item) => {
|
||||||
|
setFieldPermission(item, data.formFieldsPermission[item]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向父页面发送消息
|
||||||
|
* 根据环境类型选择不同的通信方式
|
||||||
|
*/
|
||||||
|
function postMessageToParent(message: { data: any; type: string }) {
|
||||||
|
const messageData = {
|
||||||
|
source: 'bpm-mobile-form',
|
||||||
|
type: message.type,
|
||||||
|
data: message.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 小程序环境:使用 uni.postMessage
|
||||||
|
if (envType.value === 'miniapp') {
|
||||||
|
if (window.uni?.postMessage) {
|
||||||
|
// 传递的消息信息,必须写在 data 对象中
|
||||||
|
window.uni.postMessage({ data: message.data });
|
||||||
|
} else {
|
||||||
|
console.error('小程序环境下 uni 对象未定义');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// H5 环境:使用 window.postMessage
|
||||||
|
if (envType.value === 'h5' && window.parent !== window) {
|
||||||
|
window.parent.postMessage(messageData, '*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全地克隆对象,移除不可序列化的属性
|
||||||
|
*/
|
||||||
|
function safeClone(obj: any): any {
|
||||||
|
try {
|
||||||
|
// 先使用 toRaw 移除 Vue 的响应式代理
|
||||||
|
const raw = toRaw(obj);
|
||||||
|
// 使用 JSON 序列化来移除函数、DOM 元素等不可序列化的内容
|
||||||
|
// eslint-disable-next-line unicorn/prefer-structured-clone
|
||||||
|
return JSON.parse(JSON.stringify(raw));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('克隆对象失败:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置表单权限 */
|
||||||
|
function setFieldPermission(field: string, permission: string) {
|
||||||
|
fieldPermissions.value[field] = permission;
|
||||||
|
if (permission === BpmFieldPermissionType.READ) {
|
||||||
|
fApi.value?.disabled(true, field);
|
||||||
|
}
|
||||||
|
if (permission === BpmFieldPermissionType.WRITE) {
|
||||||
|
fApi.value?.disabled(false, field);
|
||||||
|
}
|
||||||
|
if (permission === BpmFieldPermissionType.NONE) {
|
||||||
|
fApi.value?.hidden(true, field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确定按钮点击事件
|
||||||
|
* 获取表单数据并发送给父页面
|
||||||
|
*/
|
||||||
|
function handleConfirm() {
|
||||||
|
// 获取最新的表单值(转换为普通对象,避免 Proxy 序列化问题)
|
||||||
|
const rawValue = detailForm.value.value;
|
||||||
|
const currentValue = safeClone(rawValue);
|
||||||
|
|
||||||
|
// 发送表单数据给父页面
|
||||||
|
postMessageToParent({
|
||||||
|
type: 'FORM_SUBMIT',
|
||||||
|
data: {
|
||||||
|
formValue: currentValue,
|
||||||
|
fieldPermissions: safeClone(fieldPermissions.value),
|
||||||
|
processInstanceId: route.query.processInstanceId,
|
||||||
|
taskId: route.query.taskId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.uni?.navigateBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 验证环境类型
|
||||||
|
if (!initEnvType()) {
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 先加载微信 JSSDK(微信小程序需要)
|
||||||
|
const wxScript = document.createElement('script');
|
||||||
|
wxScript.type = 'text/javascript';
|
||||||
|
wxScript.src = 'https://res.wx.qq.com/open/js/jweixin-1.4.0.js';
|
||||||
|
|
||||||
|
wxScript.addEventListener('load', () => {
|
||||||
|
// 2. 微信 SDK 加载完成后,加载 UniApp WebView SDK
|
||||||
|
const uniScript = document.createElement('script');
|
||||||
|
uniScript.type = 'text/javascript';
|
||||||
|
uniScript.src = 'https://unpkg.com/@dcloudio/uni-webview-js@0.0.3/index.js';
|
||||||
|
|
||||||
|
uniScript.addEventListener('load', () => {
|
||||||
|
// 所有 SDK 加载完成后初始化
|
||||||
|
initApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
uniScript.addEventListener('error', () => {
|
||||||
|
error.value = 'UniApp WebView SDK 加载失败';
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.head.append(uniScript);
|
||||||
|
});
|
||||||
|
|
||||||
|
wxScript.addEventListener('error', () => {
|
||||||
|
// 微信 SDK 加载失败,尝试只加载 UniApp SDK(可能是其他小程序)
|
||||||
|
const uniScript = document.createElement('script');
|
||||||
|
uniScript.type = 'text/javascript';
|
||||||
|
uniScript.src = 'https://unpkg.com/@dcloudio/uni-webview-js@0.0.3/index.js';
|
||||||
|
|
||||||
|
uniScript.addEventListener('load', () => {
|
||||||
|
initApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
uniScript.addEventListener('error', () => {
|
||||||
|
error.value = 'SDK 加载失败';
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.head.append(uniScript);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.head.append(wxScript);
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
initApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化应用
|
||||||
|
*/
|
||||||
|
function initApp() {
|
||||||
|
// 设置主题为 light 模式
|
||||||
|
updatePreferences({
|
||||||
|
theme: {
|
||||||
|
mode: 'light',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化 token
|
||||||
|
initToken();
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
if (route.query.processInstanceId) {
|
||||||
|
getDetail();
|
||||||
|
} else {
|
||||||
|
loading.value = false;
|
||||||
|
error.value = '缺少必要参数:processInstanceId';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mobile-form-preview-antd">
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<Spin size="large" tip="加载中..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误状态 -->
|
||||||
|
<Empty v-else-if="error" :description="error" />
|
||||||
|
|
||||||
|
<!-- 表单内容 -->
|
||||||
|
<template v-else>
|
||||||
|
<!-- 有表单规则时渲染 form-create -->
|
||||||
|
<div v-if="hasFormContent" class="mt-4">
|
||||||
|
<form-create
|
||||||
|
v-model="detailForm.value"
|
||||||
|
v-model:api="fApi"
|
||||||
|
:option="detailForm.option"
|
||||||
|
:rule="detailForm.rule"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 确定按钮 -->
|
||||||
|
<div class="form-footer">
|
||||||
|
<Button type="primary" size="large" block @click="handleConfirm">
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 无表单内容时显示空状态 -->
|
||||||
|
<Empty v-else description="暂无表单内容" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 响应式适配 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-form-preview-antd {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-form-item) {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-form-preview-antd {
|
||||||
|
min-height: 100px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-footer {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
box-shadow: 0 -2px 8px rgb(0 0 0 / 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-footer :deep(.ant-btn) {
|
||||||
|
height: 48px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user