!331 Merge remote-tracking branch 'yudao/dev' into dev
Merge pull request !331 from Jason/dev
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 };
|
||||
|
||||
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