af-mobile-client-vue3 1.4.14 → 1.4.15

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "af-mobile-client-vue3",
3
3
  "type": "module",
4
- "version": "1.4.14",
4
+ "version": "1.4.15",
5
5
  "packageManager": "pnpm@10.13.1",
6
6
  "description": "Vue + Vite component lib",
7
7
  "engines": {
@@ -0,0 +1,312 @@
1
+ <script setup lang="ts">
2
+ import type { SignatureComponentExpose, SignatureComponentProps } from '@af-mobile-client-vue3/components/core/Signature/signature'
3
+ import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
4
+ import { showImagePreview, showToast } from 'vant'
5
+ import { defineEmits, defineProps, inject, ref } from 'vue'
6
+
7
+ const props = withDefaults(defineProps<SignatureComponentProps>(), {
8
+ label: '用户签字',
9
+ required: false,
10
+ disabled: false,
11
+ uploadMode: 'server',
12
+ imageList: null,
13
+ formReadonly: false,
14
+ isAsyncUpload: false,
15
+ })
16
+
17
+ const emit = defineEmits<{
18
+ signatureComplete: [data: any]
19
+ }>()
20
+
21
+ const signatureImage = ref<string>('')
22
+ const rawBase64Data = ref<string>('') // 保存原始base64数据
23
+ const hasSignature = ref(false)
24
+ const parentData: any = inject('provideParent')
25
+ const imageList = ref<Array<any>>(props.imageList ?? [])
26
+ console.log('imageList', imageList.value)
27
+ console.log('props.imageList', props.imageList)
28
+ // 处理签字后的上传
29
+ function getImageMimeType(fileName: string): string {
30
+ const ext = fileName.split('.').pop()?.toLowerCase()
31
+ if (ext === 'jpg' || ext === 'jpeg')
32
+ return 'image/jpeg'
33
+ if (ext === 'png')
34
+ return 'image/png'
35
+ if (ext === 'gif')
36
+ return 'image/gif'
37
+ return 'image/png' // 默认
38
+ }
39
+
40
+ function handleSignature() {
41
+ if (props.disabled)
42
+ return
43
+
44
+ mobileUtil.execute({
45
+ funcName: 'showSignaturePad',
46
+ param: {},
47
+ callbackFunc: (result: any) => {
48
+ console.log('签字结果:', result)
49
+ if (result.status === 'success') {
50
+ const mimeType = getImageMimeType(result.data.filePath)
51
+ // 保存原始base64数据
52
+ rawBase64Data.value = result.data.base64
53
+ // 添加base64图片前缀用于显示
54
+ signatureImage.value = `data:${mimeType};base64,${result.data.base64}`
55
+ hasSignature.value = true
56
+ const tempFile = {
57
+ uid: Date.now() + Math.random().toString(36).substr(2, 5),
58
+ name: result.data.filePath.split('/').pop(),
59
+ status: 'uploading',
60
+ message: '上传中...',
61
+ url: `data:${mimeType};base64,${result.data.base64}`,
62
+ isImage: true,
63
+ type: mimeType,
64
+ }
65
+ if (imageList.value) {
66
+ imageList.value = [tempFile]
67
+ }
68
+
69
+ const param = {
70
+ resUploadMode: props.uploadMode,
71
+ pathKey: 'Default',
72
+ formType: 'image',
73
+ useType: 'Default',
74
+ resUploadStock: '1',
75
+ filename: tempFile.name,
76
+ // 暂无size
77
+ // filesize: photoData.size,
78
+ f_operator: 'server',
79
+ imgPath: result.data.filePath,
80
+ urlPath: `/api/${import.meta.env.VITE_APP_SYSTEM_NAME}/resource/upload`,
81
+ commonId: parentData?.commonId.value ?? '',
82
+ }
83
+ if (props.isAsyncUpload) {
84
+ // 添加上传队列
85
+ mobileUtil.execute({
86
+ funcName: 'queueUpload',
87
+ param,
88
+ callbackFunc: (res: any) => {
89
+ console.warn('上传结果', res)
90
+ let resStatus = 'success'
91
+ // 成功
92
+ if (res.data && res.data.enqueued) {
93
+ const index = imageList.value.findIndex(item => item.uid === tempFile.uid)
94
+ if (index !== -1) {
95
+ delete imageList.value[index].message
96
+ imageList.value[index].status = 'done'
97
+ imageList.value[index].photo_name = tempFile.name
98
+ imageList.value[index].type = mimeType
99
+ }
100
+ }
101
+ else {
102
+ resStatus = 'error'
103
+ // 上传失败,显示错误提示并清除图片
104
+ const errorMessage = res.message || res.error || '上传失败,请重试'
105
+ showToast({
106
+ message: errorMessage,
107
+ type: 'fail',
108
+ duration: 3000,
109
+ })
110
+ clearSignature()
111
+ }
112
+ emit('signatureComplete', {
113
+ status: resStatus,
114
+ data: imageList.value?.length > 0 ? imageList.value[0] : null,
115
+ })
116
+ },
117
+ })
118
+ }
119
+ else {
120
+ // 上传到服务器
121
+ mobileUtil.execute({
122
+ funcName: 'uploadResource',
123
+ param,
124
+ callbackFunc: (result: any) => {
125
+ const index = imageList.value.findIndex(item => item.uid === tempFile.uid)
126
+ if (result.status === 'success') {
127
+ if (index !== -1) {
128
+ imageList.value[index].uid = result.data.id
129
+ imageList.value[index].id = result.data.id
130
+ imageList.value[index].photo_name = result.data.f_filename
131
+ imageList.value[index].f_downloadpath = result.data.f_downloadpath
132
+ delete imageList.value[index].message
133
+ imageList.value[index].status = 'done'
134
+ }
135
+ }
136
+ else {
137
+ // 上传失败,显示错误提示并清除图片
138
+ const errorMessage = result.message || result.error || '上传失败,请重试'
139
+ showToast({
140
+ message: errorMessage,
141
+ type: 'fail',
142
+ duration: 3000,
143
+ })
144
+ clearSignature()
145
+ }
146
+ emit('signatureComplete', {
147
+ status: result.status,
148
+ data: result.data,
149
+ })
150
+ },
151
+ })
152
+ }
153
+ }
154
+ },
155
+ })
156
+ }
157
+
158
+ function clearSignature() {
159
+ signatureImage.value = ''
160
+ rawBase64Data.value = ''
161
+ hasSignature.value = false
162
+ imageList.value = []
163
+ emit('signatureComplete', {
164
+ base64: '',
165
+ status: 'cleared',
166
+ })
167
+ }
168
+
169
+ // 预览签字照片
170
+ function previewSignature() {
171
+ if (imageList.value) {
172
+ showImagePreview(imageList.value.map(item => item.url))
173
+ }
174
+ }
175
+
176
+ // 暴露方法供父组件调用
177
+ defineExpose<SignatureComponentExpose>({
178
+ clearSignature,
179
+ hasSignature: () => hasSignature.value,
180
+ getSignatureData: () => rawBase64Data.value, // 返回原始base64数据
181
+ previewSignature,
182
+ getSignatureList: () => imageList.value.map((item) => { return { photo_name: item.photo_name, id: item.id, f_downloadpath: item.f_downloadpath } }),
183
+ })
184
+ </script>
185
+
186
+ <template>
187
+ <van-field
188
+ center
189
+ name="signature"
190
+ :label="label"
191
+ :required="required"
192
+ >
193
+ <template #input>
194
+ <div class="signature-container">
195
+ <!-- 签字照片预览 -->
196
+ <div v-if="imageList.length > 0 || hasSignature" class="signature-preview">
197
+ <van-image
198
+ v-for="(item, index) in imageList"
199
+ :key="`${item}_${index}`"
200
+ :src="item.url"
201
+ alt="签字照片"
202
+ class="signature-image"
203
+ fit="contain"
204
+ :show-loading="true"
205
+ :show-error="true"
206
+ loading-icon="photo-o"
207
+ error-icon="warning-o"
208
+ @click="previewSignature"
209
+ />
210
+ </div>
211
+
212
+ <!-- 按钮行 -->
213
+ <div class="button-row">
214
+ <!-- 签字按钮 -->
215
+ <van-button
216
+ v-if="!props.formReadonly"
217
+ :disabled="disabled"
218
+ type="default"
219
+ size="small"
220
+ class="signature-btn dashed-btn"
221
+ @click="handleSignature"
222
+ >
223
+ <van-icon :name="hasSignature ? 'edit' : 'edit'" class="btn-icon" />
224
+ {{ imageList.length > 0 || hasSignature ? '重签' : '点击签字' }}
225
+ </van-button>
226
+
227
+ <!-- 清除按钮 -->
228
+ <van-button
229
+ v-if="(imageList.length > 0 || hasSignature) && !props.formReadonly"
230
+ type="default"
231
+ size="small"
232
+ class="dashed-btn clear-btn"
233
+ @click="clearSignature"
234
+ >
235
+ <van-icon name="delete" class="btn-icon" />
236
+ 清除
237
+ </van-button>
238
+ </div>
239
+ </div>
240
+ </template>
241
+ </van-field>
242
+ </template>
243
+
244
+ <style scoped lang="less">
245
+ .signature-container {
246
+ display: flex;
247
+ flex-direction: column;
248
+ gap: 12px;
249
+ width: 100%;
250
+ }
251
+
252
+ .signature-preview {
253
+ position: relative;
254
+ display: flex;
255
+ flex-direction: column;
256
+ align-items: center;
257
+ gap: 8px;
258
+
259
+ .signature-image {
260
+ max-width: 200px;
261
+ max-height: 120px;
262
+ border: 1px solid #e8e8e8;
263
+ border-radius: 4px;
264
+ cursor: pointer;
265
+ transition: all 0.2s ease;
266
+ }
267
+ }
268
+
269
+ // 按钮行样式
270
+ .button-row {
271
+ display: flex;
272
+ gap: 12px;
273
+ align-items: center;
274
+ justify-content: space-between;
275
+ }
276
+
277
+ .signature-btn {
278
+ flex-shrink: 0;
279
+ margin-right: auto;
280
+ }
281
+
282
+ // 虚线按钮样式
283
+ .dashed-btn {
284
+ border: 1px dashed #1989fa !important;
285
+ background-color: transparent !important;
286
+ color: #1989fa !important;
287
+ border-radius: 4px !important;
288
+
289
+ &:hover {
290
+ border-color: #0570d9 !important;
291
+ color: #0570d9 !important;
292
+ }
293
+
294
+ &:active {
295
+ border-color: #0366d6 !important;
296
+ color: #0366d6 !important;
297
+ background-color: rgba(25, 137, 250, 0.1) !important;
298
+ }
299
+
300
+ &:disabled {
301
+ border-color: #c8c9cc !important;
302
+ color: #c8c9cc !important;
303
+ background-color: transparent !important;
304
+ }
305
+ }
306
+
307
+ // 按钮图标样式
308
+ .btn-icon {
309
+ margin-right: 4px;
310
+ font-size: 14px;
311
+ }
312
+ </style>
@@ -0,0 +1,38 @@
1
+ // 签字结果接口
2
+ export interface SignatureResult {
3
+ status: 'success' | 'failed' | 'cancelled' | 'cleared'
4
+ base64?: string
5
+ message?: string
6
+ }
7
+
8
+ // 签字组件属性接口
9
+ export interface SignatureComponentProps {
10
+ label?: string
11
+ required?: boolean
12
+ disabled?: boolean
13
+ uploadMode?: string
14
+ imageList?: Array<any>
15
+ formReadonly?: boolean
16
+ isAsyncUpload?: boolean // 是否使用异步上传-异步上传后将不会返回file表的数据,需要通过上传时的文件名去t_files中查询
17
+ }
18
+
19
+ // 签字完成事件数据接口
20
+ export interface SignatureCompleteData {
21
+ base64: string
22
+ status: string
23
+ }
24
+
25
+ // 签字组件暴露的方法接口
26
+ export interface SignatureComponentExpose {
27
+ clearSignature: () => void
28
+ hasSignature: () => boolean
29
+ getSignatureData: () => string
30
+ previewSignature: () => void
31
+ getSignatureList: () => Array<any>
32
+ }
33
+
34
+ // base64图片工具类型
35
+ export interface Base64ImageData {
36
+ raw: string // 原始base64数据(不含前缀)
37
+ full: string // 完整的base64图片URL(含前缀)
38
+ }
@@ -1,211 +1,211 @@
1
- <script setup lang="ts">
2
- import {
3
- Field as VanField,
4
- Picker as VanPicker,
5
- Popup as VanPopup,
6
- Search as VanSearch,
7
- } from 'vant'
8
- import { computed, defineEmits, defineModel, defineProps, onBeforeMount, ref, watch } from 'vue'
9
-
10
- const props = defineProps({
11
- columns: {
12
- type: Array,
13
- default() {
14
- return []
15
- },
16
- },
17
- option: {
18
- type: Object,
19
- default() {
20
- return { text: 'label', value: 'value', children: 'children' }
21
- },
22
- },
23
- offOption: { // 关闭option配置key-value;当数据是非集合的数组的时候,开启
24
- type: Boolean,
25
- default: false,
26
- },
27
- border: { // 是否展示边框
28
- type: Boolean,
29
- default: false,
30
- },
31
- lazyLoad: { // 是否启用懒加载
32
- type: String,
33
- default: 'false',
34
- },
35
- onSearch: { // 懒加载时的搜索函数
36
- type: Function,
37
- default: null,
38
- },
39
- })
40
- const emits = defineEmits(['confirm', 'change', 'cancel', 'input'])
41
- const show = ref(false)
42
- const resultValue = defineModel()
43
- const columnsData = ref([])
44
- const selectedOption = ref([])
45
- const searchValue = ref('')
46
- const isLoading = ref(false)
47
-
48
- // 转换空children为空字符串
49
- function transformColumns(data) {
50
- return data.map((item) => {
51
- if (item.children && item.children.length === 0) {
52
- return {
53
- ...item,
54
- children: '',
55
- }
56
- }
57
- else if (item.children && item.children.length !== 0) {
58
- return { ...item, children: transformColumns(item.children) }
59
- }
60
- else {
61
- return { ...item }
62
- }
63
- })
64
- }
65
- const filteredColumns = ref([])
66
-
67
- watch(
68
- () => searchValue.value,
69
- async (newValue) => {
70
- if (props.lazyLoad && props.lazyLoad === 'true' && props.onSearch) {
71
- if (!newValue) {
72
- filteredColumns.value = []
73
- return
74
- }
75
-
76
- isLoading.value = true
77
- try {
78
- const results = await props.onSearch(newValue)
79
- filteredColumns.value = transformColumns(results || [])
80
- }
81
- catch (error) {
82
- console.error('懒加载搜索失败:', error)
83
- filteredColumns.value = []
84
- }
85
- finally {
86
- isLoading.value = false
87
- }
88
- }
89
- else {
90
- // 普通搜索模式
91
- if (!newValue) {
92
- filteredColumns.value = columnsData.value
93
- return
94
- }
95
-
96
- const searchTextLower = newValue.toLowerCase()
97
- filteredColumns.value = columnsData.value.filter((item) => {
98
- const text = props.offOption ? item : item[props.option.text]
99
- return text?.toString().toLowerCase().includes(searchTextLower)
100
- })
101
- }
102
- },
103
- { immediate: true },
104
- )
105
-
106
- onBeforeMount(() => {
107
- columnsData.value = transformColumns(props.columns)
108
- filteredColumns.value = columnsData.value
109
- })
110
-
111
- const resultLabel = computed({
112
- get() {
113
- if (!resultValue.value)
114
- return ''
115
- const res = props.columns.filter((item) => {
116
- const data = props.offOption ? item : item[props.option.value]
117
- return data === resultValue.value
118
- })
119
- return res.length ? (props.offOption ? res[0] : res[0][props.option.text]) : ''
120
- },
121
- set() {
122
-
123
- },
124
- })
125
-
126
- function onConfirm(value, _index) {
127
- resultValue.value = props.offOption ? value.selectedValues : value.selectedValues[0]
128
- // resultValue.value = value.selectedValues
129
- selectedOption.value = value.selectedOptions
130
- show.value = !show.value
131
- emits('confirm', value.selectedValues[0], value.selectedOptions)
132
- }
133
-
134
- function change(val, index) {
135
- emits('change', val, index, resultValue.value)
136
- }
137
-
138
- function cancel(val, index) {
139
- show.value = !show.value
140
- emits('cancel', val, index, resultValue.value)
141
- }
142
-
143
- function showPopu(disabled) {
144
- if (disabled !== undefined && disabled !== false)
145
- return false
146
- columnsData.value = transformColumns(props.columns)
147
- filteredColumns.value = columnsData.value
148
- searchValue.value = '' // 重置搜索
149
- show.value = !show.value
150
- }
151
-
152
- watch(() => resultValue, (newVal, _oldVal) => {
153
- columnsData.value = transformColumns(props.columns)
154
- filteredColumns.value = columnsData.value
155
- emits('input', newVal)
156
- })
157
-
158
- // 监听 columns 变化
159
- watch(() => props.columns, () => {
160
- columnsData.value = transformColumns(props.columns)
161
- filteredColumns.value = columnsData.value
162
- searchValue.value = ''
163
- }, { deep: true })
164
- </script>
165
-
166
- <template>
167
- <VanField
168
- v-model="resultLabel"
169
- v-bind="$attrs"
170
- readonly
171
- :is-link="true"
172
- :border="props.border"
173
- @click="showPopu($attrs.readonly)"
174
- />
175
- <VanPopup v-model:show="show" position="bottom">
176
- <div class="x-select-popup">
177
- <!-- 搜索框 -->
178
- <VanSearch
179
- v-model="searchValue"
180
- placeholder="搜索"
181
- />
182
-
183
- <!-- 选择器 -->
184
- <VanPicker
185
- :loading="isLoading"
186
- v-bind="$attrs"
187
- :columns="filteredColumns"
188
- :columns-field-names="props.option"
189
- show-toolbar
190
- :value-key="props.option.text"
191
- @cancel="cancel"
192
- @confirm="onConfirm"
193
- @change="change"
194
- />
195
- </div>
196
- </VanPopup>
197
- </template>
198
-
199
- <style scoped>
200
- .loading-container {
201
- display: flex;
202
- justify-content: center;
203
- align-items: center;
204
- height: 200px;
205
- }
206
-
207
- .loading-text {
208
- color: #969799;
209
- font-size: 14px;
210
- }
211
- </style>
1
+ <script setup lang="ts">
2
+ import {
3
+ Field as VanField,
4
+ Picker as VanPicker,
5
+ Popup as VanPopup,
6
+ Search as VanSearch,
7
+ } from 'vant'
8
+ import { computed, defineEmits, defineModel, defineProps, onBeforeMount, ref, watch } from 'vue'
9
+
10
+ const props = defineProps({
11
+ columns: {
12
+ type: Array,
13
+ default() {
14
+ return []
15
+ },
16
+ },
17
+ option: {
18
+ type: Object,
19
+ default() {
20
+ return { text: 'label', value: 'value', children: 'children' }
21
+ },
22
+ },
23
+ offOption: { // 关闭option配置key-value;当数据是非集合的数组的时候,开启
24
+ type: Boolean,
25
+ default: false,
26
+ },
27
+ border: { // 是否展示边框
28
+ type: Boolean,
29
+ default: false,
30
+ },
31
+ lazyLoad: { // 是否启用懒加载
32
+ type: String,
33
+ default: 'false',
34
+ },
35
+ onSearch: { // 懒加载时的搜索函数
36
+ type: Function,
37
+ default: null,
38
+ },
39
+ })
40
+ const emits = defineEmits(['confirm', 'change', 'cancel', 'input'])
41
+ const show = ref(false)
42
+ const resultValue = defineModel()
43
+ const columnsData = ref([])
44
+ const selectedOption = ref([])
45
+ const searchValue = ref('')
46
+ const isLoading = ref(false)
47
+
48
+ // 转换空children为空字符串
49
+ function transformColumns(data) {
50
+ return data.map((item) => {
51
+ if (item.children && item.children.length === 0) {
52
+ return {
53
+ ...item,
54
+ children: '',
55
+ }
56
+ }
57
+ else if (item.children && item.children.length !== 0) {
58
+ return { ...item, children: transformColumns(item.children) }
59
+ }
60
+ else {
61
+ return { ...item }
62
+ }
63
+ })
64
+ }
65
+ const filteredColumns = ref([])
66
+
67
+ watch(
68
+ () => searchValue.value,
69
+ async (newValue) => {
70
+ if (props.lazyLoad && props.lazyLoad === 'true' && props.onSearch) {
71
+ if (!newValue) {
72
+ filteredColumns.value = []
73
+ return
74
+ }
75
+
76
+ isLoading.value = true
77
+ try {
78
+ const results = await props.onSearch(newValue)
79
+ filteredColumns.value = transformColumns(results || [])
80
+ }
81
+ catch (error) {
82
+ console.error('懒加载搜索失败:', error)
83
+ filteredColumns.value = []
84
+ }
85
+ finally {
86
+ isLoading.value = false
87
+ }
88
+ }
89
+ else {
90
+ // 普通搜索模式
91
+ if (!newValue) {
92
+ filteredColumns.value = columnsData.value
93
+ return
94
+ }
95
+
96
+ const searchTextLower = newValue.toLowerCase()
97
+ filteredColumns.value = columnsData.value.filter((item) => {
98
+ const text = props.offOption ? item : item[props.option.text]
99
+ return text?.toString().toLowerCase().includes(searchTextLower)
100
+ })
101
+ }
102
+ },
103
+ { immediate: true },
104
+ )
105
+
106
+ onBeforeMount(() => {
107
+ columnsData.value = transformColumns(props.columns)
108
+ filteredColumns.value = columnsData.value
109
+ })
110
+
111
+ const resultLabel = computed({
112
+ get() {
113
+ if (!resultValue.value)
114
+ return ''
115
+ const res = props.columns.filter((item) => {
116
+ const data = props.offOption ? item : item[props.option.value]
117
+ return data === resultValue.value
118
+ })
119
+ return res.length ? (props.offOption ? res[0] : res[0][props.option.text]) : ''
120
+ },
121
+ set() {
122
+
123
+ },
124
+ })
125
+
126
+ function onConfirm(value, _index) {
127
+ resultValue.value = props.offOption ? value.selectedValues : value.selectedValues[0]
128
+ // resultValue.value = value.selectedValues
129
+ selectedOption.value = value.selectedOptions
130
+ show.value = !show.value
131
+ emits('confirm', value.selectedValues[0], value.selectedOptions)
132
+ }
133
+
134
+ function change(val, index) {
135
+ emits('change', val, index, resultValue.value)
136
+ }
137
+
138
+ function cancel(val, index) {
139
+ show.value = !show.value
140
+ emits('cancel', val, index, resultValue.value)
141
+ }
142
+
143
+ function showPopu(disabled) {
144
+ if (disabled !== undefined && disabled !== false)
145
+ return false
146
+ columnsData.value = transformColumns(props.columns)
147
+ filteredColumns.value = columnsData.value
148
+ searchValue.value = '' // 重置搜索
149
+ show.value = !show.value
150
+ }
151
+
152
+ watch(() => resultValue, (newVal, _oldVal) => {
153
+ columnsData.value = transformColumns(props.columns)
154
+ filteredColumns.value = columnsData.value
155
+ emits('input', newVal)
156
+ })
157
+
158
+ // 监听 columns 变化
159
+ watch(() => props.columns, () => {
160
+ columnsData.value = transformColumns(props.columns)
161
+ filteredColumns.value = columnsData.value
162
+ searchValue.value = ''
163
+ }, { deep: true })
164
+ </script>
165
+
166
+ <template>
167
+ <VanField
168
+ v-model="resultLabel"
169
+ v-bind="$attrs"
170
+ readonly
171
+ :is-link="true"
172
+ :border="props.border"
173
+ @click="showPopu($attrs.readonly)"
174
+ />
175
+ <VanPopup v-model:show="show" position="bottom">
176
+ <div class="x-select-popup">
177
+ <!-- 搜索框 -->
178
+ <VanSearch
179
+ v-model="searchValue"
180
+ placeholder="搜索"
181
+ />
182
+
183
+ <!-- 选择器 -->
184
+ <VanPicker
185
+ :loading="isLoading"
186
+ v-bind="$attrs"
187
+ :columns="filteredColumns"
188
+ :columns-field-names="props.option"
189
+ show-toolbar
190
+ :value-key="props.option.text"
191
+ @cancel="cancel"
192
+ @confirm="onConfirm"
193
+ @change="change"
194
+ />
195
+ </div>
196
+ </VanPopup>
197
+ </template>
198
+
199
+ <style scoped>
200
+ .loading-container {
201
+ display: flex;
202
+ justify-content: center;
203
+ align-items: center;
204
+ height: 200px;
205
+ }
206
+
207
+ .loading-text {
208
+ color: #969799;
209
+ font-size: 14px;
210
+ }
211
+ </style>
@@ -4,7 +4,7 @@ import dayjs from 'dayjs/esm/index'
4
4
  import { ref } from 'vue'
5
5
 
6
6
  const { serviceName, dictName, dictValue, dictType } = defineProps<{
7
- serviceName: string
7
+ serviceName?: string
8
8
  dictName: string | null | undefined
9
9
  dictValue: string | number | null | undefined
10
10
  // 用于非字典型的格式化展示:如日期/小数/整数等
@@ -225,8 +225,6 @@ function checkModel(val = props.modelValue) {
225
225
  */
226
226
  function getDefaultValue() {
227
227
  const val = props.modelValue
228
- console.warn('>>>>>props', props)
229
- console.warn('>>>> attr', attr)
230
228
  // 如果有有效值,直接返回(datePicker 初始值需按 formValueFormat 归一)
231
229
  // 目的:外部通过 formData 传入的初始值在组件挂载即与配置格式一致
232
230
  if (checkModel(val)) {
@@ -188,4 +188,4 @@ export interface PolygonLayerConfig {
188
188
  onClick?: (polygon: PolygonData, event: any) => void
189
189
  /** 多边形数据提供者 */
190
190
  dataProvider?: () => PolygonData[] | Promise<PolygonData[]>
191
- }
191
+ }
@@ -22,6 +22,7 @@ export interface WebConfig {
22
22
  slideshowList: Array<any>
23
23
  registerRequire: boolean
24
24
  requestEncrypt: boolean
25
+ phoneLoginVerification: boolean
25
26
  }
26
27
 
27
28
  // 存放 webConfig 中的 setting 配置
@@ -214,7 +214,7 @@ export const useUserStore = defineStore('app-user', () => {
214
214
  setToken(data.access_token)
215
215
  // 第三方教培系统鉴权兼容
216
216
  const LoginTicket = crypto.AESEncrypt(JSON.stringify(params), '3KMKqvgwR8ULbR8Z')
217
- if (data.session && useSettingStore().getSetting()?.requestEncrypt) {
217
+ if (data.session && useSettingStore().getSetting().requestEncrypt) {
218
218
  const k = encryptUtil.RSADecrypt(data.session as string)
219
219
  localStorage.setItem('v4-session-key', k)
220
220
  secureStorageWrite('v4-session-key', k)
@@ -9,92 +9,18 @@ const emit = defineEmits(['deleteRow'])
9
9
  // 访问路由
10
10
  const router = useRouter()
11
11
  // 获取默认值
12
- const idKey = ref('o_id')
12
+ // const idKey = ref('o_id')
13
13
 
14
14
  // 简易crud表单测试
15
- const configName = ref('mobile_instrumentManageCRUD')
16
- const serviceName = ref('af-revenue')
17
-
18
- // 资源权限测试
19
- // const configName = ref('crud_sources_test')
20
- // const serviceName = ref('af-system')
21
-
22
- // 实际业务测试
23
- // const configName = ref('lngChargeAuditMobileCRUD')
24
- // const serviceName = ref('af-gaslink')
25
-
26
- // 跳转到详情页面
27
- // function toDetail(item) {
28
- // router.push({
29
- // name: 'XCellDetailView',
30
- // params: { id: item[idKey.value] }, // 如果使用命名路由,推荐使用路由参数而不是直接构建 URL
31
- // query: {
32
- // operName: item[operNameKey.value],
33
- // method:item[methodKey.value],
34
- // requestMethod:item[requestMethodKey.value],
35
- // operatorType:item[operatorTypeKey.value],
36
- // operUrl:item[operUrlKey.value],
37
- // operIp:item[operIpKey.value],
38
- // costTime:item[costTimeKey.value],
39
- // operTime:item[operTimeKey.value],
40
- //
41
- // title: item[titleKey.value],
42
- // businessType: item[businessTypeKey.value],
43
- // status:item[statusKey.value]
44
- // }
45
- // })
46
- // }
47
-
48
- // 跳转到表单——以表单组来渲染纯表单
49
- // function toDetail(item) {
50
- // router.push({
51
- // name: 'XFormGroupView',
52
- // query: {
53
- // id: item[idKey.value],
54
- // // id: item.rr_id,
55
- // // o_id: item.o_id,
56
- // },
57
- // })
58
- // }
59
-
60
- // 新增功能
61
- // function addOption(totalCount) {
62
- // router.push({
63
- // name: 'XFormView',
64
- // params: { id: totalCount, openid: totalCount },
65
- // query: {
66
- // configName: configName.value,
67
- // serviceName: serviceName.value,
68
- // mode: '新增',
69
- // },
70
- // })
71
- // }
72
-
73
- // 修改功能
74
- // function updateRow(result) {
75
- // router.push({
76
- // name: 'XFormView',
77
- // params: { id: result.o_id, openid: result.o_id },
78
- // query: {
79
- // configName: configName.value,
80
- // serviceName: serviceName.value,
81
- // mode: '修改',
82
- // },
83
- // })
84
- // }
85
-
86
- // // 删除功能
87
- // function deleteRow(result) {
88
- // emit('deleteRow', result.o_id)
89
- // }
15
+ const configName = ref('ceshiCRUD')
16
+ const serviceName = ref('af-linepatrol')
90
17
  </script>
91
18
 
92
19
  <template>
93
- <NormalDataLayout id="XCellListView" title="测试">
20
+ <NormalDataLayout id="XCellListView" title="工作计划">
94
21
  <template #layout_content>
95
22
  <XCellList
96
23
  :config-name="configName"
97
- :service-name="serviceName"
98
24
  />
99
25
  </template>
100
26
  </NormalDataLayout>
@@ -28,7 +28,6 @@ function onSubmit(data: any) {
28
28
  ref="formGroupAddConstruction"
29
29
  mode="修改"
30
30
  :config-name="configName"
31
- :service-name="serviceName"
32
31
  :form-data="formData"
33
32
  @on-submit="onSubmit"
34
33
  />
@@ -1,118 +1,118 @@
1
- <script setup lang="ts">
2
- import type { LocationResult } from '@af-mobile-client-vue3/components/data/XOlMap/types'
3
- import LocationPicker from '@af-mobile-client-vue3/components/data/XOlMap/XLocationPicker/index.vue'
4
- import NormalDataLayout from '@af-mobile-client-vue3/components/layout/NormalDataLayout/index.vue'
5
- import { showNotify } from 'vant'
6
- import { ref } from 'vue'
7
-
8
- const selectedLocation = ref<LocationResult>()
9
-
10
- // 处理位置选择
11
- function handleLocationConfirm(location: LocationResult) {
12
- // console.log('选择的位置:', location)
13
- // selectedLocation.value = location
14
- showNotify({ type: 'success', message: '位置已选择' })
15
- }
16
- </script>
17
-
18
- <template>
19
- <NormalDataLayout id="XLocationPicker" title="XOlMap地址选择器">
20
- <template #layout_content>
21
- <div class="location-picker-demo">
22
- <!-- 页面标题 -->
23
- <div class="page-header">
24
- <div class="title">
25
- 位置选择
26
- </div>
27
- </div>
28
-
29
- <!-- 选择结果展示 -->
30
- <div v-if="selectedLocation" class="location-result">
31
- <div class="label">
32
- 已选位置:
33
- </div>
34
- <div class="value">
35
- {{ selectedLocation.address }}
36
- </div>
37
- <div class="coordinates">
38
- 经度: {{ selectedLocation.longitude.toFixed(6) }},
39
- 纬度: {{ selectedLocation.latitude.toFixed(6) }}
40
- </div>
41
- </div>
42
-
43
- <!-- 地图组件 -->
44
- <div class="map-container">
45
- <LocationPicker
46
- v-model="selectedLocation"
47
- :default-center="[108.948024, 34.263161]"
48
- :default-zoom="12"
49
- @confirm="handleLocationConfirm"
50
- />
51
- </div>
52
- </div>
53
- </template>
54
- </NormalDataLayout>
55
- </template>
56
-
57
- <style scoped lang="less">
58
- .location-picker-demo {
59
- width: 100%;
60
- height: 100%;
61
- position: relative;
62
- display: flex;
63
- flex-direction: column;
64
- background-color: #f7f8fa;
65
- }
66
-
67
- .page-header {
68
- height: 44px;
69
- display: flex;
70
- align-items: center;
71
- justify-content: center;
72
- background: white;
73
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
74
- position: relative;
75
- z-index: 1;
76
-
77
- .title {
78
- font-size: 16px;
79
- color: #333;
80
- font-weight: 500;
81
- }
82
- }
83
-
84
- .location-result {
85
- background: white;
86
- padding: 12px 16px;
87
- margin: 10px;
88
- border-radius: 8px;
89
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
90
-
91
- .label {
92
- font-size: 14px;
93
- color: #666;
94
- margin-bottom: 4px;
95
- }
96
-
97
- .value {
98
- font-size: 16px;
99
- color: #333;
100
- margin-bottom: 8px;
101
- word-break: break-all;
102
- }
103
-
104
- .coordinates {
105
- font-size: 12px;
106
- color: #999;
107
- }
108
- }
109
-
110
- .map-container {
111
- flex: 1;
112
- position: relative;
113
- margin: 0 10px 10px 10px;
114
- border-radius: 8px;
115
- overflow: hidden;
116
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
117
- }
118
- </style>
1
+ <script setup lang="ts">
2
+ import type { LocationResult } from '@af-mobile-client-vue3/components/data/XOlMap/types'
3
+ import LocationPicker from '@af-mobile-client-vue3/components/data/XOlMap/XLocationPicker/index.vue'
4
+ import NormalDataLayout from '@af-mobile-client-vue3/components/layout/NormalDataLayout/index.vue'
5
+ import { showNotify } from 'vant'
6
+ import { ref } from 'vue'
7
+
8
+ const selectedLocation = ref<LocationResult>()
9
+
10
+ // 处理位置选择
11
+ function handleLocationConfirm(location: LocationResult) {
12
+ // console.log('选择的位置:', location)
13
+ // selectedLocation.value = location
14
+ showNotify({ type: 'success', message: '位置已选择' })
15
+ }
16
+ </script>
17
+
18
+ <template>
19
+ <NormalDataLayout id="XLocationPicker" title="XOlMap地址选择器">
20
+ <template #layout_content>
21
+ <div class="location-picker-demo">
22
+ <!-- 页面标题 -->
23
+ <div class="page-header">
24
+ <div class="title">
25
+ 位置选择
26
+ </div>
27
+ </div>
28
+
29
+ <!-- 选择结果展示 -->
30
+ <div v-if="selectedLocation" class="location-result">
31
+ <div class="label">
32
+ 已选位置:
33
+ </div>
34
+ <div class="value">
35
+ {{ selectedLocation.address }}
36
+ </div>
37
+ <div class="coordinates">
38
+ 经度: {{ selectedLocation.longitude.toFixed(6) }},
39
+ 纬度: {{ selectedLocation.latitude.toFixed(6) }}
40
+ </div>
41
+ </div>
42
+
43
+ <!-- 地图组件 -->
44
+ <div class="map-container">
45
+ <LocationPicker
46
+ v-model="selectedLocation"
47
+ :default-center="[108.948024, 34.263161]"
48
+ :default-zoom="12"
49
+ @confirm="handleLocationConfirm"
50
+ />
51
+ </div>
52
+ </div>
53
+ </template>
54
+ </NormalDataLayout>
55
+ </template>
56
+
57
+ <style scoped lang="less">
58
+ .location-picker-demo {
59
+ width: 100%;
60
+ height: 100%;
61
+ position: relative;
62
+ display: flex;
63
+ flex-direction: column;
64
+ background-color: #f7f8fa;
65
+ }
66
+
67
+ .page-header {
68
+ height: 44px;
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ background: white;
73
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
74
+ position: relative;
75
+ z-index: 1;
76
+
77
+ .title {
78
+ font-size: 16px;
79
+ color: #333;
80
+ font-weight: 500;
81
+ }
82
+ }
83
+
84
+ .location-result {
85
+ background: white;
86
+ padding: 12px 16px;
87
+ margin: 10px;
88
+ border-radius: 8px;
89
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
90
+
91
+ .label {
92
+ font-size: 14px;
93
+ color: #666;
94
+ margin-bottom: 4px;
95
+ }
96
+
97
+ .value {
98
+ font-size: 16px;
99
+ color: #333;
100
+ margin-bottom: 8px;
101
+ word-break: break-all;
102
+ }
103
+
104
+ .coordinates {
105
+ font-size: 12px;
106
+ color: #999;
107
+ }
108
+ }
109
+
110
+ .map-container {
111
+ flex: 1;
112
+ position: relative;
113
+ margin: 0 10px 10px 10px;
114
+ border-radius: 8px;
115
+ overflow: hidden;
116
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
117
+ }
118
+ </style>
@@ -2,7 +2,7 @@
2
2
  import type { FormInstance } from 'vant'
3
3
  import { LoginStateEnum, useFormRules, useLoginState } from '@af-mobile-client-vue3/hooks/useLogin'
4
4
  import GetAppDataService from '@af-mobile-client-vue3/plugins/AppData'
5
- import { getConfigByNameAsync, runLogic } from '@af-mobile-client-vue3/services/api/common'
5
+ import { runLogic } from '@af-mobile-client-vue3/services/api/common'
6
6
  import { getUserPermissions } from '@af-mobile-client-vue3/services/api/search'
7
7
  import { useSettingStore } from '@af-mobile-client-vue3/stores/modules/setting'
8
8
  import { useUserStore } from '@af-mobile-client-vue3/stores/modules/user'
@@ -107,15 +107,11 @@ function handleSubmit() {
107
107
  try {
108
108
  loading.value = true
109
109
  showLoadingToast('登录中...')
110
- let username = formData.username
111
- let password = formData.password
112
- const webConfig = await getConfigByNameAsync('webConfig')
113
- const verification = webConfig.setting?.phoneLoginVerification || false
114
- if (verification) {
110
+ const username = formData.username
111
+ const password = formData.password
112
+ if (setting.getSetting()?.phoneLoginVerification) {
115
113
  try {
116
- const res = await runLogic('openapi/loginVerification', { username, password }) as any
117
- username = res.username
118
- password = res.password
114
+ await runLogic('openapi/loginVerification', { username, password: bcrypt.hashSync(password, 10) }) as any
119
115
  }
120
116
  catch (e) {
121
117
  console.error('验证登录失败: ', e)
package/vite.config.ts CHANGED
@@ -11,8 +11,8 @@ export default ({ mode }: ConfigEnv): UserConfig => {
11
11
 
12
12
  const appProxys = {}
13
13
 
14
- const v4Server = 'http://117.35.109.162:31577'
15
- const v3Server = 'http://117.35.109.162:31577'
14
+ const v4Server = 'http://192.168.50.67:31567'
15
+ const v3Server = 'http://192.168.50.67:31567'
16
16
  const OSSServerDev = 'http://192.168.50.67:30351'
17
17
  const geoserver = 'http://39.104.49.8:30372'
18
18
  const mockServer = 'http://127.0.0.1:8086'
@@ -64,13 +64,6 @@ export default ({ mode }: ConfigEnv): UserConfig => {
64
64
  changeOrigin: true,
65
65
  rewrite: path => path.replace(/^\/linepatrol\/geoserver/, '/geoserver'),
66
66
  },
67
- // '/api/af-revenue': {
68
- // // target: v4Server,
69
- // rewrite: (path: string) => path.replace(/^\/api\/af-revenue\//, '/'),
70
- // target: 'http://127.0.0.1:9026',
71
- // ws: false,
72
- // changeOrigin: true,
73
- // },
74
67
  '/api': {
75
68
  // v3用
76
69
  // rewrite: (path: string) => path.replace(/^\/api\/af-system\//, '/rs/'),