gaoluyang
2026-06-29 27cd042df9aca0383a49f3514bc21958dd890912
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
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 '../../../../packages/constants/src';
import { updatePreferences } from '../../../../packages/preferences/src';
import { useAccessStore } from '../../../../packages/stores/src';
 
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 }, targetOrigin?: string) => 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 }, window.location.origin);
    } 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>