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 +184 -0
- package/package.json +1 -1
- package/src/components/core/ImageUploader/index.vue +159 -159
- package/src/components/data/XCellList/index.vue +9 -1
- package/src/components/data/XForm/index.vue +14 -1
- package/src/components/data/XFormGroup/index.vue +31 -22
- package/src/components/data/XOlMap/utils/wgs84ToGcj02.js +154 -154
- package/src/components/data/XReportForm/DateTimeSecondsPicker.vue +208 -0
- package/src/components/data/XReportForm/index.vue +86 -116
- package/src/styles/app.less +5 -0
- package/src/views/component/XCellListView/index.vue +93 -14
- package/src/views/component/XFormGroupView/index.vue +39 -2
- package/src/views/component/XFormView/index.vue +0 -17
- package/src/views/component/XOlMapView/XLocationPicker/index.vue +118 -118
- package/src/views/component/XFormView/oldindex.vue +0 -70
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,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
|
-
|
|
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
|
|
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
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|
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>
|