af-mobile-client-vue3 1.0.70 → 1.0.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@ import { defineEmits, nextTick, reactive, ref, watch } from 'vue'
14
14
  import { getConfigByNameWithoutIndexedDB } from '@af-mobile-client-vue3/services/api/common'
15
15
  import Uploader from '@af-mobile-client-vue3/components/core/Uploader/index.vue'
16
16
  import XReportFormJsonRender from '@af-mobile-client-vue3/components/data/XReportForm/XReportFormJsonRender.vue'
17
+ import XSignature from '@af-mobile-client-vue3/components/data/XSignature/index.vue'
17
18
 
18
19
  // ------------------------- 类型定义 -------------------------
19
20
  interface configDefine {
@@ -416,6 +417,11 @@ function formatConfigToForm(config: configDefine): void {
416
417
  tempObj.text = row[j].text
417
418
  tempObj.type = 'curDateInput'
418
419
  break
420
+ case 'signature' :
421
+ tempObj.dataIndex = row[j].dataIndex
422
+ tempObj.text = row[j].text
423
+ tempObj.type = 'signature'
424
+ break
419
425
  case 'inputs' :
420
426
  if (!tempObj.format)
421
427
  tempObj.format = ''
@@ -824,6 +830,19 @@ function getNow() {
824
830
  </template>
825
831
  </van-field>
826
832
  </van-cell-group>
833
+ <!-- signature -->
834
+ <van-cell-group v-else-if="row.type === 'signature'" inset style="margin-top: 4vh">
835
+ <van-field
836
+ :label="row.valueText ? `${row.valueText}:` : ''"
837
+ readonly
838
+ rows="1"
839
+ autosize
840
+ >
841
+ <template #button>
842
+ <XSignature v-model="activatedConfig.data[row.dataIndex]" />
843
+ </template>
844
+ </van-field>
845
+ </van-cell-group>
827
846
  <!-- inputs -->
828
847
  <van-cell-group v-else-if="row.type === 'inputs'" inset :title="row.label">
829
848
  <template v-if="row.inputReadOnly === true">
@@ -0,0 +1,285 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
3
+ import {
4
+ Button as VanButton,
5
+ Icon as VanIcon,
6
+ Popup as VanPopup,
7
+ Signature as VanSignature,
8
+ showToast,
9
+ } from 'vant'
10
+
11
+ interface Props {
12
+ /** 签名图片的 base64 字符串 */
13
+ modelValue?: string
14
+ /** 签名笔画的宽度,默认为 3 */
15
+ lineWidth?: number
16
+ /** 签名笔画的颜色,默认为黑色 '#000' */
17
+ strokeStyle?: string
18
+ /** 签名画布的背景颜色,默认为白色 '#fff' */
19
+ backgroundColor?: string
20
+ /** 未签名时按钮显示的文字,默认为 '签名' */
21
+ buttonText?: string
22
+ /** 自定义 CSS 类名 */
23
+ customClass?: string
24
+ /** 是否隐藏底部按钮组,默认为 false */
25
+ hideButtons?: boolean
26
+ /** 按钮尺寸,可选值为 'small' | 'normal' | 'large',默认为 'normal' */
27
+ buttonSize?: 'small' | 'normal' | 'large'
28
+ /** 确认按钮文字,默认为 '确认' */
29
+ confirmButtonText?: string
30
+ /** 清除按钮文字,默认为 '清除' */
31
+ clearButtonText?: string
32
+ /** 是否显示签名预览图,默认为 true */
33
+ showPreview?: boolean
34
+ /** 签名完成后是否显示重新签名按钮,默认为 false */
35
+ showButtonAfterSigned?: boolean
36
+ }
37
+
38
+ withDefaults(defineProps<Props>(), {
39
+ modelValue: '',
40
+ lineWidth: 3,
41
+ strokeStyle: '#000',
42
+ backgroundColor: '#fff',
43
+ buttonText: '签名',
44
+ customClass: '',
45
+ hideButtons: false,
46
+ buttonSize: 'normal',
47
+ confirmButtonText: '确认',
48
+ clearButtonText: '清除',
49
+ showPreview: true,
50
+ showButtonAfterSigned: false,
51
+ })
52
+
53
+ const emit = defineEmits<{
54
+ 'update:modelValue': [value: string]
55
+ 'save': [image: string]
56
+ 'clear': []
57
+ }>()
58
+
59
+ interface SignatureResult {
60
+ image: string
61
+ }
62
+
63
+ const showSignature = ref(false)
64
+
65
+ function onSubmit({ image }: SignatureResult) {
66
+ if (!image) {
67
+ showToast('获取签名失败')
68
+ return
69
+ }
70
+ emit('update:modelValue', image)
71
+ emit('save', image)
72
+ showSignature.value = false
73
+ }
74
+
75
+ function onClear() {
76
+ emit('update:modelValue', '')
77
+ emit('clear')
78
+ showSignature.value = false
79
+ }
80
+
81
+ const signatureRef = ref()
82
+
83
+ function handleSubmit() {
84
+ try {
85
+ signatureRef.value?.submit()
86
+ }
87
+ catch (error) {
88
+ showToast('签名提交失败')
89
+ console.error('Signature submit error:', error)
90
+ }
91
+ }
92
+
93
+ const popupStyle = computed(() => ({
94
+ width: '100%',
95
+ height: window.innerHeight > window.innerWidth ? 'auto' : '100%',
96
+ }))
97
+
98
+ function handleOrientationChange() {
99
+ if (showSignature.value) {
100
+ nextTick(() => {
101
+ signatureRef.value?.resize()
102
+ })
103
+ }
104
+ }
105
+
106
+ onMounted(() => {
107
+ window.addEventListener('orientationchange', handleOrientationChange)
108
+ })
109
+
110
+ onBeforeUnmount(() => {
111
+ window.removeEventListener('orientationchange', handleOrientationChange)
112
+ })
113
+ </script>
114
+
115
+ <template>
116
+ <div class="x-signature" role="application" aria-label="签名面板">
117
+ <div class="signature-content">
118
+ <div v-if="modelValue && showPreview" class="signature-preview" @click="showSignature = true">
119
+ <img :src="modelValue" alt="签名预览">
120
+ </div>
121
+ <VanButton
122
+ v-if="!modelValue || showButtonAfterSigned"
123
+ type="primary"
124
+ size="small"
125
+ aria-label="打开签名面板"
126
+ @click="showSignature = true"
127
+ >
128
+ {{ modelValue ? '重新签名' : buttonText }}
129
+ </VanButton>
130
+ </div>
131
+
132
+ <VanPopup v-model:show="showSignature" class="signature-popup" :style="popupStyle">
133
+ <div class="signature-wrapper">
134
+ <VanSignature
135
+ ref="signatureRef"
136
+ :line-width="lineWidth"
137
+ :stroke-style="strokeStyle"
138
+ :background-color="backgroundColor"
139
+ @submit="onSubmit"
140
+ @clear="onClear"
141
+ />
142
+ <!-- 自定义浮动按钮,仅在横屏时显示 -->
143
+ <div class="custom-buttons">
144
+ <VanButton round size="small" aria-label="清除签名" @click="onClear">
145
+ <VanIcon name="delete" />
146
+ </VanButton>
147
+ <VanButton round type="primary" size="small" @click="handleSubmit">
148
+ <VanIcon name="success" />
149
+ </VanButton>
150
+ </div>
151
+ </div>
152
+ </VanPopup>
153
+ </div>
154
+ </template>
155
+
156
+ <style scoped lang="less">
157
+ .x-signature {
158
+ width: 100%;
159
+
160
+ .signature-content {
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 12px;
164
+ }
165
+
166
+ .signature-preview {
167
+ flex: 1;
168
+ border: 1px solid #eee;
169
+ border-radius: 4px;
170
+ padding: 8px;
171
+ display: flex;
172
+ align-items: center;
173
+
174
+ img {
175
+ max-width: 100%;
176
+ max-height: 36px;
177
+ height: auto;
178
+ display: block;
179
+ }
180
+ }
181
+
182
+ .van-button {
183
+ flex-shrink: 0;
184
+ }
185
+
186
+ :deep(.van-popup) {
187
+ max-width: none !important;
188
+ width: 100% !important;
189
+ top: 20% !important;
190
+ transform: none !important;
191
+ transition: all 0.3s ease-in-out;
192
+ }
193
+
194
+ .signature-wrapper {
195
+ width: 100%;
196
+ height: 100%;
197
+ position: relative;
198
+
199
+ :deep(.van-signature) {
200
+ width: 100% !important;
201
+ height: 100% !important;
202
+ max-width: none !important;
203
+ }
204
+
205
+ // 默认隐藏自定义按钮
206
+ .custom-buttons {
207
+ display: none;
208
+ }
209
+ }
210
+
211
+ // 横屏模式样式
212
+ @media screen and (orientation: landscape) {
213
+ :deep(.van-popup) {
214
+ top: 0 !important;
215
+ right: 0 !important;
216
+ bottom: 0 !important;
217
+ left: 0 !important;
218
+ width: 100% !important;
219
+ height: 100% !important;
220
+ display: flex;
221
+ flex-direction: column;
222
+ }
223
+
224
+ .signature-wrapper {
225
+ flex: 1;
226
+ display: flex;
227
+ justify-content: center;
228
+ align-items: center;
229
+ padding: env(safe-area-inset-top) env(safe-area-inset-right)
230
+ env(safe-area-inset-bottom) env(safe-area-inset-left);
231
+
232
+ :deep(.van-signature) {
233
+ // 隐藏原有的底部按钮
234
+ .van-signature__footer {
235
+ display: none;
236
+ }
237
+ }
238
+
239
+ :deep(.van-signature__content) {
240
+ height: 95% !important;
241
+ }
242
+
243
+ // 显示自定义浮动按钮
244
+ .custom-buttons {
245
+ position: absolute;
246
+ right: 32px;
247
+ bottom: 32px;
248
+ display: flex;
249
+ gap: 16px;
250
+
251
+ .van-button {
252
+ width: 48px !important;
253
+ height: 48px !important;
254
+ border-radius: 50% !important;
255
+ padding: 0 !important;
256
+ display: flex;
257
+ align-items: center;
258
+ justify-content: center;
259
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
260
+ transition: transform 0.2s ease;
261
+
262
+ &--default {
263
+ background: #fff;
264
+ }
265
+
266
+ &:active {
267
+ transform: scale(0.95);
268
+ }
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ // 优化按钮触摸反馈
275
+ .custom-buttons {
276
+ .van-button {
277
+ transition: transform 0.2s ease;
278
+
279
+ &:active {
280
+ transform: scale(0.95);
281
+ }
282
+ }
283
+ }
284
+ }
285
+ </style>
@@ -10,6 +10,9 @@ import PageLayout from '@af-mobile-client-vue3/layout/PageLayout.vue'
10
10
  import login from '@af-mobile-client-vue3/views/user/login/index.vue'
11
11
  import NotFound from '@af-mobile-client-vue3/views/common/NotFound.vue'
12
12
  import SingleLayout from '@af-mobile-client-vue3/layout/SingleLayout.vue'
13
+ import XFormGroupView from '@af-mobile-client-vue3/views/component/XFormGroupView/index.vue'
14
+ import XSignatureView from '@af-mobile-client-vue3/views/component/XSignatureView/index.vue'
15
+ import XForm from '@af-mobile-client-vue3/components/data/XForm/index.vue'
13
16
 
14
17
  const routes: Array<RouteRecordRaw> = [
15
18
  {
@@ -23,6 +26,18 @@ const routes: Array<RouteRecordRaw> = [
23
26
  name: 'XReportFormIframeView',
24
27
  component: XReportFormIframeView,
25
28
  },
29
+ {
30
+ path: '/Components/XForm',
31
+ name: 'XForm',
32
+ component: XForm,
33
+ props: route => ({
34
+ groupFormItems: JSON.parse(decodeURIComponent(route.query.groupFormItems as string)),
35
+ serviceName: route.query.serviceName,
36
+ formData: JSON.parse(decodeURIComponent(route.query.formData as string)),
37
+ mode: route.query.mode,
38
+ }),
39
+ meta:{ title: '新增/修改表单' }
40
+ },
26
41
  ],
27
42
  },
28
43
  {
@@ -46,11 +61,28 @@ const routes: Array<RouteRecordRaw> = [
46
61
  name: 'XCellDetailView',
47
62
  component: XCellDetailView,
48
63
  },
64
+ {
65
+ path: '/Component/XFormGroupView',
66
+ name: 'XFormGroupView',
67
+ component: XFormGroupView,
68
+ },
49
69
  {
50
70
  path: '/Component/XReportFormView',
51
71
  name: 'XReportFormView',
52
72
  component: XReportFormView,
53
73
  },
74
+ // {
75
+ // path: '/Components/XForm',
76
+ // name: 'XForm',
77
+ // component: XForm,
78
+ // props: route => ({
79
+ // groupFormItems: JSON.parse(decodeURIComponent(route.query.groupFormItems as string)),
80
+ // serviceName: route.query.serviceName,
81
+ // formData: JSON.parse(decodeURIComponent(route.query.formData as string)),
82
+ // mode: route.query.mode,
83
+ // }),
84
+ // meta:{ title: '新增/修改表单' }
85
+ // },
54
86
  {
55
87
  path: '/Component/XFormView/:id/:openid',
56
88
  name: 'XFormView',
@@ -61,6 +93,11 @@ const routes: Array<RouteRecordRaw> = [
61
93
  name: 'EvaluateRecordView',
62
94
  component: EvaluateRecordView,
63
95
  },
96
+ {
97
+ path: '/Component/XSignatureView',
98
+ name: 'XSignatureView',
99
+ component: XSignatureView,
100
+ },
64
101
  ],
65
102
  },
66
103
  {
@@ -0,0 +1,136 @@
1
+ import { METHOD, request } from '@af-mobile-client-vue3/utils/http/index'
2
+ import { runLogic } from '@af-mobile-client-vue3/services/api/common'
3
+
4
+ function getLeafNodes (nodes) {
5
+ // console.log(nodes)
6
+ // 确保 nodes 是数组
7
+ const nodeArray = Array.isArray(nodes) ? nodes : [nodes]
8
+ return nodeArray.reduce((leaves, node) => {
9
+ if (node.children && node.children.length) {
10
+ // 递归处理子节点并合并结果
11
+ leaves.push(...getLeafNodes(node.children))
12
+ } else {
13
+ // 当前节点是叶子节点时,直接加入结果
14
+ leaves.push(node)
15
+ }
16
+ return leaves
17
+ }, [])
18
+ }
19
+
20
+ function getLeafNodesByCondition (type, nodes, parent = null) {
21
+ // console.log('nodes------',nodes)
22
+ // 确保 nodes 是数组
23
+ const nodeArray = Array.isArray(nodes) ? nodes : [nodes]
24
+
25
+ return nodeArray.reduce((leaves, node) => {
26
+ const isValidNode = node.resourcetype === type && node.name !== '组织机构'
27
+ const updatedParent = node.name === '组织机构' ? null : node.name
28
+
29
+ if (node.children && node.children.length) {
30
+ // 当前节点符合条件时,加入叶子节点列表
31
+ if (isValidNode && node.resourcetype === 'organization') {
32
+ leaves.push({
33
+ ...node,
34
+ name: parent ? `${node.name}` : node.name,
35
+ children: [],
36
+ })
37
+ }
38
+ if (isValidNode && node.resourcetype === 'department') {
39
+ leaves.push({
40
+ ...node,
41
+ name: parent ? `${node.name}-${parent}` : node.name,
42
+ children: [],
43
+ })
44
+ }
45
+ // 递归处理子节点
46
+ leaves.push(...getLeafNodesByCondition(type, node.children, updatedParent))
47
+ } else if (isValidNode) {
48
+ // 无子节点但符合条件时,直接加入叶子节点列表
49
+ if (node.resourcetype === 'organization'){
50
+ leaves.push({
51
+ ...node,
52
+ name: parent ? `${node.name}-${parent}` : node.name,
53
+ })
54
+ }else if (node.resourcetype === 'department' && !nodeArray.includes(node.name)) {
55
+ // console.log('数组',nodeArray.includes(node.name))
56
+ leaves.push({
57
+ ...node,
58
+ name: parent ? `${node.name}-${parent}` : node.name,
59
+ })
60
+ }
61
+ }
62
+ return leaves
63
+ }, [])
64
+ }
65
+
66
+ function transformData (inputData) {
67
+ function transform (node) {
68
+ return {
69
+ label: node.name,
70
+ value: node.id,
71
+ f_organization_id: node.f_organization_id,
72
+ f_department_id: node.f_department_id,
73
+ parentid: node.parentid,
74
+ orgid: node.orgid,
75
+ children: node.children ? node.children.map(transform) : []
76
+ }
77
+ }
78
+
79
+ return inputData.map(transform)
80
+ }
81
+
82
+ function getResData (params, toCallback) {
83
+ const data = { userId: params.userid, roleName: params.roleName }
84
+ if (params.source === '获取分公司') {
85
+ runLogic('getOrgBySearch', data, 'af-system').then(res => toCallback(res))
86
+ } else if (params.source === '获取部门') {
87
+ runLogic('getDepBySearch', data, 'af-system').then(res => toCallback(res))
88
+ } else if (params.source === '获取人员') {
89
+ runLogic('getUserBySearch', data, 'af-system').then(res => toCallback(res))
90
+ } else if (params.source === '根据角色获取人员') {
91
+ runLogic('getUserBySearchRole', data, 'af-system').then(res => toCallback(res))
92
+ } else {
93
+ return search(params).then(res => toCallback(res))
94
+ }
95
+ }
96
+
97
+ export async function searchToOption (params, callback) {
98
+ function toCallback (res) {
99
+ if (res.length) {
100
+ if (res[0].children && res[0].children.length) {
101
+ if (res[0].children[0].children) {
102
+ callback(transformData(res[0].children[0].children))
103
+ } else {
104
+ callback(transformData(res[0].children))
105
+ }
106
+ } else {
107
+ callback(res[0].children)
108
+ }
109
+ } else {
110
+ callback(res)
111
+ }
112
+ }
113
+
114
+ await getResData(params, toCallback)
115
+ }
116
+
117
+ export async function searchToListOption (params, callback) {
118
+ function toCallback (res) {
119
+ if (params.source.includes('人员')) {
120
+ // console.log('ren---------',res)
121
+ callback(transformData(getLeafNodes(res)))
122
+ } else {
123
+ const type = params.source.includes('公司') ? 'organization' : 'department'
124
+ // console.log('bumenpgonngsi',res)
125
+ callback(transformData(getLeafNodesByCondition(type, res)))
126
+ }
127
+ }
128
+
129
+ await getResData(params, toCallback)
130
+ }
131
+
132
+ export async function search (params) {
133
+ return request('/rs/search', METHOD.POST, params, params.config)
134
+ }
135
+
136
+ export default { searchToOption, search }
@@ -48,7 +48,6 @@ function hasAnyItem(required, source, filter): boolean {
48
48
  * @param route 路由
49
49
  * @param permissions 用户权限集合
50
50
  * @param roles 用户角色集合
51
- * @returns {boolean}
52
51
  */
53
52
  function hasAuthority(route, permissions, roles) {
54
53
  // TODO 此处判断可能有问题
@@ -3,6 +3,7 @@ import { showToast } from 'vant'
3
3
  import { ContentTypeEnum, ResultEnum } from '@af-mobile-client-vue3/enums/requestEnum'
4
4
  import { ACCESS_TOKEN } from '@af-mobile-client-vue3/stores/mutation-type'
5
5
  import { useUserStore } from '@af-mobile-client-vue3/stores/modules/user'
6
+ import axios from 'axios'
6
7
 
7
8
  // 默认 axios 实例请求配置
8
9
  const configDefault = {
@@ -155,4 +156,37 @@ class Http {
155
156
  }
156
157
  }
157
158
 
159
+ const METHOD = {
160
+ GET: 'get',
161
+ POST: 'post',
162
+ PUT: 'put',
163
+ DELETE: 'delete'
164
+ }
165
+ /**
166
+ * axios请求
167
+ * @param url 请求地址
168
+ * @param method {METHOD} http method
169
+ * @param params 请求参数
170
+ * @param config
171
+ * @returns {Promise<AxiosResponse<T>>}
172
+ */
173
+ async function request (url, method, params, config) {
174
+ switch (method) {
175
+ case METHOD.GET:
176
+ return axios.get(url, { params, ...config })
177
+ case METHOD.POST:
178
+ return axios.post(url, params, config)
179
+ case METHOD.PUT:
180
+ return axios.put(url, params, config)
181
+ case METHOD.DELETE:
182
+ return axios.delete(url, { params, ...config })
183
+ default:
184
+ return axios.get(url, { params, ...config })
185
+ }
186
+ }
187
+
158
188
  export const http = new Http(configDefault)
189
+ export {
190
+ METHOD,
191
+ request,
192
+ }
@@ -0,0 +1,13 @@
1
+ export function executeStrFunction(funcString, args) {
2
+ // 使用 eval 执行传入的函数字符串
3
+ // eslint-disable-next-line no-eval
4
+ return eval(`(${funcString})`)(...args)
5
+ }
6
+
7
+ export function executeStrFunctionByContext(context, fnStr, args) {
8
+ // 使用 new Function 创建函数,并绑定 context 作为 this
9
+ // eslint-disable-next-line no-new-func
10
+ const fn = new Function(`return (${fnStr});`)()
11
+ // 使用 bind 绑定 context,并立即调用
12
+ return fn.bind(context)(...args)
13
+ }