af-mobile-client-vue3 1.2.27 → 1.2.29

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/CLAUDE.md ADDED
@@ -0,0 +1,184 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ This is a Vue 3 mobile client application (`af-mobile-client-vue3`) built as a smart gas system (智慧燃气) mobile application. It serves as a micro-frontend main application with comprehensive business components and dynamic form capabilities.
8
+
9
+ ## Development Commands
10
+
11
+ ```bash
12
+ # Install dependencies
13
+ pnpm install
14
+
15
+ # Development server (with mock server on port 8086)
16
+ pnpm dev
17
+
18
+ # Build for production
19
+ pnpm build
20
+
21
+ # Build for development environment
22
+ pnpm build:dev
23
+
24
+ # Lint and type checking
25
+ pnpm lint
26
+
27
+ # Auto-fix linting issues
28
+ pnpm lint:fix
29
+
30
+ # Type checking only
31
+ pnpm typecheck
32
+
33
+ # Version release
34
+ pnpm release
35
+ ```
36
+
37
+ ## Technology Stack
38
+
39
+ - **Vue 3** with Composition API and `<script setup>` syntax
40
+ - **TypeScript** for type safety
41
+ - **Vite** as build tool (port 7190)
42
+ - **Vant 4** as primary UI component library
43
+ - **Pinia** for state management with persistence
44
+ - **pnpm** as package manager (requires Node.js >=20.19.0)
45
+ - **UnoCSS** for atomic CSS utilities
46
+ - **@micro-zoe/micro-app** for micro-frontend architecture
47
+
48
+ ## Code Style & Standards
49
+
50
+ From `.cursorrules`:
51
+
52
+ - Use Composition API and `<script setup>` syntax
53
+ - Component names: PascalCase (e.g., `UserProfile`)
54
+ - Variables: camelCase (e.g., `userName`)
55
+ - Boolean variables: use `is/has/should` prefix (e.g., `isLoading`)
56
+ - Event handlers: use `handle` prefix (e.g., `handleSubmit`)
57
+ - CSS classes: kebab-case (e.g., `user-profile`)
58
+ - **Must explicitly import Vant components** - auto-import is disabled for this component library project
59
+ - Use `interface` for type definitions, `type` for unions/intersections
60
+ - Props must specify types
61
+
62
+ ## Architecture
63
+
64
+ ### Directory Structure
65
+
66
+ ```
67
+ src/
68
+ ├── components/
69
+ │ ├── core/ # Core UI components (NavBar, Tabbar, Uploader)
70
+ │ ├── data/ # Business data components
71
+ │ │ ├── XReportForm/ # Dynamic form with JSON configuration
72
+ │ │ ├── XReportGrid/ # Data grid with reporting
73
+ │ │ ├── XForm/ # General form components
74
+ │ │ └── XOlMap/ # OpenLayers map integration
75
+ │ └── layout/ # Layout components
76
+ ├── stores/ # Pinia state management
77
+ ├── utils/ # Utility functions
78
+ ├── views/ # Page components
79
+ ├── router/ # Vue Router configuration
80
+ └── api/ # API layer
81
+ ```
82
+
83
+ ### Key Business Components
84
+
85
+ **XReportForm Component** (`src/components/data/XReportForm/index.vue`):
86
+
87
+ - Dynamic form generation from JSON configuration
88
+ - Supports field types: `input`, `datePicker`, `timePicker`, `dateTimeSecondsPicker`, `curDateInput`, `signature`, `images`, `inputs`, `inputColumns`
89
+ - Built-in validation with custom error messages
90
+ - Slot-based extensibility for complex layouts
91
+ - Integration with file upload and signature capture
92
+
93
+ **XReportGrid Component**:
94
+
95
+ - Data grid with reporting capabilities
96
+ - Print functionality integration
97
+ - Design mode for form configuration
98
+
99
+ **XOlMap Component**:
100
+
101
+ - OpenLayers integration for GIS functionality
102
+ - Location picker with coordinate transformation
103
+
104
+ ### State Management
105
+
106
+ - **Pinia stores** with persistence via `pinia-plugin-persistedstate`
107
+ - **User store**: Authentication, permissions, user data
108
+ - **Settings store**: Application configuration
109
+ - **Route cache store**: Performance optimization
110
+
111
+ ### API Layer
112
+
113
+ - **Axios-based HTTP client** with interceptors
114
+ - **Request/Response transformers** for v3/v4 API compatibility
115
+ - **Automatic token management** and error handling
116
+ - **Mock server integration** for development
117
+
118
+ ## Development Workflow
119
+
120
+ ### Server Configuration
121
+
122
+ - Development server runs on port 7190
123
+ - Mock server runs on port 8086
124
+ - Multiple API proxy endpoints configured in `vite.config.ts`
125
+
126
+ ### Build Process
127
+
128
+ - Code splitting: `third` (node_modules), `views` (business pages)
129
+ - Assets organized in `static/` directory with hashing
130
+ - CSS code splitting disabled for mobile optimization
131
+ - Legacy browser support via `@vitejs/plugin-legacy`
132
+
133
+ ### Git Hooks
134
+
135
+ - Pre-commit: `pnpm lint-staged` (ESLint auto-fix)
136
+ - Commit message: `pnpm commitlint` (conventional commits)
137
+
138
+ ## Micro-Frontend Integration
139
+
140
+ This is a main application for micro-frontend architecture:
141
+
142
+ - Uses `@micro-zoe/micro-app` for micro-app management
143
+ - Child apps register in `microApps.ts`
144
+ - Supports dynamic loading and routing
145
+ - Unmount function available at `window.unmount`
146
+
147
+ ## Mobile-Specific Features
148
+
149
+ - **@vant/touch-emulator** for desktop development
150
+ - **postcss-mobile-forever** for viewport handling
151
+ - **vite-plugin-pwa** for PWA capabilities
152
+ - **VConsole** for mobile debugging
153
+ - Dark mode support throughout the application
154
+
155
+ ## Common Patterns
156
+
157
+ ### Adding New Field Types to XReportForm
158
+
159
+ 1. Add type to `generateDefaultRequiredMessage()` function
160
+ 2. Add case in `formatConfigToForm()` switch statement
161
+ 3. Add template section in the component template
162
+ 4. Follow existing patterns for validation and data binding
163
+
164
+ ### Component Development
165
+
166
+ - Use `<script setup lang="ts">` syntax
167
+ - Explicitly import Vant components
168
+ - Define props with TypeScript interfaces
169
+ - Use `defineEmits` for events
170
+ - Follow existing component structure patterns
171
+
172
+ ### API Integration
173
+
174
+ - Use the existing HTTP client in `src/utils/http/`
175
+ - Follow v3/v4 API patterns established in the codebase
176
+ - Handle errors consistently with existing patterns
177
+
178
+ ## Testing & Quality
179
+
180
+ - **ESLint** with `@antfu/eslint-config`
181
+ - **TypeScript** strict mode
182
+ - **Vue TSC** for type checking
183
+ - **Commitlint** for commit message standards
184
+ - **Lint-staged** for pre-commit hooks
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "af-mobile-client-vue3",
3
3
  "type": "module",
4
- "version": "1.2.27",
4
+ "version": "1.2.29",
5
5
  "packageManager": "pnpm@10.12.3",
6
6
  "description": "Vue + Vite component lib",
7
7
  "engines": {
@@ -1,159 +1,159 @@
1
- <script setup lang="ts">
2
- import { deleteFile } from '@af-mobile-client-vue3/services/api/common'
3
- import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
4
- import {
5
- Button as vanButton,
6
- Uploader as vanUploader,
7
- } from 'vant'
8
- import { ref, watch } from 'vue'
9
-
10
- const props = defineProps({
11
- imageList: Array<any>,
12
- outerIndex: { default: undefined },
13
- authority: { default: 'user' },
14
- uploadMode: { default: 'server' },
15
- attr: { type: Object as () => { addOrEdit?: string }, default: () => ({}) },
16
- })
17
- const emit = defineEmits(['updateFileList'])
18
-
19
- const imageList = ref<Array<any>>(props.imageList ?? [])
20
-
21
- // 同步 props.imageList 到内部 imageList,保证每次变化都响应
22
- watch(() => props.imageList, (newVal) => {
23
- imageList.value = Array.isArray(newVal) ? [...newVal] : []
24
- }, { immediate: true })
25
-
26
- // 触发拍照
27
- function triggerCamera() {
28
- mobileUtil.execute({
29
- funcName: 'takePicture',
30
- param: {},
31
- callbackFunc: (result: any) => {
32
- if (result.status === 'success') {
33
- handlePhotoUpload(result.data)
34
- }
35
- },
36
- })
37
- }
38
-
39
- // 处理拍照后的上传
40
- function getImageMimeType(fileName: string): string {
41
- const ext = fileName.split('.').pop()?.toLowerCase()
42
- if (ext === 'jpg' || ext === 'jpeg')
43
- return 'image/jpeg'
44
- if (ext === 'png')
45
- return 'image/png'
46
- if (ext === 'gif')
47
- return 'image/gif'
48
- return 'image/png' // 默认
49
- }
50
-
51
- function handlePhotoUpload(photoData: any) {
52
- // 添加临时预览
53
- const mimeType = getImageMimeType(photoData.filePath)
54
- const tempFile = {
55
- uid: Date.now() + Math.random().toString(36).substr(2, 5),
56
- name: photoData.filePath.split('/').pop(),
57
- status: 'uploading',
58
- message: '上传中...',
59
- url: `data:${mimeType};base64,${photoData.content}`,
60
- }
61
-
62
- if (!imageList.value) {
63
- imageList.value = [tempFile]
64
- }
65
- else {
66
- imageList.value.push(tempFile)
67
- }
68
-
69
- const param = {
70
- resUploadMode: props.uploadMode,
71
- pathKey: 'Default',
72
- formType: 'image',
73
- useType: 'Default',
74
- resUploadStock: '1',
75
- filename: photoData.name,
76
- filesize: photoData.size,
77
- f_operator: 'server',
78
- imgPath: photoData.filePath,
79
- urlPath: `/api/${import.meta.env.VITE_APP_SYSTEM_NAME}/resource/upload`,
80
- }
81
- // 上传到服务器
82
- mobileUtil.execute({
83
- funcName: 'uploadResource',
84
- param,
85
- callbackFunc: (result: any) => {
86
- if (result.status === 'success') {
87
- const index = imageList.value.findIndex(item => item.uid === tempFile.uid)
88
- if (index !== -1) {
89
- imageList.value[index].uid = result.data.id
90
- imageList.value[index].id = result.data.id
91
- delete imageList.value[index].message
92
- imageList.value[index].status = 'done'
93
- imageList.value[index].url = result.data.f_downloadpath
94
- }
95
- }
96
- else {
97
- const index = imageList.value.findIndex(item => item.uid === tempFile.uid)
98
- if (index !== -1) {
99
- imageList.value[index].status = 'failed'
100
- imageList.value[index].message = '上传失败'
101
- }
102
- }
103
-
104
- if (props.outerIndex !== undefined)
105
- emit('updateFileList', imageList.value.filter(item => item.status === 'done'), props.outerIndex)
106
- else
107
- emit('updateFileList', imageList.value.filter(item => item.status === 'done'))
108
- },
109
- })
110
- }
111
-
112
- // 删除图片
113
- function deleteFileFunction(file: any) {
114
- if (file.id) {
115
- deleteFile({ ids: [file.id], f_state: '删除' }).then((res: any) => {
116
- if (res.msg !== undefined) {
117
- const targetIndex = imageList.value.findIndex(item => item.id === file.id)
118
- if (targetIndex !== -1) {
119
- imageList.value.splice(targetIndex, 1)
120
- if (props.outerIndex !== undefined)
121
- emit('updateFileList', imageList.value.filter(item => item.status === 'done'), props.outerIndex)
122
- else
123
- emit('updateFileList', imageList.value.filter(item => item.status === 'done'))
124
- }
125
- }
126
- })
127
- }
128
- }
129
- </script>
130
-
131
- <template>
132
- <div class="uploader-container">
133
- <van-button
134
- v-if="props.attr?.addOrEdit !== 'readonly'"
135
- icon="photograph"
136
- type="primary"
137
- @click="triggerCamera"
138
- >
139
- 拍照
140
- </van-button>
141
-
142
- <van-uploader
143
- v-model="imageList"
144
- :show-upload="false"
145
- :deletable="props.attr?.addOrEdit !== 'readonly' && props.authority === 'admin'"
146
- :multiple="props.authority === 'admin'"
147
- :preview-image="true"
148
- :before-delete="props.attr?.addOrEdit !== 'readonly' && props.authority === 'admin' ? deleteFileFunction : undefined"
149
- />
150
- </div>
151
- </template>
152
-
153
- <style scoped lang="less">
154
- .uploader-container {
155
- display: flex;
156
- flex-direction: column;
157
- gap: 16px;
158
- }
159
- </style>
1
+ <script setup lang="ts">
2
+ import { deleteFile } from '@af-mobile-client-vue3/services/api/common'
3
+ import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
4
+ import {
5
+ Button as vanButton,
6
+ Uploader as vanUploader,
7
+ } from 'vant'
8
+ import { ref, watch } from 'vue'
9
+
10
+ const props = defineProps({
11
+ imageList: Array<any>,
12
+ outerIndex: { default: undefined },
13
+ authority: { default: 'user' },
14
+ uploadMode: { default: 'server' },
15
+ attr: { type: Object as () => { addOrEdit?: string }, default: () => ({}) },
16
+ })
17
+ const emit = defineEmits(['updateFileList'])
18
+
19
+ const imageList = ref<Array<any>>(props.imageList ?? [])
20
+
21
+ // 同步 props.imageList 到内部 imageList,保证每次变化都响应
22
+ watch(() => props.imageList, (newVal) => {
23
+ imageList.value = Array.isArray(newVal) ? [...newVal] : []
24
+ }, { immediate: true })
25
+
26
+ // 触发拍照
27
+ function triggerCamera() {
28
+ mobileUtil.execute({
29
+ funcName: 'takePicture',
30
+ param: {},
31
+ callbackFunc: (result: any) => {
32
+ if (result.status === 'success') {
33
+ handlePhotoUpload(result.data)
34
+ }
35
+ },
36
+ })
37
+ }
38
+
39
+ // 处理拍照后的上传
40
+ function getImageMimeType(fileName: string): string {
41
+ const ext = fileName.split('.').pop()?.toLowerCase()
42
+ if (ext === 'jpg' || ext === 'jpeg')
43
+ return 'image/jpeg'
44
+ if (ext === 'png')
45
+ return 'image/png'
46
+ if (ext === 'gif')
47
+ return 'image/gif'
48
+ return 'image/png' // 默认
49
+ }
50
+
51
+ function handlePhotoUpload(photoData: any) {
52
+ // 添加临时预览
53
+ const mimeType = getImageMimeType(photoData.filePath)
54
+ const tempFile = {
55
+ uid: Date.now() + Math.random().toString(36).substr(2, 5),
56
+ name: photoData.filePath.split('/').pop(),
57
+ status: 'uploading',
58
+ message: '上传中...',
59
+ url: `data:${mimeType};base64,${photoData.content}`,
60
+ }
61
+
62
+ if (!imageList.value) {
63
+ imageList.value = [tempFile]
64
+ }
65
+ else {
66
+ imageList.value.push(tempFile)
67
+ }
68
+
69
+ const param = {
70
+ resUploadMode: props.uploadMode,
71
+ pathKey: 'Default',
72
+ formType: 'image',
73
+ useType: 'Default',
74
+ resUploadStock: '1',
75
+ filename: photoData.name,
76
+ filesize: photoData.size,
77
+ f_operator: 'server',
78
+ imgPath: photoData.filePath,
79
+ urlPath: `/api/${import.meta.env.VITE_APP_SYSTEM_NAME}/resource/upload`,
80
+ }
81
+ // 上传到服务器
82
+ mobileUtil.execute({
83
+ funcName: 'uploadResource',
84
+ param,
85
+ callbackFunc: (result: any) => {
86
+ if (result.status === 'success') {
87
+ const index = imageList.value.findIndex(item => item.uid === tempFile.uid)
88
+ if (index !== -1) {
89
+ imageList.value[index].uid = result.data.id
90
+ imageList.value[index].id = result.data.id
91
+ delete imageList.value[index].message
92
+ imageList.value[index].status = 'done'
93
+ imageList.value[index].url = result.data.f_downloadpath
94
+ }
95
+ }
96
+ else {
97
+ const index = imageList.value.findIndex(item => item.uid === tempFile.uid)
98
+ if (index !== -1) {
99
+ imageList.value[index].status = 'failed'
100
+ imageList.value[index].message = '上传失败'
101
+ }
102
+ }
103
+
104
+ if (props.outerIndex !== undefined)
105
+ emit('updateFileList', imageList.value.filter(item => item.status === 'done'), props.outerIndex)
106
+ else
107
+ emit('updateFileList', imageList.value.filter(item => item.status === 'done'))
108
+ },
109
+ })
110
+ }
111
+
112
+ // 删除图片
113
+ function deleteFileFunction(file: any) {
114
+ if (file.id) {
115
+ deleteFile({ ids: [file.id], f_state: '删除' }).then((res: any) => {
116
+ if (res.msg !== undefined) {
117
+ const targetIndex = imageList.value.findIndex(item => item.id === file.id)
118
+ if (targetIndex !== -1) {
119
+ imageList.value.splice(targetIndex, 1)
120
+ if (props.outerIndex !== undefined)
121
+ emit('updateFileList', imageList.value.filter(item => item.status === 'done'), props.outerIndex)
122
+ else
123
+ emit('updateFileList', imageList.value.filter(item => item.status === 'done'))
124
+ }
125
+ }
126
+ })
127
+ }
128
+ }
129
+ </script>
130
+
131
+ <template>
132
+ <div class="uploader-container">
133
+ <van-button
134
+ v-if="props.attr?.addOrEdit !== 'readonly'"
135
+ icon="photograph"
136
+ type="primary"
137
+ @click="triggerCamera"
138
+ >
139
+ 拍照
140
+ </van-button>
141
+
142
+ <van-uploader
143
+ v-model="imageList"
144
+ :show-upload="false"
145
+ :deletable="props.attr?.addOrEdit !== 'readonly' && props.authority === 'admin'"
146
+ :multiple="props.authority === 'admin'"
147
+ :preview-image="true"
148
+ :before-delete="props.attr?.addOrEdit !== 'readonly' && props.authority === 'admin' ? deleteFileFunction : undefined"
149
+ />
150
+ </div>
151
+ </template>
152
+
153
+ <style scoped lang="less">
154
+ .uploader-container {
155
+ display: flex;
156
+ flex-direction: column;
157
+ gap: 16px;
158
+ }
159
+ </style>
@@ -146,6 +146,14 @@ const slots = useSlots()
146
146
  // 当前组件实例(不推荐使用,可能会在后续的版本更迭中调整,暂时用来绑定函数的上下文)
147
147
  const currInst = getCurrentInstance()
148
148
 
149
+ // 列表底部的文字显示
150
+ function finishedBottomText() {
151
+ if (buttonState.value?.add && buttonState.value.add === true && (filterButtonPermissions('add').state === false || ((filterButtonPermissions('add').state === true && userState.f.resources.f_role_name.includes((filterButtonPermissions('add').roleStr))))))
152
+ return '已加载全部内容,如需新增请点击右上角的 + 号'
153
+ else
154
+ return '已加载全部内容'
155
+ }
156
+
149
157
  onBeforeMount(() => {
150
158
  initComponent()
151
159
  })
@@ -584,7 +592,7 @@ defineExpose({
584
592
  v-model:loading="loading"
585
593
  class="list_main"
586
594
  :finished="finished"
587
- finished-text="已加载全部内容"
595
+ :finished-text="finishedBottomText()"
588
596
  :immediate-check="isInitQuery"
589
597
  @load="onLoad"
590
598
  >
@@ -13,7 +13,7 @@ import {
13
13
  CellGroup as VanCellGroup,
14
14
  Form as VanForm,
15
15
  } from 'vant'
16
- import { computed, defineEmits, defineProps, nextTick, onBeforeMount, reactive, ref, watch } from 'vue'
16
+ import { computed, defineEmits, defineProps, inject, nextTick, onBeforeMount, reactive, ref, watch } from 'vue'
17
17
 
18
18
  interface FormItem {
19
19
  addOrEdit: string
@@ -74,6 +74,9 @@ const props = withDefaults(defineProps<{
74
74
  const emits = defineEmits(['onSubmit', 'xFormItemEmitFunc'])
75
75
  const userStore = useUserStore()
76
76
 
77
+ // inject
78
+ const formDataChange = inject('formDataChange', null)
79
+
77
80
  // 核心状态
78
81
  const xFormRef = ref<FormInstance>()
79
82
  const loaded = ref(false)
@@ -287,6 +290,16 @@ watch(() => props.formData, (newVal) => {
287
290
  }
288
291
  })
289
292
 
293
+ watch(
294
+ () => form.value,
295
+ (val) => {
296
+ if (typeof formDataChange === 'function') {
297
+ formDataChange(val)
298
+ }
299
+ },
300
+ { deep: true },
301
+ )
302
+
290
303
  // 组件挂载时初始化
291
304
  onBeforeMount(() => {
292
305
  initializeForm()
@@ -1,12 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  import XForm from '@af-mobile-client-vue3/components/data/XForm/index.vue'
3
3
  import { getConfigByName } from '@af-mobile-client-vue3/services/api/common'
4
- import {
5
- Button as VanButton,
6
- Tab as VanTab,
7
- Tabs as VanTabs,
8
- } from 'vant'
9
- import { defineEmits, defineProps, onBeforeMount, ref, watch } from 'vue'
4
+ import { Button as VanButton, Tab as VanTab, Tabs as VanTabs } from 'vant'
5
+ import { defineEmits, defineProps, onBeforeMount, onMounted, ref, watch } from 'vue'
10
6
 
11
7
  const props = withDefaults(defineProps<{
12
8
  configName?: string
@@ -34,16 +30,12 @@ const submitGroup = ref(false)
34
30
  const submitSimple = ref(false)
35
31
  const isInit = ref(false)
36
32
  const initStatus = ref(false)
37
- const propsData = ref({})
33
+ const propsData = ref<Form>({})
38
34
 
39
35
  // 组件初始化函数
40
36
  function init(params: Form) {
41
37
  initStatus.value = true
42
38
  propsData.value = {
43
- // configName: '',
44
- // serviceName: undefined,
45
- // groupFormData: () => ({}),
46
- // mode: '查询',
47
39
  configName: props.configName,
48
40
  serviceName: props.serviceName,
49
41
  groupFormData: props.groupFormData,
@@ -53,7 +45,6 @@ function init(params: Form) {
53
45
  formData.value = propsData.value.groupFormData
54
46
  getConfigByName(propsData.value.configName, (result) => {
55
47
  if (result?.groups) {
56
- // submitGroup.value = true
57
48
  groupItems.value = result.groups
58
49
  result.groups.forEach((group) => {
59
50
  if (!formData.value[group.groupName])
@@ -63,7 +54,7 @@ function init(params: Form) {
63
54
  })
64
55
  }
65
56
  else {
66
- submitSimple.value = result.showSubmitBtn
57
+ submitSimple.value = result?.showSubmitBtn
67
58
  groupItems.value = [{ ...result }]
68
59
  }
69
60
  isInit.value = true
@@ -85,24 +76,29 @@ async function submit() {
85
76
  emit('submit', formData.value)
86
77
  }
87
78
 
88
- // function initXForm(index: number) {
89
- // 获取自身示例
90
- // refs[`xFormListRef-${index}`].init({})
91
- // }
79
+ // 动态计算 offsetTop = var(--van-nav-bar-height) + 10px
80
+ const offsetTop = ref(0)
81
+ onMounted(() => {
82
+ const root = document.documentElement
83
+ const navBarHeight = getComputedStyle(root).getPropertyValue('--van-nav-bar-height')
84
+ offsetTop.value = Number.parseInt(navBarHeight, 10) || 60 + 10
85
+ })
92
86
 
93
87
  defineExpose({ init })
94
88
  </script>
95
89
 
96
90
  <template>
97
91
  <div v-if="isInit" id="x-form-group">
98
- <VanTabs scrollspy sticky>
92
+ <VanTabs scrollspy sticky :offset-top="offsetTop">
99
93
  <VanTab
100
94
  v-for="(item, index) in groupItems"
101
95
  :key="item.groupName ? (item.groupName + index) : index"
102
96
  :title="item.describe ? item.describe : item.tableName "
103
97
  >
104
- <div class="x-form-group-item">
105
- <!-- :ref="`xFormListRef-${index}`" -->
98
+ <div
99
+ class="x-form-group-item"
100
+ :class="{ 'is-last': index === groupItems.length - 1 }"
101
+ >
106
102
  <XForm
107
103
  ref="xFormListRef"
108
104
  :is-group-form="true"
@@ -125,10 +121,23 @@ defineExpose({ init })
125
121
 
126
122
  <style scoped lang="less">
127
123
  #x-form-group {
124
+ display: flex;
125
+ flex-direction: column;
128
126
  background-color: rgb(247, 248, 250);
129
- padding-bottom: 10px;
127
+ height: calc(100vh - var(--van-nav-bar-height) - 20px);
128
+ flex: 1;
129
+ overflow-y: auto;
130
+ // 让 Tabs 区域自适应剩余空间
131
+ .van-tabs {
132
+ flex: 1;
133
+ min-height: 0;
134
+ overflow: auto;
135
+ }
130
136
  .x-form-group-item {
131
- margin: 20px 0;
137
+ margin-bottom: 20px;
132
138
  }
133
139
  }
140
+ .x-form-group-item.is-last {
141
+ min-height: calc(100vh - var(--van-nav-bar-height) - 40px);
142
+ }
134
143
  </style>